0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

LITSを解く — パズルの名前が定理で、一意性はテトロミノの「スライド」を潰して作る

0
Posted at

LITS を、3 つのルールセット内蔵でブラウザに実装した。盤は領域に切り分けられている。各領域でちょうど 4 マスを塗り、その 4 マスはテトロミノを成す。塗りマス全体はひとつながり2×2 が全部塗りになってはいけない。そして違う領域のテトロミノどうしが辺で接するとき、合同な形(回転・鏡像込みで同じ形)であってはならない。ここまでがルールの全部だ。パズルの名前は使ってよいテトロミノの一覧——L・I・T・S——から来ていて、5 つ目の O だけが禁止されている。だが気づいてほしい。O とは 2×2 の正方形そのものであり、2×2 は別のルールが既に禁止している。名前に書いてある制約は、実は定理なのだ。ソルバー内蔵パズル第 27 弾。

🌐 ライブデモ: https://sen.ltd/portfolio/lits/
📦 GitHub: https://github.com/sen-ltd/lits

スクリーンショット

ルール

  1. 盤は領域(4 マス以上のポリオミノ)に分割されている
  2. 各領域でちょうど 4 マスを塗り、それはテトロミノ(連結な 4 マス)を成す
  3. 塗りマス全体はひとつながり(上下左右で連結)
  4. 2×2 の正方形が 4 マスとも塗りになってはいけない
  5. 違う領域のテトロミノが接するなら、合同禁止(回転・鏡像は同じ形とみなす)

使えるテトロミノは L・I・T・S の 4 種。O(田の字)は禁止——というのが公式のルールだが、ここが本作の入口になる。

名前が定理になっている

テトロミノは 5 種類ある: L, I, T, S, O。LITS の名前はこのうち 4 つを許すという宣言で、排除されるのは O だけだ。ところで O とはそれ自体が 2×2 の正方形である唯一のテトロミノで、ルール 4 は 2×2 の全塗りを既に禁止している。

つまり「L・I・T・S のみ」という条項は冗長だ。ルール 2 を「連結な 4 マス」に弱め、形の名前を一切書かなくても、パズルは 1 ミリも変わらない。

これは口で言うだけでなく、テストで全数検査してある。5×5 盤の連結な 4 マス部分集合は 228 個。そのうち 2×2 正方形を含むものは 16 個で、shape 分類が O を返す集合と完全に一致する。

it('THE THEOREM: for connected 4-sets, being O is exactly containing a 2×2 square', () => {
  // 5×5 の全 4 マス部分集合を列挙して
  // 「2×2 を含む」⟺「O と分類される」を 1 件ずつ確認する
  expect(hasSquare).toBe(shape === 'O');
});

だからエンジンの中に「許される形のリスト」は事実上存在しない。shapeOf は連結判定とバウンディングボックスの場合分けだけで L/I/T/S/O を返し、O は 2×2 ルールの影として勝手に死ぬ。

分類はきれいに閉じる。連結な 4 マスのバウンディングボックスは 1×4・2×2・2×3(と転置)しかない——4 マスのパスは (行数−1)+(列数−1) ≤ 3 しか広がれないからだ。1×4 なら I、2×2 なら O、2×3 は行ごとのマス数で L/T/S に割れる。

変数はまた領域——ドメインは「テトロミノの置き方」

前作のりのりと同じく、盤には数字が 1 つも書かれていない。領域分割そのものがパズルであり、自然な変数はマスではなく領域だ。

のりのりでは領域の決定は「どの 2 マスを塗るか」で、ドメインは C(k,2) 個のペアだった。LITS では決定は「どのテトロミノをどこに置くか」で、ドメインは領域内の連結な非 O の 4 マス部分集合——数個から数十個の placement のリストになる。

この持ち上げの気持ちよさは、LITS の看板ルールがそのまま溶けることだ。「合同な形が境界越しに睨み合ってはいけない」という、形の同値類の話にしか見えない条件が、隣接領域間の普通の二項制約になる:

export function binaryOk(state, puzzle, i, pi, j, pj): boolean {
  // 接していて形が同じなら非互換
  if (pi.shape === pj.shape && placementsTouch(pi, pj, n)) return false;
  // 2 領域にまたがる 2×2 が閉じるなら非互換
  for (const s of sharedSquares(i, j)) {
    if (squareCells(s, n).every((k) => read(k) === SHADED)) return false;
  }
  return true;
}

