スリザーリンク#

スリザーリンク(Slitherlink)は、方眼状の盤面に配置された数字をヒントにして、線を引いて一つの閉じたループを作るパズルです。以下に基本ルールを説明します。

スリザーリンクパズルのサイト

https://ja.puzzle-loop.com

helper.puzzle.extract_slither_link() を使うと、このサイトのパズル盤面を取得できます。ブラウザの開発者ツールを開き、盤面を表す <div> タグのコードをコピーし、この関数を実行すると、盤面が配列として取得できます。

  1. 線を引く
    ・盤面の点(格子点)を結ぶように、縦または横に線を引くことができます。
    ・線は途中で途切れたり、交差したり、枝分かれしたりしてはいけません。

  2. 数字の意味
    ・マスに書かれた数字は、そのマスの周囲に引かれる線の本数を表します。
    ・例えば「3」が書かれたマスの周囲には、必ず3本の線が引かれます。
    ・「0」のマスの周囲には線を引いてはいけません。
    ・数字が書かれていないマスは、線の本数に制限がありません。

  3. 閉じたループを作る
    ・すべての線はつながり、一つの閉じたループ(輪)を形成しなければなりません。
    ・ループは枝分かれや交差をしてはいけません。

盤面上の点を頂点とし、ループの横線または縦線を辺とすることで、スリザーリンクは回路を探索する問題となります。そのため、CP-SATのadd_circuit()制約を使用して解くことができます。 また、すべての頂点を通る必要がないため、特定の頂点を除外できるように、すべての頂点に対して(i, i, bool_var)の辺をadd_circuit()に追加します。

from ortools.sat.python import cp_model
import numpy as np
from matplotlib import pyplot as plt
from collections import defaultdict

盤面の読み込みと描画#

次のコードで盤面を読み込み、描画します。

from matplotlib.collections import LineCollection

def plot_slither_link_board(board, result=None):
    h, w = board.shape    

    fig, ax = plt.subplots(figsize=(w*0.3, h*0.3))
    ax.set_ylim(-0.1, h + 0.1)
    ax.set_xlim(-0.1, w + 0.1)
    ax.invert_yaxis()
    ax.set_aspect("equal")
    all_segments = []
    circuit_segments = []

    if result is not None:
        for (n1, n2), flag in result.items():
            if flag:
                r1, c1 = n1 // (w + 1), n1 % (w + 1)
                r2, c2 = n2 // (w + 1), n2 % (w + 1)                
                circuit_segments.append([(c1, r1), (c2, r2)])

    for r, c in np.ndindex(h + 1, w + 1):
        if r + 1 <= h:
            all_segments.append([(c, r), (c, r + 1)])
        if c + 1 <= w:
            all_segments.append([(c, r), (c + 1, r)])

    ax.add_collection(LineCollection(all_segments, color='gray', alpha=0.5))
    ax.add_collection(LineCollection(circuit_segments, color='green', linewidth=2))
    for (i, j), v in np.ndenumerate(board):
        if v != -1:
            ax.text(j + 0.5, i + 0.5, str(v), fontsize=12, va='center', ha='center')
    ax.autoscale_view()        
    ax.axis('off')
    return fig, ax

board = np.loadtxt('data/slitherlink01.txt', dtype=np.int8)
plot_slither_link_board(board);
_images/861adc6b28faa4db115520445e6b162501dfb6d070c8af2f55a89c8203bd8151.png

解くコード#

次のコードは、スリザーリンクパズルを解くクラス SlitherLinkSolver を定義しています。

