Chapter 13
Nurikabe
Nurikabe is the puzzle of islands in a sea. The grid is a rectangle of cells; some cells carry positive integers. The solver must shade some cells black and leave the rest white, subject to four rules. Every numbered cell is white and serves as the seed of a connected white island of exactly the declared size. Islands of different seeds do not touch each other along an edge. The black cells together form a single connected sea. Nowhere does the sea contain a all-black square.
The puzzle was published by Nikoli in , with the name Nurikabe drawn from a yokai of Japanese folklore: a white-walled spirit that obstructs travellers’ paths. It joins the family of shading puzzles of which Heyawake, LITS, and Hitori are also members; among them it is the one whose constraints are most balanced between local (no , adjacency) and global (per-region connectivity, region size).
The chapter introduces the cleanest CP-SAT pattern for multi-region connectivity: one BFS-distance variable per cell, with the distance equal to zero at the region’s root (a clue cell for islands, a designated cell for the sea), and a reified “has a same-region neighbour with ” constraint elsewhere. The same pattern will return in subsequent shading puzzles.
Rules and a small instance
A Nurikabe puzzle is an grid. A subset of cells carry a positive integer clue indicating the size of the white island whose seed it is. The constraints:
Every cell is either black or white.
Each numbered cell is white and belongs to a connected white region of exactly the declared size.
Two distinct islands share no edge.
The set of black cells is connected.
No block of cells is entirely black.
Figure 13.1 is puzzle from the janko.at Nurikabe archive, an instance by Ooya Tate Tadashi. The clues run from (a singleton island) to .
The programming model
Let denote the region label of cell , an integer between and where is the number of clues. The label marks the sea; labels mark the islands in some fixed enumeration of the clue cells. Let be the indicator (“ is sea”).
The local constraints are direct.
Each clue cell has and .
For each island , (the clue’s value).
For each pair of orthogonally adjacent cells with , at least one of is . Equivalently: two whites that are adjacent must share a label.
The total number of sea cells is .
No block is entirely sea: .
Connectivity by BFS-distance
Local constraints alone do not enforce that each region is connected. To do so, we attach a distance variable to every cell, intended to hold the BFS distance from the cell to the root of its region.
For each clue , (the clue is the root of island ).
Exactly one sea cell has (the sea root); introduce a Boolean for each cell with , and .
For every other cell, and there exists an orthogonal neighbour with and .
The third bullet’s existential is a reified disjunction over the four neighbours; CP-SAT handles it through AddBoolOr guarded by OnlyEnforceIf. The result is that the cells of any region form a tree rooted at the declared root, hence are all reachable from the root, hence connected.
A solver in seventy lines
from ortools.sat.python import cp_model
def solve_nurikabe(R, C, clues):
"""{(r,c): size} -> 2D grid of {0,1}."""
cs = sorted(clues)
N = len(cs)
sea_total = R * C - sum(clues.values())
m = cp_model.CpModel()
reg = {(r, c): m.NewIntVar(0, N, "")
for r in range(R) for c in range(C)}
sea = {(r, c): m.NewBoolVar("")
for r in range(R) for c in range(C)}
for k in reg:
m.Add(reg[k] == 0).OnlyEnforceIf(sea[k])
m.Add(reg[k] >= 1).OnlyEnforceIf(sea[k].Not())
for i, c in enumerate(cs, 1):
m.Add(reg[c] == i)
for i, c in enumerate(cs, 1):
bs = []
for k in reg:
b = m.NewBoolVar("")
m.Add(reg[k] == i).OnlyEnforceIf(b)
m.Add(reg[k] != i).OnlyEnforceIf(b.Not())
bs.append(b)
m.Add(sum(bs) == clues[c])
m.Add(sum(sea.values()) == sea_total)
for r in range(R-1):
for c in range(C-1):
m.Add(sea[(r,c)] + sea[(r+1,c)]
+ sea[(r,c+1)] + sea[(r+1,c+1)] <= 3)
def same_region(p, q):
b = m.NewBoolVar("")
m.Add(reg[p] == reg[q]).OnlyEnforceIf(b)
m.Add(reg[p] != reg[q]).OnlyEnforceIf(b.Not())
return b
for r in range(R):
for c in range(C):
for dr, dc in [(0,1),(1,0)]:
nr, nc = r+dr, c+dc
if not (0 <= nr < R and 0 <= nc < C):
continue
m.AddBoolOr([same_region((r,c), (nr,nc)),
sea[(r,c)], sea[(nr,nc)]])
d = {(r, c): m.NewIntVar(0, R*C-1, "")
for r in range(R) for c in range(C)}
for c in cs: m.Add(d[c] == 0)
rho = {(r, c): m.NewBoolVar("")
for r in range(R) for c in range(C)}
for k in rho:
m.Add(d[k] == 0).OnlyEnforceIf(rho[k])
m.Add(sea[k] == 1).OnlyEnforceIf(rho[k])
if sea_total > 0:
m.Add(sum(rho.values()) == 1)
def less_dist(p, q):
b = m.NewBoolVar("")
m.Add(d[q] + 1 == d[p]).OnlyEnforceIf(b)
m.Add(d[q] + 1 != d[p]).OnlyEnforceIf(b.Not())
return b
cs_set = set(cs)
for r in range(R):
for c in range(C):
if (r, c) in cs_set: continue
sr = rho[(r, c)]
m.Add(d[(r,c)] >= 1).OnlyEnforceIf(sr.Not())
preds = []
for dr, dc in [(-1,0),(1,0),(0,-1),(0,1)]:
nr, nc = r+dr, c+dc
if not (0 <= nr < R and 0 <= nc < C):
continue
same = same_region((r,c), (nr,nc))
less = less_dist((r,c), (nr,nc))
pred = m.NewBoolVar("")
pair = [same, less]
neg = [same.Not(), less.Not()]
m.AddBoolAnd(pair).OnlyEnforceIf(pred)
m.AddBoolOr(neg).OnlyEnforceIf(pred.Not())
preds.append(pred)
m.AddBoolOr(preds).OnlyEnforceIf(sr.Not())
s = cp_model.CpSolver()
s.Solve(m)
return [[s.Value(sea[(r, c)])
for c in range(C)] for r in range(R)]
The toy of Figure 13.1 solves uniquely in about milliseconds.
A hard instance
Figure 13.3 is puzzle from the janko.at Nurikabe archive, the hardest instance there, by Koyoppz. With fourteen clues totalling white cells, the sea has cells and threads sinuously around several neighbouring islands. CP-SAT returns the unique shading in about milliseconds.
Sources. Nurikabe was published by Nikoli in . The name comes from a Japanese folkloric yokai, a white-walled spirit that blocks travellers. The toy is puzzle in the janko.at Nurikabe archive (Ooya Tate Tadashi); the hard instance is puzzle (Koyoppz). McPhail established the NP-completeness of Nurikabe in via a reduction from -SAT (Carleton University technical report); the BFS-distance connectivity formulation used here is standard in CP-SAT modelling and goes back to the network-design literature.