あとは教科書どおりの AC-3 で回すだけ。生き残った全 placement が塗るマスは塗り確定、どの placement も塗らないマスは白確定。

連結性だけは二項に分解できない

「塗りマス全体がひとつながり」は盤全体の性質で、隣接領域のペアにどう頑張っても畳み込めない。そこで連結性には専用の大域伝播器を用意した:

  • マスが塗られうるとは、その領域の生存 placement のどれかに含まれること
  • placement は連結なので、「塗られうるマスのグラフ」のちょうど 1 つの連結成分の中に住む
  • 解の塗りマスは連結で、全領域を通る。だから解が住める成分は「全領域に placement を残している成分」だけ
  • それ以外の成分に住む placement は孤立していて、削れる
// 全領域に placement を残す成分だけが「生きられる」成分
const viable = components.filter((c) =>
  regions.every((i) => live[i].some((p) => componentOf(p) === c)),
);
// 孤立 placement を落として AC-3 とまた不動点まで往復する

AC-3 とこの成分フィルタを不動点まで往復させたものが tetro ルールセットだ。

3 つのルールセット

local — 領域を 4 まで数える、3 隅塗りの 2×2 を白で閉じる、繰り返す。マスから出ない解き手の目に映る LITS。形も連結性も見えない。反証として出荷する。

tetro — local + placement ドメインの AC-3 + 連結性伝播器。

probe — その上の singleton consistency。1 マス仮置きして伝播が死んだら逆を確定。

探索なしで完走したら、それは一意性の証明書でもある。ここの伝播器はすべて健全(確定するマスは全解で同じ値)なので、伝播だけで盤面が埋まり独立バリデータを通れば、解は 1 つしかありえない。

完走率を測った。ここで前々作までの教訓がひとつ効いている: 可解性フィルタをかけた盤で可解率を測ると 100% の列は circular になる。だから計測は「一意でありさえすればよい」モード(mode: 'any')で生成した盤に対して行う:

local tetro probe 平均領域サイズ
6×6 0% 85% 100% 6.0
8×8 0% 60% 100% 6.4

局所ルールがほぼ何もできないのは予想どおり——数えるべき数字が無く、square ルールだけでは盤面は割れない。900 局面のプロパティテストでも同じ景色が出る: リフティングは局所ルールより 896 / 900 局面で多くのマスを決め、平均 +13.8 マス、逆に局所が勝った局面は 0(構成上の包含)。

生成——密度が運命を分ける

LITS の生成器が動かせるものは領域分割しかない。そしてランダムな分割はまずパズルにならない。ここまではのりのりと同じだが、壊れ方が真逆だった。

のりのりのランダム分割は大半が「解なし」に落ちる。LITS は逆で、領域サイズ 6〜10 のランダム分割は:

解なし ちょうど 1 2 つ以上
6×6 7% 0% 93%
8×8 12% 0% 88%

一意はゼロ。ほぼ全部が「解が多すぎる」側に死んでいる。

原因は密度の算術だ。各領域はちょうど 4 マス塗るので、平均領域サイズ = 4 ÷ 塗り率が恒等式として成り立つ。塗り率 45% なら領域は平均 9 マス——C(9,4) 級の placement ドメインを抱えた領域が並び、盤は際限なく緩む。締まった盤が欲しければ塗り率を上げて領域を小さくするしかない。

ところがテトロミノを貪欲に付け足していく生成は塗り率 55% あたりで詰む。2×2 禁止と合同接触禁止が高密度で一斉に効いてくるからだ。そこで詰め込みをランダム化バックトラッキングにした——候補を混ぜて 12 個ずつ試し、行き止まりなら 1 手戻る。これで 65%(平均領域 6.5 マス)まで届く。

残った白マスの配り方にも 1 手ある。各白マスを「最も placement が増えない隣接領域」に流し込む。たったこの貪欲で、素の分割の解の個数が桁で変わる——最小領域優先 flood はカウンタの打ち切り上限 3000 に張り付くのに対し、ドメイン最小 flood は中央値 36(最小 6・最大 747)に落ちる。

最後に立ちはだかるのは「スライド」

ここからが本作の主役だ。ほぼ一意まで締まった盤の残りのライバル解を解剖すると、毎回同じ顔をしている:

sol0: 差分 [5,39]     ← 領域0 と 領域2
sol1: 差分 [5]        ← 領域0 だけ
sol2: 差分 [5,29,39]  ← 領域0,3,2
sol3: 差分 [5,29]     ...
(8 つの解 = 3 つの独立スライドの 2³ 格子)

