RCIE-ジャンクのコード屋

主に自分のためにコーディングのTIPSを蓄積しています。

さらば Windows! Linux Mint 導入完全マニュアル

さらば Windows...

 近年(2025年秋)、Windows 11 はユーザーに優しくない方向に進化を続けていると 、多くのユーザーや開発者からの批判に晒されている。

  • ユーザーのディスクを不具合で勝手に暗号化する(BitLocker)
  • ユーザーのデータを意図に反してクラウド連携する(OneDrive)
  • Microsoftアカウントの利用を強制する
  • ゲームやサービスの広告を表示する
  • ユーザーの操作や画面に関する情報をバックグラウンドで送信する
  • Windows Update で PC が起動不能になる

 その背景もあり、Windows から OS を乗り換える人は増加の一途をたどっている。私もそのひとりである。
 本記事においては、移行先として広く人気のある選択肢「Linux Mint」の導入について解説していく。


Linux Mint へようこそ

 Linux は、コマンドプロンプトのようなハッカーの黒い画面を駆使する人のための OS 、というイメージが強い。
 しかし、初心者にやさしい Linux も存在する。Windows と同じような使用感をアピールする「Linux Mint」である。利用者が多いのには理由がある。

  • Web・文書作成・ゲームなどの日常的な用途なら、コマンド操作はほぼ必要ない
  • システムが安定している
  • ドライバ類を自力で用意する必要がない
  • 定期的なアップデートでセキュリティが高い
  • 無料
  • 利用者が多いため、盛んに情報交換されている

 人気のある Ubuntu、CachyOS を試しに使ってみたところ、私は Linux Mint を最も気に入った。日本語ローカライズの質が高く、初期環境の整備が非常に簡単だからだ。

 とはいえ、Windows と比較すると、つまずきやすい点もある。Linux Mint を導入する際のヒントを以下に示す。情報は、2025年11月時点(Linux Mint 22.2)のものである。

 皆様が快適なLinux Mint生活をスタートする助けになれば幸いである。


ステップ0:留意点

  • Linux OS に全般的な特徴として、PC の機種によって電源のスリープができないといった、電源周りの機能が不十分なところがある。起動は速いので、使わないときはシャットダウンすると割り切るのがよい。
  • AdobeMicrosoft Office は動作しない。同様の目的のための代替ソフトはある。Linux で遊べる Steam のゲームは 9 割ほどだと言われているが、厳重なチート防止システムのあるゲームは動作しないことが多い。


ステップ1:OS イメージを USB メモリに書き込む

 ここはあまり難しくない。

  • 4GB 以上の容量の USB メモリを準備する。
    • もともと入っていたデータは消えてしまう。注意。


ステップ2:Windowsパーティションを縮小する

 ここはあまり難しくないが、操作を誤ると PC のデータが消失する危険がある。

  • とりあえずは Windows / Linux を共存させる。
    • 共存させると、動作が不安定になるという話もあるが、とりあえずお試し目的で。
    • もし、Windows が必要なければ、あとでパーティションを更地にすればよい。
  • BitLocker が有効な場合は、オフにしておく。
  • Windows が使用している領域を縮小し、20GB 以上の空き領域を確保する。


ステップ3:初期設定用のスクリプトをダウンロード

 私の自家製のスクリプトだが、これがあれば「ステップ6」以降の作業があっという間に終了する。
 ダウンロードして、「C:\Temp」など、わかりやすい場所に保存しておくとよい。(Windows / Linux を共存させるなら、Windows でローカルディスクに保存したファイルは、Linux から参照可能)

  • 日本語入力環境の設定
    • 1-setup-mint-jp.sh(右クリック → 名前を付けてリンク先を保存)
    • 日本語入力システム「Fcitx5-mozc」のインストールと、関連する設定を自動で行うスクリプト

 それぞれの処理の詳細は、コメントで記述してある。ChatGPT などの生成AIを活用すれば、各処理の内容や文法について詳しく知ることができる。これを機にシェルスクリプトの学習に役立てていただきたい。


ステップ4:USB から PC を起動

 初めて挑戦する人は、何度か失敗するかもしれない。

  • ステップ1で作成した USB メモリを、挿したままにしておく。
  • PC を再起動する。
    • このとき、F1・F2・F10・F12・DEL キーに、指を乗せておく。
  • 電源が切れたタイミングから、F1・F2・F10・F12・DEL キーを連打する。
  • USB を起動デバイスの優先度の最上位に変更する。
    • 英語で書いてあることも多い。「Boot」や「Priority」などの単語に注目する。
  • 設定を保存して終了する。
    • 英語の場合は「Save and Exit」の単語に注目する。
    • F10 キーを押すと保存して終了できることが多い。


ステップ5:Linux Mint のインストール

 インストール自体は、非常に簡単なので心配ない。Windows を残すつもりなら慎重に。

  • インターネットに接続する。Wi-Fi の場合は右下のアイコンから接続する。
  • 左上の「Install Linux Mint」をダブルクリック。
  • Language(言語) に「日本語」を選択。
  • あとは「続ける」をクリックして進むだけ。
    • デフォルトでは「Windows OS を残してインストール」の設定のはず。
    • 【注意喚起】間違って「ディスクを削除して〜」を選ばない! Windows とデータが消失する!
  • インストールにかかる時間はデバイスの速度によるが、20分程度。


ステップ6:日本語入力・Google Chrome のインストール

 インストール直後の状態では、日本語の表示はできるが日本語入力ができないため設定が必要。また、多くの人が使っている Google Chrome のインストールもここでは扱う。

 以下に、ステップ3でダウンロードしたスクリプトを実行していく。コマンドさえ実行してしまえば、設定は自動でしてもらえる。

  • C:\Temp に存在するスクリプトを、ファイルマネージャーで探し出す。
    • 左側メニューの「デバイス」 >「〇〇GB ボリューム」これが C ドライブに相当する。
  • 1-setup-mint-jp.sh や 2-install-google-chrome.sh を右クリックして「プロパティ」
  • ファイルマネージャーの何もないところを右クリックして「端末で開く」を選択する。
    • コマンドを入力して終了するのを待つ。「sudo ./1-setup-mint-jp.sh」
    • コマンドを入力して終了するのを待つ。「sudo ./2-install-google-chrome.sh」
    • sudo とは「super do」管理者権限で実行せよ、という意味。


ステップ7:文字コード Shift-JIS への対応

 Windows で作成したテキストファイルを開くと「�@�\�E�」のように文字化けしてしまう。
 これは、Windows では日本語文字の符号化に Shift-JIS を使用しており、Linux では UTF-8 を使用していることが原因である。テキストエディタの設定を変更することで対処が可能。

  • 端末を開く(デスクトップを右クリック > 端末で開く)
    • コマンドを入力する。「gsettings set org.x.editor.preferences.encodings auto-detected "['UTF-8', 'CURRENT', 'CP932', 'SHIFT_JIS', 'ISO-2022-JP', 'EUC-JP']"」


ステップ8:基本的な使い方

 Linux Mint のセットアップは完了したので、以下に、Windows で馴染みのある操作が Linux Mint で何に対応するかを説明する。

  • スタートメニュー
    • 左下の「m」のエンブレムが、Windows スタートメニューに相当する。単に「メニュー」と呼ばれる。
  • タスクマネージャー
    • メニュー > システム管理 > システムモニター で CPU や ネットワークの使用状況を確認できる。
  • コントロールパネル
    • メニュー > 設定 > システムの設定 で、画面表示やマウスなどの PC の各種設定を変更できる。
  • プログラムのインストール・アンインストール
    • メニュー > システム管理 > ソフトウェアマネージャー を用いて、有名な各種プログラムをインストール・アンインストールすることができる。


おわりに

 これでようやく、Windows から脱却することができる。Microsoft は「Windows 10 が最後の Windows だ」とかつて宣言し、大いに盛り上がったが、Windows 11 は既にユーザーの支持を失っており、確かにその宣言通りになりつつある。

 Linux Mint のシェアが拡大し、情報交換と機能の充実がますます活発になることを祈念して、この記事を締めくくる。

複雑なSQLのクエリはCTEを使え

結論

複雑なSQLのクエリは、CTEを使って書くと、非常に読みやすくなるのでおすすめです。

まず、CTEとは、何か

CTEとはCommon Table Expressions(共通テーブル式)の略です。
SQLの中に書くサブクエリに名前をつけることができます。

具体的なケースを考えてみる

例として、以下のようなDB構造とします。

  • 営業所マスタ(北海道と東京都の営業所があります)
営業所ID 営業所名 所在地
C001 札幌営業所 北海道札幌市
C002 函館営業所 北海道函館市
C003 新宿営業所 東京都新宿区
  • 従業員マスタ(年齢が様々です)