class SlitherLinkSolver:
    def __init__(self, board):
        self.board = board
        h, w = self.board.shape
        self.nodes = nodes = np.arange((h + 1) * (w + 1)).reshape(h + 1, w + 1)

        self.edges = edges = []
        for (r, c), s in np.ndenumerate(nodes):
            if c + 1 <= w:
                right = (r, c + 1)
                e = nodes[right]
                edges.extend([(s, e), (e, s)])
            if r + 1 <= h:
                below = (r + 1, c)
                e = nodes[below]
                edges.extend([(s, e), (e, s)])
        
        self.cell_edges = cell_edges = defaultdict(set)
        for (r, c), _ in np.ndenumerate(board):
            n1 = nodes[r, c]
            n2 = nodes[r, c + 1]
            n3 = nodes[r + 1, c]
            n4 = nodes[r + 1, c + 1]
            for edge in [(n1, n2), (n1, n3), (n2, n4), (n3, n4)]:
                cell_edges[r, c].add(edge)
                cell_edges[r, c].add(edge[::-1])
        
        model = cp_model.CpModel()
        variables = {e:model.new_bool_var(f'edge{e}') for e in edges}
        dummies = [(i, i, model.new_bool_var(f'dummy_{i}')) for i in range(0, (h + 1) * (w + 1))]
        model.add_circuit([(s, e, v) for (s, e), v in variables.items()] + dummies)
        
        for (r, c), count in np.ndenumerate(board):
            box_edges = [variables[edge] for edge in cell_edges[r, c]]
            if count != -1:
                model.add(sum(box_edges) == count)
        
        solver = cp_model.CpSolver()
        solver.solve(model)
        self.result = {key:solver.value(val) for key, val in variables.items()}

solver = SlitherLinkSolver(board)
plot_slither_link_board(board, solver.result);
_images/83c14fc04dc8070a665640666921497d1e6b634b49a3aa4da039189a82bba25c.png

コードの説明#

h × w の盤面の場合、(h+1) × (w+1) の格子点が存在します。各格子点を 1 次元の整数インデックスで管理し、形状 (h+1, w+1) の NumPy 配列 nodes に格納します。これにより、各ノードに固有の ID を割り当てることができます。

self.nodes = nodes = np.arange((h + 1) * (w + 1)).reshape(h + 1, w + 1)

次に、各ノード (r, c) について、右 (r, c+1) および下 (r+1, c) に隣接するノードとの間に双方向の辺 (s, e), (e, s) を追加します。これにより、ループを形成するためのすべての候補となる線が self.edges に格納されます。

self.edges = edges = []
for (r, c), s in np.ndenumerate(nodes):
    if c + 1 <= w:
        right = (r, c + 1)
        e = nodes[right]
        edges.extend([(s, e), (e, s)])
    if r + 1 <= h:
        below = (r + 1, c)
        e = nodes[below]
        edges.extend([(s, e), (e, s)])

次に、各セル (r, c) について、そのセルを構成する 4 本の辺(合計 8 つの有向辺)を cell_edges[r, c] に格納します。

self.cell_edges = cell_edges = defaultdict(set)
for (r, c), _ in np.ndenumerate(board):
    n1 = nodes[r, c]
    n2 = nodes[r, c + 1]
    n3 = nodes[r + 1, c]
    n4 = nodes[r + 1, c + 1]
    for edge in [(n1, n2), (n1, n3), (n2, n4), (n3, n4)]:
        cell_edges[r, c].add(edge)
        cell_edges[r, c].add(edge[::-1])

各辺に対応するブール変数を作成します。

model = cp_model.CpModel()
variables = {e: model.new_bool_var(f'edge{e}') for e in edges}

add_circuit() は回路制約を追加し、1 つの閉じたループを強制します。ただし、スリザーリンクではすべてのノードを通る必要がないため、(i, i, bool_var) の自己ループ(ダミー辺)を作成し、add_circuit() に追加します。この自己ループを利用することで、ループに含まれないノードを表現し、部分的な回路の除外を可能にします。

dummies = [(i, i, model.new_bool_var(f'dummy_{i}')) for i in range((h + 1) * (w + 1))]
model.add_circuit([(s, e, v) for (s, e), v in variables.items()] + dummies)

最後に、セルごとの辺の制約を追加します。マスに書かれた数字 count-1 でない場合、そのマスの周囲の線の本数が count になるように制約を追加します。

for (r, c), count in np.ndenumerate(board):
    box_edges = [variables[edge] for edge in cell_edges[r, c]]
    if count != -1:
        model.add(sum(box_edges) == count)

最も難しいパズル#

最後に、最も難しいパズルを解いてみます。

board = np.loadtxt('data/slitherlink02.txt', dtype=np.int8)
solver = SlitherLinkSolver(board)
plot_slither_link_board(board, solver.result);
_images/029e17549c76475096d58a1466513c795825450ef60926aaa1e3db46eae6881c.png