ライバルは**単一領域内でテトロミノが 1 マスずれる/ピボットする「スライド」**の独立な組み合わせだった。8 つの解は 2³、つまり 3 か所のスライドの直積として現れる。

そこで領域の local slack(局所スラック) を定義する: 他の領域の塗りを解のまま固定したとき、その領域が単独で差し替えられるテトロミノの数。スラックの合計が 0 なら単一領域スライドは全滅で、経験上それはほぼ一意性そのものだ。しかもスラックは領域ごとに、解を数えずに計算できる——placement を並べて isValid に当てるだけ。

生成の仕上げはこのスラックの山下りになる。動かすのは解が白のままにしているマスだけ。白マスを隣の領域へ移す移動は意図した解を絶対に壊さない——塗りマスは 1 つも領域を変えないから、各領域は自分のテトロミノをそのまま保ち、大域ルール(塗りと「塗りマスの所属領域」しか読まない)には移動が見えない

  1. placement ドメインの合計サイズを下げる差分貪欲(状態レス・高速)
  2. スラック合計を 0 へ降下。改善する 1 手が無ければスラックを持つ領域から白マスを 1 枚剥がす(peel)——領域が縮めば placement が減り、スライドの逃げ場が消える
  3. スラック 0 は複数領域が絡む合わせ技のライバルまでは殺せないので、最後に本物の探索で裏取り。残っていれば盤ごと棄却

反例を 1 個ずつ狩る方式(のりのりの生成)はここでは機能しなかった。ライバルが数千いる状態から 1 killずつでは収束せず、スライドの当該マスが領域の内側に埋まって合法手が無くなる。解の個数ではなく、スラックという構造を直接下ろすのが効いた。

3 つのルール、荷重は均等ではない

収録の 6×6 盤でバリデータのルールを 1 本ずつ切って数え直す:

切ったルール 一意のまま残る盤 中央値の解数
合同接触禁止 1/16 4
連結性 12/16 1
2×2 禁止(= O 解禁) 0/16 159

合同接触禁止と 2×2 禁止は、切った瞬間に一意性がほぼ全滅する(16 枚中 1 枚 / 0 枚しか生き残らない)。連結性だけは毛色が違って 12/16 は切っても一意のまま——効くのは 4 枚だけだ。ただしその 4 枚では、一意性を支えているのは連結性しかない。飾りのルールは無いが、荷重は均等でもない。

おまけに収録盤の文字の人口統計も取った: 6×6 は 96 領域で L=29% I=28% T=22% S=21%、16 枚中 2 枚は 4 文字が揃わない。8×8 は 160 領域で L=32% I=27% T=22% S=19%、10×10 は 240 領域で L=31% I=22% T=22% S=25%——こちらは文字の欠けた盤ゼロ。接している隣接領域ペアの割合は盤が大きいほど下がる(72% → 61% → 57%)。

健全性は 4 カウンタの全一致で固定

伝播器とコードを共有しないブルートフォース 2 本——マスを行順に歩く版と、領域ごとに placement を選ぶ版——に、ルールセット付き探索カウンタと、形をゼロから分類し直す独立バリデータ。全員が全盤面で解の個数に合意しなければテストが落ちる。健全でない伝播器はここで必ず捕まる(このシリーズの過去の教訓そのもの)。全 65 テスト。

まとめ

  • LITS の名前(L・I・T・S のみ)はルールではなく定理——2×2 禁止が O を含意する。5×5 の連結 4 マス集合 228 個で全数検査済み
  • 変数は領域、ドメインはテトロミノ placement。合同接触禁止は AC-3 に溶け、連結性だけは成分の生存可能性で削る大域伝播器を建てた
  • ランダム分割は一意になった試しがない(のりのりと真逆の壊れ方)。平均領域サイズ = 4 ÷ 塗り率の恒等式が全てを支配し、バックトラッキング詰め込みで塗り率 65% まで上げる
  • 最後のライバルは単一領域のテトロミノ・スライド(2³ 格子)。解を数えずに計算できる local slack を 0 へ下ろし、詰まったら領域から白マスを剥がす
  • 健全性は独立カウンタ 4 本の解数全一致で担保。伝播だけで完走 = 一意性の証明書

🌐 ライブデモ: https://sen.ltd/portfolio/lits/
📦 GitHub: https://github.com/sen-ltd/lits

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?