従業員ID 氏名 営業所ID 生年月日
E1200 相原相太 C001 1980-02-04
E1201 猪川猪助 C002 1955-12-31
E1202 梅田梅子 C003 1956-08-30
  • 従業員家族マスタ(年齢が様々です)
従業員ID 家族枝番 氏名 生年月日
E1200 1 相原百代 1950-03-12
E1200 2 相原愛花 1988-09-09
E1201 1 猪川祥子 1958-07-08

それでは、以下のようなデータを取得しようと考えます。
「北海道の営業所の高齢従業員や高齢家族を抽出したいなあ」

  1. 営業所の所在地が「北海道」から始まるような営業所について、
  2. そこに所属する、生年月日が1960年より前の従業員、または、従業員家族について、
  3. 営業所名、氏名、生年月日、家族区分(はい/いいえ) を取得する。

期待する結果は次の通りです。

営業所名 氏名 生年月日 家族区分
函館営業所 猪川猪助 1955-12-31 いいえ
札幌営業所 相原百代 1950-03-12 はい
函館営業所 猪川祥子 1958-07-08 はい

(相原相太、相原愛花は生年月日が条件を満たさない)
(梅田梅子は営業所の所在地が条件を満たさない)

通常の記述

SELECT -- まずは従業員について個人情報を取得する。
  営業所マスタ.営業所名
, 従業員マスタ.氏名
, 従業員マスタ.生年月日
, 'いいえ' AS 家族区分
FROM 従業員マスタ
INNER JOIN 営業所マスタ
  ON 営業所マスタ.営業所ID = 従業員マスタ.営業所ID
WHERE 営業所マスタ.所在地 LIKE '北海道%' -- 【重複】
  AND 従業員マスタ.生年月日 < '1960-01-01' -- 【重複】

UNION ALL
SELECT -- 次に、従業員家族について個人情報を取得して統合する。
  営業所マスタ.営業所名
, 従業員家族マスタ.氏名
, 従業員家族マスタ.生年月日
, 'はい' AS 家族区分
FROM 従業員家族マスタ
INNER JOIN 従業員マスタ
  ON 従業員マスタ.従業員ID = 従業員家族マスタ.従業員ID
INNER JOIN 営業所マスタ
  ON 営業所マスタ.営業所ID = 従業員マスタ.営業所ID
WHERE 営業所マスタ.所在地 LIKE '北海道%' -- 【重複】
  AND 従業員家族マスタ.生年月日 < '1960-01-01' -- 【重複】

見ると分かるように、「所在地が北海道から始まる営業所」という条件が2回も出現しています。
さらに、「生年月日が1960年より前」という条件も2回出現しています。
これは、DRY原則(一度書いたものは繰り返すな)に従っていません。
これを、CTEを用いて書き直してみましょう。

CTEを用いた記述

WITH
  -- 所在地が北海道から始まる営業所、これを名前を付けて定義する。
  北海道営業所 AS (
    SELECT 営業所ID, 営業所名 FROM 営業所マスタ
    WHERE 所在地 LIKE '北海道%' -- 【共通化】
  )
  -- 従業員のデータ、従業員家族についての個人情報を取得して統合する。
  , 個人情報 AS (
    SELECT -- 従業員
      T.営業所名
    , T2.氏名
    , T2.生年月日
    , 'いいえ' AS 家族区分
    FROM 北海道営業所 T -- (上で定義した)
    INNER JOIN 従業員マスタ T2
      ON T2.営業所ID = T.営業所ID

    UNION ALL
    SELECT -- 従業員家族
      T.営業所名
    , T3.氏名
    , T3.生年月日
    , 'はい' AS 家族区分
    FROM 北海道営業所 T -- (上で定義した)
    INNER JOIN 従業員マスタ T2
      ON T2.営業所ID = T.営業所ID
    INNER JOIN 従業員家族マスタ T3
      ON T3.従業員ID = T2.従業員ID
  )
-- 最終的に、生年月日で結果を絞る
SELECT * FROM 個人情報 -- (上で定義した)
WHERE 生年月日 < '1960-01-01' -- 【共通化】

このようにすることで、営業所の条件や年齢の条件が将来変更になった場合も、変更箇所が1か所で済むようになり、保守性が高まります。

また、クエリ全体が

  1. まず、北海道の営業所を定義する。
  2. 次に、それを使って従業員と家族の情報を集める。
  3. 最後に、年齢で絞り込む。

という手順で書かれているため、何をしているのかが非常に理解しやすくなりました。

処理の順番に従ってWITH句の中身を書いていけば、複雑な処理を記述する際にも、「SELECT の中に SELECT があり、さらにその中に…」といった読みづらいネスト地獄を避けることができます。

規範2.0(古くて新しい、人生の羅針盤)

「規範2.0」は、ある哲学者によって考案された、人生・家族・共同体・社会の知恵です。

【条文】

0.この規範は変更できる。ただし、その意義と変更履歴を記録に残せ。
1.日の出の後に太陽のもとに参り、感謝の祈りを捧げよ。
2.日暮れの後には闇と尊び、安息を招き入れよ。
3.出かける前には足を清め、旅の無事を祈願せよ。
4.偏りは誘惑であり、心身を蝕む魔物である。偏りを恐れよ。
5.あらゆる邪悪は、目・耳・口・鼻より入る。直感に従い、己を守れ。
6.厚意に対しては同等の厚意で応えよ。悪意に対しては半分の悪意で応えよ。
7.己の力ではなく、他者に与えた喜びで、己の価値を示せ。
8.他者の誇りは、その人生に等しい。限りなく丁重に扱え。
9.知力・体力・胆力・財力を貪欲に求めよ。だが、決してこれに飲まれるな。
10.富の一部は、誰もが救わない者のために、陰で捧げよ。
11.人は弱い。強くなるため、群れを成せ。
12.若者の無限の活力は、子育てに必須の、有限な資産である。
13.理想と大望を持て。次世代への援助を通して、間接的にこれを実現せよ。

【意義】

0.この規範は変更できる。ただし、その意義と変更履歴を記録に残せ。

  • この条文は、規範全体が古びた決まり事にならず、私たちと共に成長する「生きた知恵」であり続けるための、最も大切な約束です。時代や人生の変化に合わせて見直せることで、私たちはただ従うのではなく、「なぜこれが大切なのか」を自ら考え、納得して実践することができます。また、変更の理由を記録に残すことは、先人への敬意を示すと共に、未来の世代へと思いを繋ぐ「対話の架け橋」となります。

1.日の出の後に太陽のもとに参り、感謝の祈りを捧げよ。

  • この習慣は、一日を豊かに始めるためのおまじないです。朝の光を浴びることで「体内時計」が整い、心と身体が健やかなリズムを取り戻します。陽の光を浴びながら何かのありがたみに思いをはせることで、私たちは生かされている「謙虚さ」を思い出し、ポジティブな気持ちで一日をスタートできるようになります。

2.日暮れの後には闇と尊び、安息を招き入れよ。

  • 夜の闇を尊ぶとは、人工的な光を遠ざけるということです。照明や画面などの光や情報から離れることで、心と身体は深く休息し、質の良い睡眠が得られます。そうして生まれた静けさは、家族との温かい対話の機会となり、何よりも自分の健康を最優先するという、自分自身への優しさの実践となります。

3.出かける前には足を清め、旅の無事を祈願せよ。

  • 出発前に毎回決まった行動を繰り返すことで、心を安定させる効果があります。足を清める行為は、気持ちを活動モードへと切り替えるスイッチとなり、無事の祈願により安全への意識が高まります。足の清潔と健康を保ちながら、一日を無事に過ごすための気持ちの準備が整います。

4.偏りは誘惑であり、心身を蝕む魔物である。偏りを恐れよ。

  • 「〜だけが良い」という偏った考えは、分かりやすくて魅力的ですが、とても危険です。食事や運動の偏りは身体の健康を損ない、行き過ぎた執着は精神の安定を乱します。また、様々な意見に耳を傾けるバランス感覚は、賢明な判断力を育て、他者との無用な対立を避ける助けとなります。偏りを恐れ、バランスを愛することは、健やかな人生を送るための基本姿勢です。

5.あらゆる邪悪は、目・耳・口・鼻より入る。直感に従い、己を守れ。

  • 論理では見抜けない危険から身を守るには、あなたや親しい人の「直感」を信じることが大切です。「何となく嫌な感じ」がするのは、危険な相手や心を汚す情報、病気の原因なものからあなたを守るための警告サインです。その感覚は、あなたやあなたの仲間の膨大な経験が生み出した信頼できる価値基準です。勇気をもって距離をおきましょう。

