カナルビュー(Canal View)を、4 段のソルバー内蔵でブラウザに実装した。ルールは「数字は四方向に見える黒マスの合計」。効いているのは見えるという一語だ。連なりは最初の白マスで止まるので、その白の向こうにどれだけ運河が続いていても数には入らない。つまり数字は自分の行と列についての主張ではなく、その手前の端についての主張でしかない。出荷 72 盤で測ると、8×8 の数字は自分の四方向に実在する黒 5.28 マスのうち 3.84 しか見ておらず(72.8%)、12×12 では 64.7% まで落ちる。12×12 では白 1 マスが、それを正面から見ている数字に対して黒の連なり 10 マスを隠していた。視線を二度数えると
4 × 運河 = 見えた + その方向に数字が無い + 白に遮られたがぴったり成立し、8×8 で 4,616 = 1,576 + 2,451 + 589。ソルバー内蔵パズル第 61 弾。
デモ: https://sen.ltd/portfolio/canal-view/
リポジトリ: https://github.com/sen-ltd/canal-view
ルール
いくつかのマスを黒く塗る(この記事とアプリでは「運河に沈める」と呼ぶ)。
- 黒マス全体が辺で連結した 1 つのかたまりになる
- 2×2 が全部黒になってはいけない
- 数字のマスは常に白。その数字は、四方向に見える黒マスの合計。自分の隣から始まる連なりを足す
以上。ルール自体は 3 行で終わる。にもかかわらず、この 3 行のどこが実際に難しさを作っているのかは、書いてみるまで自分の予想と一致しなかった。
「見える」という一語
数字の光線を外に向かって歩くと、答えがどうであれ形は必ずこうなる。黒が a マス、次に白が 1 マス、その先は何でもよい。
「その先は何でもよい」がこの puzzle の全部だ。白 1 マスの向こうに黒が 8 マス並んでいても、数字は 0 としか言わない。数字は行と列の総量ではなく、その手前の端しか報告していない。
出荷している 72 盤(8×8 と 12×12 を 36 枚ずつ)で測った。
| 盤 | 数字が言う値 | 自分の光線上に実在する黒 | 見えている割合 | 壁の向こうに運河がある光線 | 隠れているマス(平均) | 白1マスが隠した最大 |
|---|---|---|---|---|---|---|
| 8×8 | 3.84 | 5.28 | 72.8% | 9.5 | 1.73 | 6 |
| 12×12 | 5.06 | 7.83 | 64.7% | 25.5 | 2.34 | 10 |
盤が大きくなるほど、数字は自分の行と列に対して盲目になっていく。12×12 では 1 枚あたり 25.5 本の光線が「壁の向こうに運河を持つ」状態で、最大のものは白 1 マスが黒 10 連を隠していた。
同じ 3 という数字が、隣が 3 連なのか、隣が 1 マスで奥に何かあるのか、まったく区別しない。これがクロダコ(黒どこ)系の「見通し」パズルと Canal View を分ける点でもある。
視線を二度数える
黒マス 1 つを固定すると、それを見ている数字は各方向にたかだか 1 つ(最初にぶつかった数字だけ)。だから「(数字, その数字が見ている黒マス)」の組を二通りに数えられる。
- 数字側から数えれば、それは数字の総和
- マス側から数えれば、黒マス 1 つあたり 4 以下
したがって探索を一切しなくても運河の下限が出る。上限は 2×2 禁止から出る。h×w の格子で 2×2 を含まない最大の部分集合は hw − ⌊h/2⌋⌊w/2⌋ だ(1 行おきに全部塗り、その間の行は 1 マスおきに塗る)。
⌈Σ数字 / 4⌉ ≤ 運河 ≤ hw − ⌊h/2⌋⌊w/2⌋
| 盤 | 数字から出る下限 | 実際の運河 | 2×2 から出る上限 | 数字の総和 ÷ 運河 |
|---|---|---|---|---|
| 8×8 | 11 | 32.1 | 48 | 1.37 |
| 12×12 | 28 | 72.4 | 108 | 1.51 |
このサンドイッチは緩い。そしてその緩さの理由が、そのまま最初の節の内容になる。上限の 4 が達成されるのは「すべての黒マスが四方すべてから見えている」ときだけで、実測は 1.37(8×8)と 1.51(12×12)だった。足りない視線を分類するとこうなる。
| 盤 | (黒マス, 方向) の組 | 見えている | その方向に数字が無い | 途中に白がある | 遮蔽の割合 |
|---|---|---|---|---|---|
| 8×8 | 4,616 | 1,576 | 2,451 | 589 | 12.8% |
| 12×12 | 10,432 | 3,933 | 4,351 | 2,148 | 20.6% |
2 列目は運河のマス数のちょうど 4 倍、3 列目はちょうど数字の総和になる。まったく別の経路で計算した 2 つの数が一致するので、この恒等式はテストにしてある(npm test が盤ごとに検証する)。遮蔽は 12×12 で全視線の 20.6% を占め、盤が大きくなるほど増える。
黒マスを「何人の数字が見ているか」で分けると、8×8 では 1,154 マス中 96 マスが誰からも見えていない。
光線は 2m 通りではなく m+1 通り
さきほどの「黒 a、白 1、あとは自由」という形は、実装上とても都合がいい。長さ m の方向が取りうる状態は 2^m ではなく m+1(腕の長さ)しかなく、数字 1 つのクロス全体は 値を 4 つの腕に分ける有界な組成(composition) ちょうど 1 つで決まる。
ソルバーが分岐するのは、最後までこの組成の上だけだ。
| 盤 | 数字の個数 | 数字の光線上のマス | 2マス | 実際に成立する組成 |
|---|---|---|---|---|
| 8×8 | 11.4 | 8.9 | 478 | 8.4 |
| 12×12 | 21.6 | 13 | 8,192 | 17.9 |
だから完全ソルバーが安い。12×12 の中央値は探索ノード 7 個、最悪でも 319 個、1 枚 22.2 ms で解ける。
ラダーは 4 段。
-
arm— 組成に対する弧無矛盾(arc consistency)。今の盤面と矛盾する腕の割り当てを捨て、生き残った全部が一致する所だけ確定させる -
block— 加えて 2×2 のうち 3 つが黒なら残り 1 つは白 -
flow— 加えて連結性。もう合流できない 2 つの黒の塊は矛盾、運河が絶対に届かないマスは白、そこを失うと運河が二分されるマスは黒。さらに上のサンドイッチを残りマスの上下限として使う -
search— 完全ソルバー。最も制約の強い数字の組成で分岐する
| 段 | 確定マス 8×8 | 確定マス 12×12 | 完答 8×8 | 完答 12×12 |
|---|---|---|---|---|
arm |
54.6% | 41.9% | 0/36 | 0/36 |
block |
63.7% | 53.9% | 0/36 | 0/36 |
flow |
81.5% | 67.5% | 18/36 | 6/36 |
search |
100% | 100% | 36/36 | 36/36 |
運河は湖を囲んでいい
Canal View には「白マスが連結していること」というルールが無い。ぬりかべ系に慣れているとつい足してしまうが、無い。そして実際の盤はそれを遠慮なく使う。
| 盤 | 運河 | 盤に占める割合 | 湖(外から遮断された白の領域) | 湖のある盤 | 独立サイクル数 | ループのある盤 |
|---|---|---|---|---|---|---|
| 8×8 | 32.1 | 50.1% | 3.5 | 36/36 | 2.1 | 35/36 |
| 12×12 | 72.4 | 50.3% | 12.1 | 36/36 | 6.8 | 36/36 |
湖を囲む運河とは、サイクルを持つ運河のことだ。ここで 2×2 禁止が効いてくる。2×2 を含まないのに木でない最小の形は 8 マスのリング(3×3 の外周)で、しかも 8 マスではそれが唯一である。固定ポリオミノを両方の基準で数えると、2 つの概念が分かれる瞬間がそのまま見える。
| マス数 | 固定ポリオミノ | 2×2 を含まない | 木構造 | 2×2 無しでサイクル有り |
|---|---|---|---|---|
| 7 | 760 | 570 | 570 | 0 |
| 8 | 2,725 | 1,909 | 1,908 | 1 |
| 9 | 9,910 | 6,485 | 6,473 | 12 |
| 10 | 36,446 | 22,282 | 22,202 | 80 |
2 列目と 4 列目は登録済みの数列(A001168 と A066158)で、npm run ledger は列挙器がどちらかとずれた瞬間に出力を書かずに落ちる。3 列目(運河が実際に満たすべき条件)はどちらでもなく、OEIS にも見当たらなかった。
小さい格子に収まる運河そのものも数えた。全 2^hw 部分集合を 1 つずつ試す方法と、アンカーから成長させる方法の 2 通りで数え、一致を要求している(4×5 で連結集合 116,166 のうち運河は 25,822、22.2%)。
1 マス反転させると、どのルールが怒るか
出荷した答えの 1 マスを反転させる。6,301 通りすべてが不正だった。問題はどのルールがそれを止めたかだ。
| ルール | 止めた反転 | そのルールだけが止めた反転 |
|---|---|---|
| 数字 | 5,298 | 2,974 |
| 2×2 禁止 | 932 | 197 |
| 運河の連結性 | 2,395 | 806 |
| どれも止めない | 0 | — |
15.9% は 2×2 か連結性だけが止めている。 言い換えると、この 2 つのどちらかを落とした瞬間に、答えの 1 マス編集がその数だけ合法になる。これは次の表で実際に測れる。
4 通りの誤読と、その結末
| 誤読 | 盤 | 枚数 | 意図した答えが合法か | 解の個数(中央値) | 一意 | 解無し |
|---|---|---|---|---|---|---|
| 2×2 禁止を落とす | 8×8 | 24 | 24/24 | 32 | 2 | 0 |
| 12×12 | 24 | 24/24 | ≥2,000 | 0 | 0 | |
| 連結性を落とす | 8×8 | 24 | 24/24 | 1,248 | 0 | 0 |
| 12×12 | 24 | 24/24 | ≥2,000 | 0 | 0 | |
| 白も連結させる | 8×8 | 24 | 0/24 | 0 | 0 | 24 |
| 12×12 | 24 | 0/24 | 0 | 0 | 24 | |
| 数字を行・列全体と読む | 8×8 | 12 | 0/12 | 0 | 0 | 11 |
| 12×12 | 12 | 0/12 | 0 | 0 | 12 |
上下 2 組は逆向きに壊れる。ルールを落とすと、意図した答えは合法なまま数百の解に埋もれる——だから解けてしまうし、2 つ目の解に気づくまで何もおかしく見えない。無いルールを足す(白も連結させる)か、見えないものまで数えると、意図した答えのほうが違法になり、盤は空になる。存在しない答えを延々と探すことになるのはこちら側だ。
ダイヤルは「残す数字の数」
生成器は答えを先に描く。ランダムな連結・2×2 なしの集合を成長させ、白マス全部に数字を書き、そこから 1 つずつ数字を消していく——一意性が保たれる限り。
白マス全部に数字を書いた盤は、答えを声に出して言っているようなものだ。そこからランダムに消すとどうなるか。
| 残した数字(8×8) | 試行 | 一意 | 率 |
|---|---|---|---|
| 白マス全部 | 108 | 108 | 100.0% |
| 26 | 108 | 83 | 76.9% |
| 22 | 108 | 54 | 50.0% |
| 18 | 108 | 21 | 19.4% |
| 15 | 108 | 6 | 5.6% |
| 12 | 108 | 0 | 0.0% |
| 10 | 108 | 0 | 0.0% |
出荷盤が持っている数字は 8×8 で 11.4 個、12×12 で 21.6 個。ランダムに 12 個まで削った 108 回の試行で一意は 0 回(12×12 で 22 個に削った 108 回も 0 回)。出荷盤の数字は 1 つ残らず「消してみて、駄目だったから戻した」ものだ。
そして表の 1 行目には落とし穴がある。白マス全部に数字を書いても一意とは限らない。 新しく引いた 8×8 の運河 200 本のうち 3 本は、全マスに数字を書いてなお 2 つ目の答えを持っていた。動いたマスは 3 本とも、どの数字からも見えていないマスだった。答えを全部書き出しても、言ったことにならない場合がある。
偶然には生まれない
数字をランダムな位置に、ランダムな値で置いてみる(実際の盤が使う小さい範囲と、光線が許す全範囲の 2 通り)。1,600 回引いて一意な盤は 0 枚。面白いのは「解無し」の列で、答えの周りに作られていない盤は、そもそも解けないことのほうが多い。要求している数と、それを支える運河の形が、噛み合わないからだ。
2 つのエンジン
組成による分解がこの実装の要なので、それを一切知らないエンジンと突き合わせている。マスを読み順に走査し、黒→白の順に試し、枝刈りは「すでに閉じた 2×2」と「もう値に着地できない数字」だけ、最後にルールを直接検査する。
6×6 の 30 盤(総当たり側 1,939,359 ノード)で、両者は 30 回一致・0 回不一致。しかも解の集合が一致している。2×2 禁止を落として解が数百に増える設定でも 30 回一致(合計 1,126 解)。
まとめ
3 行のルールのうち、盤を難しくしているのは数字ではなかった。数字は運河の 65〜73% しか見ておらず、残りは白 1 マスの陰に隠れている。それでも盤が一意になるのは、見えない部分を連結性と 2×2 禁止が押さえているからだ。1 マス反転の 15.9% はその 2 つだけが止めている。
- 実装: TypeScript + Vite、ランタイム依存ゼロ、20 テスト
- 生成: 答えを描いてから数字を削る。出荷 72 盤はすべて「これ以上削れない」局所最大
- 検証: 完全ソルバーと素朴エンジンの解集合一致、A001168 / A066158 との照合、格子上の運河数を 2 通りで計数
デモとソースは冒頭のリンクから。記事中の数値はすべて npm run stats / npm run ledger の出力から生成していて、README とページの本文はその JSON から機械的に書き出している。
