Chapter 9
Slitherlink
Slitherlink draws a single closed loop on a lattice of dots. The grid is a rectangle of cells; some cells carry a small clue digit between and . The solver’s task is to draw a non-self-crossing closed curve along the lattice edges between dots, such that for every clue cell the number of edges on the curve incident to that cell exactly equals the clue. A clue of forbids any of the cell’s four edges from being on the loop; a forces three of them on; a or leaves exactly one or two free.
The puzzle was published in in Nikoli’s puzzle magazine under the name Suri-rinku (Japanese for “slitherlink”), designed by an editor going by the handle Renin. It travelled to the West in the late s and is now solved by aficionados in several languages, often under the names Loop the Loop or Fences. Of all Nikoli puzzles, Slitherlink is perhaps the purest of the loop-puzzle family: the entire content of a solution is a single closed curve.
The new modelling ingredient is the curve itself. Up to this chapter the puzzles in this book have placed labels in cells. Here the unknowns live on the edges between dots, and the non-trivial constraint is that the chosen edges form a single closed loop, not two or more. CP-SAT supports this directly through its AddCircuit primitive, which lets a CP model declare that a set of directed arcs must form a Hamiltonian circuit; we use it via the standard trick that lets vertices “skip” the loop with a self-loop arc.
Rules and a small instance
A Slitherlink puzzle is an rectangle of cells. Vertices sit at lattice points with and ; edges connect each pair of horizontally or vertically adjacent vertices. A subset of cells carries a clue: an integer from to .
Each edge is either on the loop or off.
For each clue cell, the number of its four bounding edges that are on the loop equals the clue.
The set of on-edges forms a single closed loop: a non-self-crossing closed curve on the edge graph.
Equivalently, every vertex in the lattice has degree or in the on-edge graph (degree if not on the loop, if on), and the on-edges form a single connected component.
Figure 9.1 is a instance with eleven clues, generated and checked for uniqueness by the solver of this chapter.
The programming model
Let denote the boolean for the horizontal edge from vertex to vertex , and for the vertical edge from to . A cell at has bounding edges (top), (bottom), (left), and (right).
The clue constraint is direct:
The vertex degree constraint, for each lattice vertex , is that the sum of its incident edges is or . CP-SAT expresses this with a Boolean indicator: a variable that is iff the vertex is on the loop, with .
The single-loop constraint
The clue and degree constraints alone admit configurations with multiple disjoint loops. To rule those out, the model uses CP-SAT’s AddCircuit primitive. The trick: turn each undirected edge into two directed arcs (one per direction); add a self-loop arc at every vertex that is enabled iff the vertex is off the loop; then AddCircuit on the union of arcs forces the active set to be a single Hamiltonian circuit on the vertex set, where “Hamiltonian” visits every vertex but uses the self-loop for vertices not on the actual loop.
Concretely, for each undirected edge we introduce two arc booleans and with the constraint , so exactly one is true when the edge is on the loop, and both are false otherwise. For each vertex we introduce a self-loop arc with the constraint iff vertex has degree in the loop. AddCircuit on the resulting arc list does the rest.
A solver in forty lines
from ortools.sat.python import cp_model
def solve_slitherlink(R, C, clues):
"""clues: {(r, c): k} for clued cells, k in 0..3."""
m = cp_model.CpModel()
h = {(r, c): m.NewBoolVar("")
for r in range(R+1) for c in range(C)}
v = {(r, c): m.NewBoolVar("")
for r in range(R) for c in range(C+1)}
for (r, c), k in clues.items():
m.Add(h[(r, c)] + h[(r+1, c)]
+ v[(r, c)] + v[(r, c+1)] == k)
arcs = []
vid = lambda r, c: r * (C+1) + c
for (r, c), e in h.items():
f = m.NewBoolVar(""); b = m.NewBoolVar("")
m.Add(f + b == e)
arcs.append((vid(r, c), vid(r, c+1), f))
arcs.append((vid(r, c+1), vid(r, c), b))
for (r, c), e in v.items():
f = m.NewBoolVar(""); b = m.NewBoolVar("")
m.Add(f + b == e)
arcs.append((vid(r, c), vid(r+1, c), f))
arcs.append((vid(r+1, c), vid(r, c), b))
for r in range(R+1):
for c in range(C+1):
edges = []
if c > 0: edges.append(h[(r, c-1)])
if c < C: edges.append(h[(r, c)])
if r > 0: edges.append(v[(r-1, c)])
if r < R: edges.append(v[(r, c)])
d = sum(edges)
sl = m.NewBoolVar("")
m.Add(d == 0).OnlyEnforceIf(sl)
m.Add(d == 2).OnlyEnforceIf(sl.Not())
arcs.append((vid(r, c), vid(r, c), sl))
m.AddCircuit(arcs)
s = cp_model.CpSolver()
s.Solve(m)
return ({(r, c): s.Value(h[(r, c)]) for (r, c) in h},
{(r, c): s.Value(v[(r, c)]) for (r, c) in v})
The toy of Figure 9.1 solves in about milliseconds.
A hard instance
Figure 9.3 is a instance, again generated and checked for uniqueness by this chapter’s solver. Thirty-five clues sit on cells, a little over a third; the loop must thread edges through the lattice without intersecting itself. The same solver code returns the unique loop in a few milliseconds.
Sources. Slitherlink was first published by Nikoli in in the magazine Puzzle Communication Nikoli under the Japanese name Suri-rinku, attributed to the editor Renin. The puzzle is also known in English as Loop the Loop or Fences. The toy and hard instances shown here were generated and checked for a unique solution by the AddCircuit model of this chapter. The AddCircuit-with-self-loops technique used in the model is documented in the OR-Tools reference and is the standard CP modelling trick for connectivity-with-skip on undirected graphs.