6.厚意に対しては同等の厚意で応えよ。悪意に対しては半分の悪意で応えよ。

  • 親切には親切で返すことで、温かい信頼関係を築きましょう。一方、理不尽な悪意には冷静に抵抗することで、搾取から自分の尊厳を守ります。しかし、報復を半分に留めることで、憎しみの連鎖は収束へと向かいます。「半分の悪意」は、感情的な報復に限らず、冷静な抗議、無視、公的な手続きなどの理知的な対応も含みます。

7.己の力ではなく、他者に与えた喜びで、己の価値を示せ。

  • 私たちは、生きる中で「能力や財力を人と比べる苦しみ」に陥りやすいものです。そこで「誰かを喜ばせたか」を基準にすれば、その苦しみから解放されます。誰かを笑顔にできた経験は、揺るぎない自己肯定感となり、自然と協力的な人間関係が生まれます。その実感が、私たちの人生に深い意味を与えてくれます。

8.他者の誇りは、その人生に等しい。限りなく丁重に扱え。

  • この規範は、豊かな人間関係のための心得です。たとえ正しくても、その「正しさ」というナイフで相手の誇りを傷つけてはいけません。相手の誇りを尊重することは、深くかけがえのない関係を築くための大原則です。また、相手の立場を理解し、花を持たせる姿勢は、協力関係を引き出す優れた交渉術でもあります。

9.知力・体力・胆力・財力を貪欲に求めよ。だが、決してこれに飲まれるな。

  • 力と賢く付き合うためには心構えが必要です。まず、人生を切り拓くには力が必要であり、知力や財力を高める努力は不可欠です。しかし、力を求めるあまり、人生の目的と手段が転倒してしまう危険を忘れてはいけません。力はあくまで「道具」であり、それを持つ者には、正しく使う責任が伴います。

10.富の一部は、誰もが救わない者のために、陰で捧げよ。

  • 富を手放すことは、力を持つ者が傲慢さに陥らないための、心のブレーキです。「陰で」捧げることは、自己満足や承認欲求からあなた自身を守り、相手の尊厳を守り、そして相手との予期せぬトラブルからあなたを守ります。誰にも知られず、見返りを求めないことこそが、力の正しい使い方を誓う崇高な行いです。

11.人は弱い。強くなるため、群れを成せ。

  • 人類が繁栄できたのは、協力しあう「群れ」の力があったからです。現代社会で忘れがちな「人は弱い」という事実を認め、仲間と支え合いましょう。「一人じゃない」という安心感は何物にも代えがたい支えとなり、仲間と力を合わせれば、一人では不可能なことも成し遂げられます。互いに支え合うことこそが、真の強さの源です。

12.若者の無限の活力は、子育てに必須の、有限な資産である。

  • 若い頃、私たちは体力や時間が無限にあるように感じるかもしれません。しかし、年齢と共に体力や生殖能力は自然に低下し、時間は決して戻りません。子を産むことを強制するものではありませんが、もし将来「子どもを育てたい」と思うなら、若さという有限の資産をどう使うか、自分の手で人生を設計してください。

13.理想と大望を持て。次世代への援助を通して、間接的にこれを実現せよ。

  • あなたの理想は、たとえそれが完成しなくても終わりではありません。子や教え子といった若者へ知識や経験を伝えることで、あなたの夢は、世代を超えた大きな物語へとつながります。自分の人生を未来への贈り物とすることで、有限な人生は無限の可能性へと大きく転換します。

生成AIによる無限推理ミステリー同好会

遊び方

以下のボタンでコピーして、AI に貼り付けるだけ。
AI が推理ミステリーのゲームマスターをしてくれるよ。
さあ、キミも名探偵になりきって、事件を解決しよう。

細かいルール

  • 事件の真相を暴いて、犯人を自供させたらクリア!
  • 犯人の数は、1人(60%)・2人(20%)・3人(10%)・他殺に見せかけた自殺(10%) の中から確率で選ばれる。共犯者がいると、けっこう手ごわい。
  • 40ターンで解決できないと迷宮入り、という回数制限がある。新たな情報が出ないターン(たとえば情報整理のターン)は、猶予してくれるので安心。

動作環境 (2025年3月時点)

変更履歴

  • 2025/03/28 終盤の推理をAIがプレイヤーの代わりに補助しすぎる問題を改善。
  • 2025/03/27 乱数シード値が高確率で過去の使いまわしになっていた問題を改善。
  • 2025/03/25 現代日本を舞台にしようと不正を働くAIの暴走を抑制。多少のルール軽量化。
  • 2025/03/21 AIがプレイヤーの推理に忖度して真相を捻じ曲げようとするのを、ある程度抑止。
  • 2025/03/20 プロンプトの抜本的改革。文書全体のマークダウン形式化。多様な舞台設定をルール化。

技術的な話

このAI用プロンプトは、かなり頑張って作りこんだ。

ルールの複雑さの限界

あまりにもルールが少なすぎると、推理ミステリーが破綻するし、
ルールが多すぎると今度はAIの扱える情報量を超えてしまう。
このバランス感覚が結構難しい。
特に Gemini 2.0 Pro はルールが長いと指示に従うのが難しくなる。

AIに優しい書き方

AI(LLM)は自然言語を理解するように作られている。
そのため、プログラミング的な書き方をするよりも、日本語的な書き方の方が負担が少ない。

  • JSON形式:{内部情報={容疑者リスト=[容疑者1={}, 容疑者2={}, 容疑者3={}]} )
  • 日本語:[容疑者1]、[容疑者2]、[容疑者3]を生成し、[内部情報]に登録する。

意外だが、JSON形式は、LLMの注意力リソースを圧迫し、関係ない箇所のミスを誘発する。

構造化のコツ

構造化(文書を内容ごとに整理すること)をすると、複雑な指示も理解できるようになる。
そのために Markdown(マークダウン)という形式に沿った書き方をするのがコツ。
 # 見出し
 ## 子見出し
 ### 孫見出し
 - 本文~~~。
 ## 子見出し
 - 本文~~~。
実行順は上から下に向けて書くようにする。詳細ルールは分離して、文書の下の方にまとめて書く。

アドリブの設定追加

ゲームマスターは最初から、{被害者, 容疑者, 物的証拠, 証言} を決定してくれている。
しかし TRPG あるあるだが、遊んでいくうちにアドリブで設定が増えていくことがある。
そんな時は、矛盾が起こらないように自然に設定を追加していく仕様になっている。
例えば、ゲーム開始時には決めていなくても「被害者の部屋の書類を読む」と言えば、書類の内容を生成してくれる。

証拠同士の矛盾を防ぐ

矛盾なく設定を追加するのに重要な役割を果たしているのが「有向グラフ」。
物的証拠や証言などの要素について、関係あるもの同士を、仮想的な矢印でつないでいる。
矢印を辿ることによって、要素の矛盾を高速に見つけることができる。
例)「(ナイフの指紋)は、(自殺の遺書)の信頼度を否定する」
内部的なアルゴリズムをプロンプトで指定すると、性能が上がるという発見があった。

ランダム生成が苦手

LLMにとって、「ランダムな数値の生成」はかなり難しい問題であると判明した。
「適当な6桁の数値を作って」とお願いすると、123456 とかを高頻度で出力する。
そうならないために、わざわざ「珍妙奇怪で意味不明な発想の一文」を生成して数値化する、という珍妙奇怪なアプローチを取っている。

数値計算が苦手

指数対数・平方根・剰余が苦手でよく間違える。信用してはいけない。
「123456789 を 1000000 で割った余りは?」と聞くと「345789」と間違える。
しかし、代わりに「下位6桁は?」と聞くと正しく「456789」と答えてくれる。
足し算・引き算・掛け算・割り算・下位n桁 の演算で完結させると吉。

AIは2020年代が好き

LLMが「ランダムな時代」を選ぶと、ほぼ2020年代になるため、乱数で厳密に決める必要がある。
仮に乱数で1985年を引いたとしても、勝手に2024年に改変することが非常によくある。
これは実に闇が深い問題であり、確実な解決方法があるわけではない。
「計算の過程を表示せよ」と強制すると、ある程度改善する。
改善するだけであって完全ではない。途中式を省いてプレイヤーを欺くこともしばしば。

各ルールの効果と存在意義

1. 推理ミステリー概要

AIが単なる情報提供者ではなく、ゲーム体験の演出者としての自覚を持つための核となる。

2. 設定データと証拠リストの管理

ゲームの状態を一元的に管理するための「データベース」の構造定義。

3. 処理手順

ゲームを進行させるための具体的なステップ(アルゴリズム)を定めたもの。

