Note
初級編と同じく、ここでの「中級」はこの記事シリーズの読み物としての順番です。Pattern本体には個々の盤面の解きやすさを判定する仕組みがなく、8×8という盤サイズも、スタート画面の「盤面サイズ(難易度の保証ではありません)」という注記のとおり、解く上での難しさを保証するものではありません。
結論。行・列の候補列挙を繰り返しても絞り切れないマスが残ったら、そのうち1つを仮に「塗り」(または「空白」)と決めて、同じ候補列挙を波及させる。波及の結果、ある行(列)の数字を満たせる塗り方が候補として1つも残らなくなったら、その仮定は誤り。もう一方が正解になる。 この記事の実例では、この「仮定→波及→矛盾の発見」が1か所で1回だけ必要でした。
まず候補列挙で絞れるところまで絞る
初級編と同じ手順(行・列の候補列挙の繰り返し)を、この8×8盤面(64マス)に適用します。出題時点の数字配置は次のとおりです。
- 行の数字(行1〜行8): 「1 3」「1 1」「1 2 1」「3 1 1」「1 1」「3 3」「2 1」「3」
- 列の数字(列1〜列8): 「4 1」「2 1 1 1」「1 1 1」「1 1」「1 2 2」「1 2」「1 2」「2 1」
pattern/engine.jsのpropagate関数をこの数字配置に対して実行すると、64マス中48マスが確定します。図にすると次のとおりです(#=塗り、.=空白、?=未確定)。
. # . ? # # ? .
. # . . . . ? ?
# . . # # . ? ?
# # # . # . ? ?
# . . ? . ? ? ?
# # # . # # # .
. . . ? # ? ? ?
# # # . . . . .
残る16マスは、行1〜行7・列1〜列8にまたがって点在しています。これ以上、候補列挙だけを繰り返しても、この16マスの状態はこれ以上絞れません(実際にNode.js上でさらに数巡試しましたが、状態は変化しませんでした)。これが「候補列挙だけでは解けない」局面です。
1マスを仮に「塗り」と決めて波及させる
未確定の16マスのうち、最初に見つかる行1列4(行1の数字「1 3」の一部)を仮に決めてみます(この位置に特別な意味はなく、未確定のマスを盤面の左上から順に見て最初に見つかったものです)。
仮定その1: 行1列4=空白 → 矛盾
- 行1列4=空白と仮定する。
- 行1(数字「1 3」) を見ると、すでに行1列1=空白・行1列2=塗り・行1列3=空白・行1列5=塗り・行1列6=塗り・行1列8=空白が確定しています。数字の「1」は行1列2の1マスで満たし済みなので、残る「3」の連は、行1列4が空白になった今、行1列5・列6とすでに塗りの2マスに加えて行1列7も塗りでなければ長さ3を満たせません。候補列挙をやり直すと、この時点で行1列7=塗りに確定します。
- 列7(数字「1 2」) を見ると、行1列7が塗りで確定したことで、数字の最初の「1」がこの1マスで満たされます。残る「2」の連は行1より下のどこかに入りますが、この時点で列7の候補列挙をやり直すと、行2列7・行3列7・行4列7が空白に確定します。
- 行2(数字「1 1」)・行3(数字「1 2 1」)・行4(数字「3 1 1」) は、列7が空白に確定したことを受けて候補列挙をやり直すと、いずれも列8を塗りでなければ数字を満たせないという結果になり、行2列8・行3列8・行4列8が塗りに確定します。
- ここで列8(数字「2 1」)を見ると、行2列8・行3列8・行4列8の3マスがすべて塗りという情報が確定しています。しかし列8の数字「2 1」は、最大でも「連続2マス→空白1マス以上→連続1マス」の組み合わせにしかならず、3マス連続で塗るrunはこの数字に存在しません。候補列挙をやり直すと、列8の候補(
filterPlacementsが返す配置)が1つも残らなくなります。
この時点で矛盾が確定します。したがって、仮定(行1列4=空白)は誤りで、正しくは「塗り」です。
仮定をひっくり返すと、残り全部が候補列挙だけで決まる
「行1列4=塗り」で確定し直したうえで、候補列挙を再び波及させると、今度は矛盾なく残り15マスすべてが確定します(pattern/engine.jsのpropagate関数に、このマスを「塗り」に固定した状態を渡して実行し、有効な状態のまま全64マスが確定することを確認済みです)。波及は次の4ラウンドで進みます。
| ラウンド | 確定したマス | 根拠 |
|---|---|---|
| 1 | 行1列7=空白 | 行1(1 3)、行1列4が塗りで確定した残り候補から |
| 1 | 行5列4=空白・行7列4=空白 | 列4(1 1) |
| 2 | 行7列6=塗り・行7列7=空白・行7列8=塗り | 行7(2 1) |
| 2 | 行5列6=空白 | 列6(1 2) |
| 2 | 行4列7=空白・行5列7=塗り | 列7(1 2) |
| 3 | 行4列8=塗り・行5列8=空白 | 行4(3 1 1)・行5(1 1) |
| 3 | 行2列8=空白・行3列8=塗り | 列8(2 1) |
| 4 | 行2列7=塗り・行3列7=空白 | 行2(1 1)・行3(1 2 1) |
完成形は次のとおりです(#が塗り、.が空白)。
. # . # # # . .
. # . . . . # .
# . . # # . . #
# # # . # . . #
# . . . . . # .
# # # . # # # .
. . . . # # . #
# # # . . . . .
この塗り分けが全数字と一致することはcluesFor関数で、E.generateが実際に持っていた答えと1マスの違いもなく一致することも確認済みです。
なぜ候補列挙の繰り返しだけでは見抜けないのか
候補列挙は「その行(列)だけ」を見て判定します。行1列4を仮に空白と決めても、決めた瞬間には行1の数字自体とは矛盾しません(候補が0件にはならない)。矛盾が表面化するのは、その仮定から数手先まで波及させて、複数の行・列の情報が積み重なったときです。この記事の実例では、行1から始まった連鎖が、列7・行2〜4を経て列8に戻ってくるまでの4段階を経て初めて、列8の数字「2 1」との食い違いが判明しました。
手順のまとめ
- 行・列の候補列挙で絞れるところまで絞る(この実例では64マス中48マス)。
- 残ったマスの中から1つを選び、仮に「塗り」(または「空白」)と決める。
- 同じ候補列挙を波及させる。
- 波及の途中で、ある行(列)の数字を満たせる塗り方の候補が1つも残らなくなった場合、その仮定は誤り。逆側に決め直して手順3をやり直す。
- 矛盾が起きなければ、そのまま候補列挙を波及させて残りを埋める。
誤りやすい判断
- 仮定した直後に矛盾が出ないからといって、その仮定が正しいとは限らない。 この記事の実例では、矛盾が判明するまでに4段階の波及が必要でした。1〜2手先で矛盾が出ないからと安心せず、波及が止まるところまで進めてください。
- どのマスを仮定に選ぶかは、この記事の手順の正しさに影響しません。 未確定の16マスのうちどれを最初に仮定しても、正しい方の状態を選べば最終的には同じ完成形にたどり着きます(この記事が行1列4を選んだのは、単に未確定のマスの一つだったからです)。ただし、選ぶマスによって矛盾までの手数(波及の長さ)は変わります。
- 仮定は「賭け」ではなく、UIの「一手戻す」で安全に試せる操作です。 仮に塗ってみて矛盾が見えたら、「一手戻す」でそのマスを含む変更を取り消し、逆側を試せます。ただしPattern本体は数字の超過を途中で警告しないため(初級編で述べたとおり)、矛盾に気づくのは自分でその行・列の数字を数え直したときになります。
読者の検証方法
- 同じ盤面は
n=8・シードnotes-pattern-scan-8-65で再現できます。 - 「行1列4=空白」を実際に試し、上の手順どおりに列8で3マス連続の塗りが数字「2 1」と食い違う状態まで進むことを、自分の手元で確認できます。
- 不安な場合は、仮定を1つ試すたびに「1マスのヒント」で答え合わせしながら進めても、手順の妥当性は変わりません。
この記事が確認していないこと
- 「未確定のマスが16個残ったら、1回の仮定と4段階の波及で解ける」という具体的な展開は、この記事の実例固有のものです。README・コードのいずれにも「中級相当の問題は仮定1回で解ける」という基準は明記されておらず、別の8×8盤面で同じ手数になることは保証されません。
- 上級編(その3)で扱う、1回の仮定では終わらず、仮定の上にさらに仮定を重ねる必要があるケースは扱っていません。
この結果が言っていないこと
- 検証は
pattern/engine.js・pattern/app.js(エンジンv1、2026-09-29時点)を対象にしています。将来の版で表示・判定基準が変わる可能性があります。 - この記事で示した「矛盾までの4段階」は、実際に
pattern/engine.jsのpropagate関数と同じ候補列挙のロジックを、各マス・各行・各列の確定済みの値を1つずつ突き合わせて手動で適用し、その結果をNode.js上で実行・記録したものです。手動での適用順は筆者が選んだものであり、propagate関数自身の内部的な処理順(全行・全列を毎回走査し直す実装)とは一致しません。