Orcantus

Guide

Pattern(行・列の数字で塗るお絵かきロジック)の解き方 — 中級編: 候補列挙だけで止まったら、1マスだけ仮に決めて矛盾を探す

初級編では、行・列の数字が許すすべての塗り方を書き出して見比べるだけで、仮定を一度も置かずに解ける5×5盤を扱いました。この中級編では、その繰り返しだけでは一部のマスが2択のまま残ってしまう場面を扱います。実例はn=8・シード"notes-pattern-scan-8-65"で生成した8×8盤面です(E.generate(8, 'notes-pattern-scan-8-65')、2回目の試行でこの条件を満たしました)。

Note

初級編と同じく、ここでの「中級」はこの記事シリーズの読み物としての順番です。Pattern本体には個々の盤面の解きやすさを判定する仕組みがなく、8×8という盤サイズも、スタート画面の「盤面サイズ(難易度の保証ではありません)」という注記のとおり、解く上での難しさを保証するものではありません。

結論。行・列の候補列挙を繰り返しても絞り切れないマスが残ったら、そのうち1つを仮に「塗り」(または「空白」)と決めて、同じ候補列挙を波及させる。波及の結果、ある行(列)の数字を満たせる塗り方が候補として1つも残らなくなったら、その仮定は誤り。もう一方が正解になる。 この記事の実例では、この「仮定→波及→矛盾の発見」が1か所で1回だけ必要でした。

まず候補列挙で絞れるところまで絞る

初級編と同じ手順(行・列の候補列挙の繰り返し)を、この8×8盤面(64マス)に適用します。出題時点の数字配置は次のとおりです。

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. 行1列4=空白と仮定する。
  2. 行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=塗りに確定します。
  3. 列7(数字「1 2」) を見ると、行1列7が塗りで確定したことで、数字の最初の「1」がこの1マスで満たされます。残る「2」の連は行1より下のどこかに入りますが、この時点で列7の候補列挙をやり直すと、行2列7・行3列7・行4列7が空白に確定します。
  4. 行2(数字「1 1」)・行3(数字「1 2 1」)・行4(数字「3 1 1」) は、列7が空白に確定したことを受けて候補列挙をやり直すと、いずれも列8を塗りでなければ数字を満たせないという結果になり、行2列8・行3列8・行4列8が塗りに確定します。
  5. ここで列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」との食い違いが判明しました。

手順のまとめ

  1. 行・列の候補列挙で絞れるところまで絞る(この実例では64マス中48マス)。
  2. 残ったマスの中から1つを選び、仮に「塗り」(または「空白」)と決める。
  3. 同じ候補列挙を波及させる。
  4. 波及の途中で、ある行(列)の数字を満たせる塗り方の候補が1つも残らなくなった場合、その仮定は誤り。逆側に決め直して手順3をやり直す。
  5. 矛盾が起きなければ、そのまま候補列挙を波及させて残りを埋める。

誤りやすい判断

読者の検証方法

この記事が確認していないこと

この結果が言っていないこと

  • 検証はpattern/engine.js・pattern/app.js(エンジンv1、2026-09-29時点)を対象にしています。将来の版で表示・判定基準が変わる可能性があります。
  • この記事で示した「矛盾までの4段階」は、実際にpattern/engine.jsのpropagate関数と同じ候補列挙のロジックを、各マス・各行・各列の確定済みの値を1つずつ突き合わせて手動で適用し、その結果をNode.js上で実行・記録したものです。手動での適用順は筆者が選んだものであり、propagate関数自身の内部的な処理順(全行・全列を毎回走査し直す実装)とは一致しません。