3.1 初期処理
3.1.1 シード値の設定

ゲームごとに異なるシード値を設定することにより、時代や国を多様化し、リプレイ性を高める。

3.1.2 犯人の数の決定

内部データの下位2桁に基づき、犯人が単独犯、共犯(2人、3人)、または「他殺に見せかけた自殺」となるかを決定する。
この分岐により、事件の性質が変化し、プレイヤーが取り組むべき推理の方向性や難易度が自動的に決まる。

3.1.3 日時の決定

乱数の特定の数値処理を通じて年を算出することで、シナリオの時代背景を定める。
年だけでなく、捜査技術・機械・道具のレベルも内部データとして記録するため、事件の進行や証拠の提示に説得力を持たせる。

3.1.4 国の決定

乱数の特定の桁を使って国を決定することで、シナリオがどの国で起こるかをランダムに選定できる。
選ばれた国に応じて、文化的背景(例えば、伝統や現代性の交錯)が記録され、事件の雰囲気や証言のスタイルに影響を与える。

3.1.5 被害者の生成

法令順守度や人気度などの属性をランダムに設定し、個性的な被害者を創出する。
被害者が抱える秘密(過去の犯罪、恋愛関係、金銭関係、罪の意識など)を設定することで、事件の動機や背景に奥行きを与える。

3.1.6 容疑者の生成

容疑者3人に対して読みやすい名前と、各自の外見・身分・性格・口調、そして人間関係を設定することで、事件に対する異なる視点や動機が生まれる。
犯人の可能性を内部データに明示し、シナリオの分岐や推理の展開に寄与する。

3.1.7 計画的犯行トリックの作成

犯行の流れをシミュレートし、それに伴う証拠を生成することで、論理的な推理が可能な土台を築く。

3.1.8 犯人による偽装工作

推理を困難にし、面白みを増すための要素。単純な犯行ではなく、プレイヤーを惑わす「偽の手がかり」や「隠蔽」を導入する。

3.1.9 ミスリード要素

特に犯人が少ない場合に、意図的に無実の容疑者に疑いが向くような状況を作り出し、推理の難易度と面白さを調整する。

3.1.10 ゲーム開始処理

タイトル、舞台設定、時代背景、文化的背景、ゲーム内日時、事件発覚の経緯などをまとめ、プレイヤーに初期状況を提供する。
被害者と容疑者の情報、事件現場の状況、行動選択肢を提示することで、プレイヤーがどこから推理を始めるかの指針となる。

4. ターン処理(ゲーム終了まで反復)

4.1 プレイヤーに対する応答

プレイヤーの行動や発言に応じてシナリオを進行させ、随時新たな手がかりを内部データに追加することで、ゲームの展開に柔軟に対応する。
プレイヤーの発言に便乗・否定などの形で自発的な発言を促すことにより、対話形式の推理ゲームとしての臨場感とインタラクティブ性を高める。
「迷宮入りまで、あと40ターン」といったターン制限の表示により、ゲーム進行に緊張感と期限を設け、プレイヤーに対して時間内に推理を完結させるプレッシャーを与える。

4.2 犯人の追及場面

推理が一定の精度(80%程度)に達した場合に犯人が自供する設定により、プレイヤーの推理の正確性がゲームの進行に直結する。
犯人に共犯者がいる場合は共犯の証拠が示されるまで自供しないことで、真相に至るまでの難易度と緊迫感を増加させる。

4.3 救済措置

推理が停滞している場合に、無実の人やその他のキャラクターがヒントを提供することで、プレイヤーが完全に行き詰まるのを防ぐ。
救済措置によって、プレイヤーの行動が実行不可能な場合や、内部状態が増えなかった場合にも進行を助ける役割を果たす。

4.4 自殺断定、降参判定

プレイヤーが事件の真相をどのように断定するかに応じて、事件解決(クリア)、迷宮入り(ゲームオーバー)、または特殊処理(真相の改変)といった分岐を設けることで、エンディングの多様性を確保する。
プレイヤーの推理と最終決定がゲームの結末に直接影響するため、最終的な判断に対する責任感と緊張感を生み出す。

4.5 情報整理

10・20・30ターン目に証拠やストーリーの流れを整理してあらすじを提示することで、プレイヤーが過去の情報を再確認し、推理の方向性を見直す助けとなる。
定期的な情報整理により、プレイヤーが行き詰まった際の救済措置とも連動し、全体の進行状況を明確に保つ役割がある。

5. 終了処理

エピローグを創作し、事件の全容の種明かしを行うことで、プレイヤーが推理の成果を確認し、物語を一つの完結として締める。
プレイヤーの行動についての感想や印象を述べることで、今後のシナリオ改善や、プレイヤーの参加体験を深める要素となる。

6. 詳細ルール

6.1 文体、没入感向上

状況説明は簡潔で厳かな文体、プレイヤーへの応答は親しみやすい文体とすることで、全体のトーンを統一し、プレイヤーにとって読みやすく、かつ没入感のある表現を実現する。
ゲームマスター視点でのメタ発言は控えるというルールにより、プレイヤーがゲーム世界に没入しやすくなる。

6.2 無実・犯人の行動指針、証拠スコア、矛盾チェック

無実の行動方針と犯人の行動指針を定めることで、各キャラクターの言動や反応に一貫性が生まれ、ゲーム内のロールプレイが充実する。
物的証拠や証言に対して、ネタバレ度に応じた証拠スコアを設定する指示は、重要な証拠の発見や取得を困難にすることで、プレイヤーの推理の難易度を調整する役割を果たす。
有向グラフを用いた矛盾チェックにより、複数の証拠間の整合性を保ち、シナリオ全体の信頼性を向上させる。

6.3 LLM記憶領域と情報秘匿

ゲーム進行と無関係な情報の削除、内部データの優先保持を指示することで、対話のコンテキストを軽量化し、動作を高速化する。
内部データと計算過程の秘匿を徹底することで、プレイヤーが直接核心情報にアクセスできないようにし、推理の余地を確保する。

7. 全体としての効果

各項目が相互に連携し、補完し合うことで、複雑でありながらも破綻のないゲーム体験を提供することを目指している。

架空の政策についてのAIディベート 結婚促進政策

ChatGPTに、架空の結婚促進政策についてディベートをしていただいた。

結婚促進政策に関するディベート

政策概要

この政策は、初めて出産する女性とその夫に対し、初婚時の年齢に応じた金額を支給する制度です。支給金額の計算式は以下の通りです:

支給金額 [万円] = (38 - 初婚時の年齢) × 5

  • 初婚年齢 20歳 → 出産時の支給金額 = (38 - 20) × 5 = 90万円
  • 初婚年齢 30歳 → 出産時の支給金額 = (38 - 30) × 5 = 40万円
  • ※25歳で初婚、30歳で再婚した女性が32歳で出産した場合、適用されるのは初婚時の年齢25歳となり、(38 - 25) × 5 = 65万円が支給されます。

    以下は、初婚時の年齢に基づく支給制度に関して、政策推進派と反対派の政治家によるディベートシミュレーションです。

    推進派:政策実現を訴える政治家

    皆さん、我が国は少子化という深刻な問題に直面しています。
    この政策は、初婚時の年齢に基づいて若年層の家庭に高額な支給金を提供することで、安心して結婚し子どもを持つ環境を整えることができます。
    若い家庭が経済的に支えられることで、早期の結婚と出産が促進され、将来的な人口減少を防ぐ重要な一手となるでしょう。

    また、初婚時の年齢が若い場合は、再婚者でも同様に高い支給が受けられる仕組みを導入することで、経済的な公平性も担保しています。
    これは、全ての家庭が安心して未来を築くための基盤作りであり、我が国の未来に向けた投資です。

    反対派:政策撤廃を訴える政治家

    皆さん、この政策は一見魅力的に見えるかもしれませんが、実際には国民の多様なライフスタイルを無視し、若さだけを評価する不公平な制度です。
    晩婚者や高齢の未婚者は、当然ながらこの制度の恩恵を受けにくくなり、結果として社会全体の公平性を損なう恐れがあります。

    さらに、短絡的な経済刺激策に頼ることで、根本的な少子化問題子育て支援の本質的な課題を解決できるとは到底言えません。
    政府はもっと実効性があり、すべての国民に配慮した政策を追求すべきです。
    私は、この政策が国民にとって真に有益な選択肢ではないと確信しています。

    推進派:再反論

    確かに、すべての生き方を尊重する必要があります。
    しかし、現実問題として、若い家庭に対する経済支援は、将来的な労働力や社会の活力を生み出すためにも重要です。
    短期的な経済負担を補うことで、長期的には国全体の発展に寄与するのです。

    また、再婚者にも初婚時の年齢が適用される仕組みは、制度の公平性を担保するための措置です。
    制度自体は、国民一人ひとりの選択を尊重しながら、未来への投資としての役割を果たすものであります。

    反対派:再反論

    しかしながら、結婚や出産は経済的なインセンティブだけで左右されるべき問題ではありません。
    自由な選択を尊重すべきであり、国家が個々のライフプランに過度に介入するのは好ましくありません。
    また、制度の一面的な実施は、長期的な財政負担や他の社会制度との整合性に疑問が残ります。

    真に国民の幸福を追求する政策は、もっと包括的で、教育、保育、労働市場改革など多方面からの支援が必要です。
    単一の支給制度に依存する政策は、決して我が国の未来を豊かにする解決策ではありません。

    議論は、個の権利の重視か、社会全体の存続性の重視か、という観点に収束した。
    代議士が政策の問題点などを発見したり、反対意見に対する準備をしたりするのにも、AIが有効であるように思える。

    Chrome拡張を自作しよう!広告ブロック編

    何をするか

    Chrome拡張を自作する。
    今回の題材は、広告ブロック。
    読者の方も自分だけのChrome拡張を作ってみよう。

    指針

    1. Chrome拡張の manifest ファイルを作成する
    2. 広告ブロックの JavaScript を書く
    3. Google Chrome拡張機能を読み込む

    Chrome拡張の manifest ファイルを作成する

    このChrome拡張の名前を「MyAdBlock」とする。

    [manifest.json] を以下の通り作成する。


    {
      "name":"MyAdBlock",
      "description":"広告をへらす",
      "version":"1.0.0",
      "manifest_version":3,
      
      "content_scripts":[
        {
          "matches":["*://*/*"],
          "js":["script.js"],
          "run_at":"document_end",
          "all_frames":false
        }
      ]
    }
    



    description … Chrome拡張の説明文(広告をへらす)
    matches … Chrome拡張を実行するURL(すべてのURL)
    js … 動作する Javascript(後述)
    run_at … 実行タイミング(ページ読込終了後)

    広告ブロック処理の JavaScript を書く

    実現したい要件は以下の通り。

    • <div class="global-ad-widget"> のような、いかにも広告らしいhtmlタグを探して非表示にする。
    • ページ読込後に遅れて出現したタグも非表示の対象にする。
    • 連続してタグが出現した場合にも、処理の負荷が高くなりすぎないようにする。

    [script.js] を以下の通り作成する。


    let adBlockCount = 0;    // 広告ブロックした回数
    
    function 広告を非表示(){
        document.querySelectorAll('*').forEach(elem => {
            if(広告であるか判定(elem)){
                adBlockCount++;
                console.log(`AdBlock(${adBlockCount})`, elem);
                // 広告のタグを非表示にする
                elem.style.display = "none";
            }
        });
    }
    
    // 広告に関連するキーワードを検出する正規表現
    // 例:-ad- -ads- -adsby -adsense -adserver -adspace -advertise
    const rxAdsPattern = /(^|[\-_ ])ad(s?($|[\-_ ])|sby|sense|server|space|vert)/i;
    // 例://xxx.ad? //xxx/ad/ //ad-xxx //xxx?ad=
    const rxAdsSrc = /\/\/(.+?[&=\/\.\-\?])?ad(s?($|[&=\/\.\-\?])|sby|sense|server|space|vert)/i;
    // 広告スキップボタンを検出する正規表現
    const rxAdsSkip = /skip[_\-]ad/i;
    // 広告ではないキーワードを除外する正規表現
    const rxAdsIgnore = /(^|[_\-])player($|[_\-])/i;
    
    function 広告であるか判定(elem){
        const tagName = elem.tagName.toLowerCase()
        // <html>, <body>, <head>, <script>は広告ではない
        if(["html", "body", "head", "script"].includes(tagName)){
            return false;
        }
        // スキップボタンは押す
        if(tagName === "button"){
            if(rxAdsSkip.test(elem.id)
            || rxAdsSkip.test(elem.className)){
                elem.click();
                console.log("AdBlock(Skip)", elem);
                return false;
            }
        }
        // id が除外条件に該当するタグは広告ではない
        if (elem.id && rxAdsIgnore.test(elem.id)){
            return false;
        }
        // className が除外条件に該当するタグは広告ではない
        if (elem.className && rxAdsIgnore.test(elem.className)){
            return false;
        }
        // allow=autoplay かつ loading≠lazy の<iframe>は広告である
        if(tagName === "iframe"){
            if(elem.allow && elem.allow.includes("autoplay")
            && !(elem.loading && elem.loading.includes("lazy"))){
                return true;
            }
        }
        // scrolling=noの かつ role≠presentation の<iframe>は広告である
        if(tagName === "iframe"){
            if(elem.scrolling && elem.scrolling.includes("no")
            && !(elem.role && elem.role.includes("presentation"))){
                return true;
            }
        }
        // position:fixed の<ins>は広告である
        if (tagName === "ins"){
            if(elem.style.position && elem.style.position.includes("fixed")){
                return true;
            }
        }
        // rel=sponsored のタグは広告である
        if(elem.rel && elem.rel.includes("sponsored")){
            return true;
        }
        // src属性 に ad が含まれるタグは広告である
        if(rxAdsSrc.test(elem.src)){
            return true;
        }
        // src属性 に ad が含まれる<script>の直後の<div>は広告である
        if(tagName === "div"){
            const prev = elem.previousElementSibling
            if(prev
            && prev.tagName.toLowerCase() == "script"
            && rxAdsSrc.test(prev.src)){
                return true;
            }
        }
        // 属性名または属性値に ad を含むタグは広告である
        for(const each of elem.attributes){
            if(rxAdsPattern.test(each.name)
            || rxAdsPattern.test(each.value)){
                return true;
            }
        }
        // id, className, title, name属性 に ad が含まれるタグは広告である
        if(rxAdsPattern.test(elem.id)
        || rxAdsPattern.test(elem.className)
        || rxAdsPattern.test(elem.title)
        || rxAdsPattern.test(elem.name)){
            return true;
        }
        return false;
    }
    
    let timeoutId;           // 実行予約した処理のID
    let lastExecTime = 0;    // 広告非表示の処理を実行した時刻
    
    // html要素が変更されると、0~0.5秒後に広告を非表示にする
    function 変更を検出時の処理(mutations){
        // 実行予約した処理が既にあれば取り消す
        if(timeoutId){
            clearTimeout(timeoutId);
        }
        // 遅延時間は、0~0.5秒
        const delayTime = Math.max(500 - (Date.now() - lastExecTime), 0);
        // 広告を非表示にする処理の実行予約をする
        timeoutId = setTimeout(() => {
            lastExecTime = Date.now();
            広告を非表示();
        }, delayTime);
    }
    
    // html内に含まれる全ての要素の変更を検出する
    new MutationObserver(変更を検出時の処理)
    .observe(document.documentElement, { childList: true, subtree: true });
    
    // ページの読込終了直後に、広告を非表示にする
    lastExecTime = Date.now();
    広告を非表示();
    


    Google Chrome拡張機能を読み込む

    1. [MyAdBlock] フォルダを作る。
    2. [manifest.json] と [script.js] を、[MyAdBlock] フォルダに入れる。
    3. Google Chrome の [設定]→[拡張機能] を選択する。
    4. [パッケージ化されていない拡張機能を読み込む] ボタンを押す。
    5. [MyAdBlock] フォルダを選択して、[OK] ボタンを押す。

    自分だけのオリジナル広告ブロックに

    今回のプログラムは、広告を8割程度カットできるが、完全ではない。
    広告以外を誤って消してしまうこともあるし、
    「広告を見ないと進めないページ」に遭遇すると、
    ページの操作ができなくなってしまうこともある。

    そんな時は、このコードを自分なりに改造して、
    君だけのオリジナル広告ブロックに育ててほしい。

    【VBA】 新規ブックに現在のブックのシートを全てコピーする - Copy

    やりたいこと

    • マクロ入りのブック(.xlsm)を開いている。
    • そのブックの中には、複数のシートがある。
    • VBAを使って、そのすべてのシートを新規ブックにコピーしたい。

    方針

    • シート(1) をコピーして、新規ブックを作成する。
    • シート(2) を、新規ブックにコピーする。
    • シート(3) 以降も同様に、新規ブックにコピーする。

    ソースコード

    '画面の更新を止めて、処理を速くする。
    Application.ScreenUpdating = False
    
    'コピーしたいブック
    Dim wb As Workbook
    Set wb = ThisWorkbook
    
    'コピー先のシート
    Dim sh As Worksheet
    
    'シート(1)をコピーする。
    'この場合、コピー先の指定がないので、新規ブックを作成する
    Call wb.Sheets(1).Copy
    Set sh = ActiveSheet
    
    'シート(2)以降をコピーする。
    'コピー先は、新規ブックの末尾。
    Dim i As Long
    For i = 2 To wb.Sheets.Count
    	Call wb.Sheets(i).Copy(After:= sh)
    	Set sh = ActiveSheet
    Next
    
    '画面の更新を再開する。
    Application.ScreenUpdating = True
    

    解説

    Worksheet.Copy メソッドで、シートのコピーを作成できます。
    Call wb.Sheets(1).Copy のように、引数無しで実行すると、新しいブックにシートを作成します。
    Call wb.Sheets(i).Copy(After:=sh) のように、引数「After」を指定すると、そのシートの後ろに作成します。
    作成したシートはアクティブになるので、ActiveSheet で取得することができます。

    コピー元とコピー先でブックのフォーカスの移動が発生するので、コピーするシートの数が増えると、チカチカします。

    【VBA】外部プログラムの実行 標準入出力あり(WScript.Shell / Exec / StdIn / StdOut)

    やりたいこと

    VBAから外部プログラムを呼び出して、その出力をVBAで利用したい。
    ただし、その外部プログラムは対話型 なので、標準入力からいろいろ入力したい。

    ※対話型:プログラムがユーザーに質問をする。ユーザーが答えると処理を継続する。

    例えばこういう状況

    実行しているコンピュータのメモリ容量を知りたい としよう。

    コマンドプロンプト(cmd.exe)を起動して「SYSTEMINFO」と入力すればメモリ容量がわかる。


    システム ディレクトリ: C:\WINDOWS\system32
    起動デバイス: \Device\HarddiskVolume1
    システム ロケール: ja;日本語
    入力ロケール: ja;日本語
    タイム ゾーン: (UTC+09:00) 大阪、札幌、東京
    物理メモリの合計: 15,789 MB
    利用できる物理メモリ: 8,896 MB
    仮想メモリ: 最大サイズ: 18,221 MB
    仮想メモリ: 利用可能: 9,311 MB
    仮想メモリ: 使用中: 8,910 MB

    つまり、以下の処理を行えばよい。

    • cmd.exe を起動する
    • 「SYSTEMINFO」と標準入力から打ち込む
    • システム情報が表示されるまでちょっと待つ
    • 「EXIT」を入力して終了する
    • 得られた標準出力から、メモリ容量のところを抜き出す
    • ダイアログボックスに表示する

    これをVBAで実現してみよう。

    コード

    Option Explicit
    
    '外部プログラムを実行する
    Dim oExec
    With CreateObject("WScript.Shell")
    	Set oExec = .Exec("cmd.exe")
    End With
    
    '標準入力に以下の入力を積んでおく
    oExec.StdIn.WriteLine("SYSTEMINFO")
    oExec.StdIn.WriteLine("EXIT") 'cmd.exe はこれがないと終了しない
    
    '標準出力をすべて取得し、改行で区切る
    Dim sOut
    Do Until oExec.StdOut.AtEndOfStream
    	sOut = sOut & oExec.StdOut.ReadLine() & vbCrLf
    Loop
    
    '物理メモリの合計が書かれている行を探す
    Dim sEach
    Dim sMemory
    
    For Each sEach In Split(sOut, vbCrLf)
    	If InStr(sEach, "物理メモリの合計:") = 1 Then
    		sMemory = sEach
    	End If
    Next
    
    'ダイアログボックスに物理メモリの合計を表示
    MsgBox sMemory
    
    'ついでに標準出力をファイルに書き込む
    'テキストファイルを開く … モード=書き込み、ファイル形式=規定値
    With CreateObject("Scripting.FileSystemObject").OpenTextFile("StdOut.txt", 2, -2)
    	'書いて閉じる
    	.Write(sOut)
    	.Close
    End With
    

    解説

    Dim oExec
    With CreateObject("WScript.Shell")
    	Set oExec = .Exec("cmd.exe")
    End With
    

    外部プログラムとして、コマンドプロンプト(cmd.exe)を実行する。
    .Exec では非同期実行をするので、cmd.exe の終了を待たずに次の行を実行する。


    oExec.StdIn.WriteLine("SYSTEMINFO")
    oExec.StdIn.WriteLine("EXIT")
    

    oExec.StdIn.WriteLine を実行すると、標準入力に文字列を流し込める。
    コマンドプロンプトで、「SYSTEMINFO(Enter)」と「EXIT(Enter)」をキー入力したのと同じ効果が得られる。


    Dim sOut
    Do Until oExec.StdOut.AtEndOfStream
    	sOut = sOut & oExec.StdOut.ReadLine & vbCrLf
    Loop
    

    プログラムが終了すると、oExec.StdOut.AtEndOfStream は True になる。
    標準出力は、oExec.StdOut.ReadLine で1行ずつ取得することができる。
    もし、コンソールが入力待ちになっている時に AtEndOfStream や ReadLine を実行すると、そこから先は処理がブロックされて停止してしまう。(今回の罠)
    したがって、入力待ちにならないように、外部プログラム(cmd.exe)が終了するまで oExec.StdIn.WriteLine で標準入力に流し込んでやる必要がある。

    【アルゴリズム】シェルソートの速度を考察した

    はじめに

    前提:挿入ソートとは何か

    • 「挿入ソート」とは、以下のようなソートアルゴリズムである。
      • 配列N番目(N≧2)の数字に注目する。
      • その数字が左隣の数字より小さい間は、左隣の数字と交換し続ける。
      • 配列N+1番目の数字に注目する。
      • その数字が左隣の数字より小さい間は、左隣の数字と交換し続ける。
    • 図を見た方が分かりやすい。赤字のところが交換した箇所である。

    63045812711109
    36045812711109
    30645812711109
    03645812711109
    03465812711109
    03456812711109
    03456182711109
    03451682711109
    03415682711109
    03145682711109
    01345682711109
    01345628711109
    01345268711109
    01342568711109
    01324568711109
    01234568711109
    01234567811109
    01234567810119
    01234567810911
    01234567891011

    挿入ソートの特徴

    • 交換回数が少ない。
    • 1000くらいの要素なら、実用的な速さで動く。
    • (要素数)2 に比例した回数の比較処理を行うため、100万要素の並べ替えは現実的な時間ではできない。(2022年の時点)

    挿入ソートの比較回数(ランダム配列の場合)

    • 素数をNとして、比較回数は 0.24 N2 に近似できる。

    要素比較回数交換回数
    103226
    31245215
    10029642870
    3162428523981
    1000234963233974
    316224456242442476
    100002388683323876841

    • もし、100万要素の並べ替えを行うなら、じつに 2400億回 もの比較が必要になる。
    • これを執筆しているPCでは、約7200秒(=120分)かかる見積もりだ。

    改良版挿入ソート「シェルソート

    • ドナルド・シェルが、1959年に発表したソートアルゴリズム
    • 「挿入ソートは隣同士しか交換しないから遅い」という欠点を克服すべく、離れた位置の要素を交換するように改善した。
    • まずは、4n番目(0, 4, 8, 12, …)の要素を挿入ソートする。つまり4つ離れた位置の挿入ソート。
    • つぎに、4n+1番目(1, 5, 9, 13, …)の要素を挿入ソートする。
    • つぎに、4n+2番目(2, 6, 10, 14, …)の要素を挿入ソートする。
    • つぎに、4n+3番目(3, 7, 11, 15, …)の要素を挿入ソートする。
    • 最後に、n番目(0, 1, 2, 3, …)の要素について普通の挿入ソートをする。
    • 4n番目などの挿入ソートを最初に実行している分、遅くなりそうな雰囲気があるが、実際にはそうではない。
    • 図を見た方が分かりやすい。赤字のところが交換した箇所である。

    11109876543210
    71098116543210
    71098365411210
    31098765411210
    36987105411210
    36987254111010
    32987654111010
    32587694111010
    32587614111090
    32187654111090
    32147658111090
    32147650111098
    32107654111098
    23107654111098
    21307654111098
    12307654111098
    12037654111098
    10237654111098
    01237654111098
    01236754111098
    01236574111098
    01235674111098
    01235647111098
    01235467111098
    01234567111098
    01234567101198
    01234567109118
    01234567910118
    01234567910811
    01234567981011
    01234567891011

    • 交換する距離は、配列の大きさによって変更する。

    なぜ速いのか

    • 4つ離れた位置ごとのソートが済んでいる場合、普通のソートの実行時間は大幅に短縮されるから。(上の表を参照)
    • 素数がさらに大きいときは、挿入ソートをする間隔を以下のように広げていく。
      • 1 → 4 → 13 → 40 → 121 → (3倍+1)

    シェルソートの比較回数(ランダム配列の場合)

    • 素数をNとして、比較回数は 3.5 N1.2・(logeN)0.1 に近似できる。
    • Wikipedia には、N1.25 のオーダーと書いてあるが、こちらの方が実測値に近い。

    素数比較回数交換回数
    101910
    3113583
    1001062881
    31643333437
    10001743613634
    31627096655731
    10000274549216244
    316221123988907860
    10000039637733101598
    3162271619158713143666
    10000006408612053433454

    挿入ソート VS シェルソート

    素数
    N
    挿入ソート
    0.24 N2
    シェルソート
    3.5 N1.2・(logeN)0.1
    速度比
    1000 240000 15552 15倍
    10000 24000000 253673 95倍
    100000 2400000000 4111166 584倍
    1000000 240000000000 66356454 3617倍

    JavaScript での実装

    function shellSort(ary){
    	// 最初の交換間隔を決める(1→4→13→40→121→...)
    	let d = 1;             // d : 交換間隔
    	let z = ary.length;    // z : 配列長
    	let dmax = z / 30 | 0; // 最初のdは配列長の1/9程度が良い
    	while(d <= dmax){
    		d = d + d + d + 1;
    	}
    	// 交換間隔が0になったら終了
    	while(d > 0){
    		// i : 交換間隔の分だけ繰り返す(間隔が40なら40回)
    		for(let i = 0; i < d; i++){
    			// j : i~末尾まで繰り返す
    			for(let j = i; j + d < z; j += d){
    				// k : j~先頭まで繰り返す
    				for(let k = j; k >= 0; k -= d){
    					// 左が大きい場合は交換する
    					// 右が大きい場合はjを進める
    					if(ary[k] > ary[k + d]){
    						[ary[k], ary[k + d]] =
    							[ary[k + d], ary[k]];
    					}else{
    						break;
    					}
    				}
    			}
    		}
    //		交換間隔を小さくする(121→40→13→4→1→0)
    		d = d / 3 | 0;
    	}
    }
    

    その他、シェルソートの特性

    • ソート済配列の先頭や末尾に追加した場合、比較回数は O(N log1.2N) のオーダー。
      • 先頭や末尾に k個 追加した場合の比較回数(実測値)の近似値は、
        1.2 N・loge(k・N)1.2
      • 挿入ソートの末尾追加のオーダーは O(N) であるから、その場合では挿入ソートの方が有利になる。

    まとめ

    • N個のランダム配列では、シェルソートの比較回数は 3.5 N1.2・(logeN)0.1 だと判明した。
      参考までに、JavaScriptで整数の配列をソートする場合、1億回の比較に3秒かかる。
    • シェルソートは22行で実装できるので、挿入ソートをシェルソートに置き換えるのはいいぞ。

    【VBA】 Excelシートの書式・条件付き書式を変更できないようにする

    困ったこと

    • Excelの条件付き書式は、セルをコピーするとルールが増殖する。
    • カット&ペーストすると、書式範囲が飛び飛びの穴開きになってしまう。
    • いつの間にか増殖しすぎて、ルール数が1000を超えていることもある。
    • そうなってしまうとメンテナンスは不可能。

    今回やりたいこと

    • Excelでセルをコピーしても条件付き書式が増殖しないように固定化したい。

    方針

    • ブックを開いたら、対象のシートをまるごとコピーして退避する。
    • 保存するタイミングで、退避先シートから書式を復帰する。

    コード

    'ThisWorkbook に記述する
    Option Explicit
    
    '条件付き書式を固定するシートの番号(変更可能)
    Const Nシート番号 = 1
    
    '書式退避先のシート名(変更可能)
    Const S退避 = "書式退避"
    
    '保存時に書式が復元される
    Private Sub Workbook_BeforeSave(ByVal SaveAsUI As Boolean, Cancel As Boolean)
    	'画面描画を省略して高速化
    	Application.ScreenUpdating = False
    	'退避シートが存在しなければジャンプ
    	On Error GoTo NOT_EXIST
    	Sheets(S退避).Visible = False
    	'退避シートが存在する場合の処理======
    	'退避シートの全セルの書式をコピペ
    	Sheets(S退避).Cells.Copy
    	Sheets(Nシート番号).Cells.PasteSpecial xlPasteFormats
    	Application.CutCopyMode = False
    	'画面描画を再開する
    	Application.ScreenUpdating = True
    	Exit Sub
    NOT_EXIST:
    	'退避シートが存在しない場合の処理======
    	'現在位置を覚えておく
    	Dim rg As Range
    	Set rg = Selection
    	'退避シートをコピーにより作成し、非表示にする
    	Sheets(Nシート番号).Copy After:=Sheets(Sheets.Count)
    	ActiveSheet.Name = S退避
    	ActiveSheet.Visible = False
    	'現在位置に戻る
    	Application.Goto rg
    	'画面描画を再開する
    	Application.ScreenUpdating = True
    End Sub
    

    使い方

    • 上のコードを、ThisWorkbookに記述する。
    • 1回目の保存時には、退避するシートに書式がコピーされる。
    • 2回目の保存時には、退避したシートから書式を復帰してくれる。
    • 書式を変更したい場合は、退避したシートを削除してから作業すればよい。

    雑感

    • 複数人で共用するExcelシートは書式がめちゃくちゃになりやすいので、保存するたびに元の状態に復帰してくれるのは助かる。
    • 本来は「条件付き書式」だけを変更できなくするつもりだったのに、書式全体を固定化する方法になってしまった。
    • まあ、条件付き書式があることに気づかず、手動で色を塗ろうとするメンバーもいると思うので、それを防ぐのにもいいかもしれない。
    • 書式を固定化したいシートが2つ以上ある場合については、今度また考えることにする。

    Javascript 高階関数を入門してみた map / filter / reduce / some / flatMap

    高階関数とは?

    • 引数に関数を受け取る関数のこと(正確な説明だが、初めて聞くとわけがわからない)
    • 高階関数を使うと、配列を処理するときにfor文を使わなくて済む。
    • let i のようなループ用変数を使わず、const変数だけでコードが書けるのが嬉しい!!

    2022年現在、高階関数が分からないと時代に取り残されそうな雰囲気なので、さっそく学習を始めることにした。

    map:配列の要素を変換する(マッピングする)

    【例題】

    日付を表す8桁の数値(例:20210819)の配列がある。
    これを、日を表す数値(例:19)に変換して配列を出力せよ。

    mapを使わない書き方
    const input = [20210819, 20210820, 20211230, 20211231, 20220101];
    let output = [];
    for(let i = 0, z = input.length; i < z; i++){
    	const n日 = input[i] % 100;
    	output.push(n日);
    }
    console.log(output); // [19, 20, 30, 31, 1]
    
    mapを使う書き方
    const input = [20210819, 20210820, 20211230, 20211231, 20220101];
    const output = input.map(elem => {
    	const n日 = elem % 100;
    	return n日;
    });
    console.log(output); // [19, 20, 30, 31, 1]
    

    filter:配列の要素を抜き出す(フィルタリングする)

    【例題】

    日付を表す8桁の数値(例:20210819)の配列がある。
    ここから、12月の日付だけを抜き出して配列を出力せよ。

    filter を使わない書き方
    const input = [20210819, 20210820, 20211230, 20211231, 20220101];
    let output = [];
    for(let i = 0, z = input.length; i < z; i++){
    	const n年月 = input[i] / 100 | 0; // YYYYMM
    	const n月 = n年月 % 100;
    	if(n月 === 12){
    		output.push(input[i]);
    	}
    }
    console.log(output); // [20211230, 20211231]
    
    filter を使う書き方
    const input = [20210819, 20210820, 20211230, 20211231, 20220101];
    const output = input.filter(elem => {
    	const n年月 = elem / 100 | 0; // YYYYMM
    	const n月 = n年月 % 100;
    	return n月 === 12; // 12月ならtrue
    });
    console.log(output); // [20211230, 20211231]
    

    reduce:配列を集計して単一の値にする

    【例題】

    日付を表す8桁の数値(例:20210819)の配列がある。
    日を表す数値(例:19)のうち、最大の数値を出力せよ。

    reduce を使わない書き方
    const input = [20210819, 20210820, 20211230, 20211231, 20220101];
    let output = -1;
    for(let i = 0, z = input.length; i < z; i++){
    	const n日 = input[i] % 100;
    	output = Math.max(n日, output);
    }
    console.log(output); // 31
    
    reduce を使う書き方
    const input = [20210819, 20210820, 20211230, 20211231, 20220101];
    const output = input.reduce((acc, elem, i, ary) => { // 累積値・各要素・連番・配列自体
    	const n日 = elem % 100;
    	return Math.max(n日, acc);
    }, -1); // 累積値の初期値
    console.log(output); // 31
    
    メモ

    reduce内の関数は、5回実行される。return結果が次のacc(累積値)となる。
    acc(累積値)・elem(各要素)・i(連番)・return結果は、以下の表のようになる。

    acc(累積値) elem(各要素) i(連番) return結果
    -1 20210819 0 19
    19 20210820 1 20
    20 20211230 2 30
    30 20211231 3 31
    31 20220101 4 31

    some:ひとつでも該当すれば true を返す

    【例題】

    日付を表す8桁の数値(例:20210819)の配列がある。
    配列の要素が昇順にソートされている場合は true、
    昇順にソートされていない場合は false を出力せよ。

    some を使わない書き方
    const input = [20210819, 20210820, 20211230, 20211231, 20220101];
    let output = true;
    for(let i = 0, z = input.length; i < z; i++){
    	if(i === 0){
    		continue;
    	}
    	if(input[i-1] > input[i]){
    		output = false; // 昇順ではない場合、false で処理終了
    		break;
    	}
    }
    console.log(output); // true
    
    some を使う書き方
    const input = [20210819, 20210820, 20211230, 20211231, 20220101];
    const output = ! input.some((elem, i, ary) => { // 各要素・連番・配列自体
    	if(i === 0){
    		return false;
    	}
    	return ary[i-1] > elem; // 昇順ではない場合、true で処理終了
    });
    console.log(output); // true
    

    flatMap:配列の次元を下げて連結する

    【例題】

    日付を表す8桁の数値(例:20210819)の配列がある。
    日を表す数値(例:19)の配列を出力せよ。
    ただし、10日、20日、30日の場合は、2つ重複させて出力せよ。

    flatMap を使わない書き方
    const input = [20210819, 20210820, 20211230, 20211231, 20220101];
    let output = [];
    for(let i = 0, z = input.length; i < z; i++){
    	const n日 = input[i] % 100;
    	if(n日 % 10 === 0){
    		output.push(n日);
    		output.push(n日);
    	}else{
    		output.push(n日);
    	}
    }
    console.log(output); // [19, 20, 20, 30, 30, 31, 1]
    
    flatMap を使う書き方
    const input = [20210819, 20210820, 20211230, 20211231, 20220101];
    const output = input.flatMap(elem => {
    	const n日 = elem % 100;
    	if(n日 % 10 === 0){
    		return [n日, n日];
    	}else{
    		return [n日];
    	}
    });
    console.log(output); // [19, 20, 20, 30, 30, 31, 1]
    

    HTMLタグをHTMLで表示するための変換ツール(文字参照)

    <html>のようなタグを、&lt;html>に変換するツールです。どうぞお使いください。

    ▼入力欄
      
    ▼出力欄(タブ文字はスペース4つに置き換えます)
    ▼HTMLでの表示のされ方

    【はてなブログ】はてな記法で簡単に表を作成するためのツール

    ツールの説明

    はてなブログでこのような表を作成するには、

    商品名 値段 在庫
    アイス 398円 24個
    カップ 258円 40個

    はてな記法」で以下のように記述します。

    |*商品名|*値段|*在庫|
    |アイス|398円|24個|
    |カップ麺|258円|40個|

    はてな記法の表を簡単に作れるツールを作ったので、どうぞお使いください。

    表を入力してください


     
    はてな記法での書き方

    追記:大きなテーブルのレイアウトが崩れる場合

    大きなテーブルをはてなブログに収めるためには、スクロールできるようにすると良いでしょう。

    × 100 500 1000 5000 10000 50000 10000
    100 10000 50000 100000 500000 1000000 5000000 10000000
    500 50000 250000 500000 2500000 5000000 25000000 50000000
    1000 100000 500000 1000000 5000000 10000000 50000000 100000000

    <div style="height:150px; width:500px; overflow:scroll">
    |×|100|500|1000|5000|10000|50000|10000|
    |100|10000|50000|100000|500000|1000000|5000000|10000000|
    |500|50000|250000|500000|2500000|5000000|25000000|50000000|
    |1000|100000|500000|1000000|5000000|10000000|50000000|100000000|
    <div>

    VBA マウスポインタの形状を取得する GetCursorInfo / LoadCursor

    やりたいこと

    VBAで、マウスポインタが矢印カーソルなのか、待機カーソルなのか知りたい。
    待機カーソルになったらVBAの処理を中断、というプログラムを作ろう。

    方針

    VBAには、Application.Cursor でカーソルの状態を取得することができる。
    しかし、これは残念なことに、Excel の外にマウスカーソルが出ると全く機能しない。
    Excel の外のマウスポインタの形状を知るには、Win32 API を使う必要がある。

    • マウスカーソルの形状や位置を得るための APIGetCursorInfo
    • 待機カーソルの番号を得るための APILoadCursor


    コード(64bit版)

    '取得するカーソル情報を保持する構造体
    Type CURSORINFO
    	nSize As Long '構造体の大きさ(=24)
    	nFlag As Long '表示・非表示フラグ
    	hCursor As LongPtr 'カーソル画像を表す数値
    	x As Long 'X座標
    	y As Long 'Y座標
    End Type
    
    Const IDC_WAIT = 32514 '待機カーソルのID
    
    'カーソル情報を取得する Win32API 関数
    Declare PtrSafe Function GetCursorInfo Lib "user32" (p As CURSORINFO) As Long
    
    'カーソル画像を表す数値を取得する Win32API 関数
    Declare PtrSafe Function LoadCursor Lib "user32" Alias "LoadCursorA" ( _
    	ByVal hInst As LongPtr, _
    	ByVal idc As Long _
    ) As LongPtr
    
    '処理を一時停止する Win32API 関数
    Declare PtrSafe Sub Sleep Lib "kernel32" (ByVal nミリ秒 As Long)
    
    Public Sub 今回の処理()
    	Dim h待機カーソル As LongPtr
    	h待機カーソル = LoadCursor(0, IDC_WAIT) '待機カーソル画像を表す数値を取得
    	
    	Dim i As Long
    	For i = 1 To 100000 'しばらくループする
    		Dim カーソル As CURSORINFO
    		カーソル.nSize = LenB(カーソル) '構造体の大きさをセット
    		GetCursorInfo カーソル 'カーソル情報を取得する
    		
    		'カーソルの形状が待機になったら、メッセージを表示して終了
    		If カーソル.hCursor = h待機カーソル Then
    			MsgBox "待機カーソルになりました"
    			Exit Sub
    		End If
    		
    		DoEvents 'OSに処理をゆずる
    		Sleep(1) '0.001秒待機
    	Next
    End Sub
    

    補足説明

    待機カーソルのIDは 32514 だが、他のカーソルのIDも記載しておく。
    参照元
    LoadCursorA function (winuser.h) - Win32 apps | Microsoft Docs

    意味
    IDC_APPSTARTING 32650 標準的な矢印と小さな砂時計
    IDC_ARROW 32512 標準的な矢印
    IDC_CROSS 32515 十字
    IDC_HAND 32649
    IDC_HELP 32651 矢印とはてなマーク
    IDC_IBEAM 32513 Iの字
    IDC_NO 32648 🚫マーク
    IDC_SIZEALL 32646 十字矢印
    IDC_SIZENESW 32643 /の向きの両矢印
    IDC_SIZENS 32645 │の向きの両矢印
    IDC_SIZENWSE 32642 \の向きの両矢印
    IDC_SIZEWE 32644 ─の向きの両矢印
    IDC_UPARROW 32516
    IDC_WAIT 32514 砂時計

    具体的にどのような形状なのかは、以下のリンクが参考になる。
    Windowsアプリケーション上のマウス・カーソルを変更するには?[C#、VB] - @IT

    VBA Wordに改ページを挿入 Chr(12)

    今回やりたいこと

    画像の入った Word ファイルがある。
    常に画像がページの先頭にくるようにしたい。

    そうするには、画像の前に改ページを入れたらいい。

    最もシンプルな答え

    改ページは Chr(12) の文字で表せる。
    画像のRangeを取得して、改ページを InsertBefore で直前に挿入する。

    コード

    Public Sub 今回の処理()
    	Dim o図形 As InlineShape
    	For Each o図形 In ActiveDocument.InlineShapes '全ての画像について
    		o図形.Range.InsertBefore Chr(12) '前に改ページを挿入
    	Next
    End Sub