Copyright GCHQ. See the GCHQ post for the full image.
As you may have seen, Britain’s intelligence organization GCHQ has released a Christmas card featuring a grid-shading puzzle.
The puzzle is an instance of a Nonogram puzzle. This is a grid with numbers by each row and column, indicating the lengths of runs of shaded cells in the completed puzzle.
Instead of solving the problem by hand, I opted to write an automatic solver using Python and some rusty CS knowledge. The same script can be adapted to solve other Nonogram puzzles.
In this post I’ll explain how my script works, with the disclaimer that I don’t claim to be an expert in CNF or SAT solvers. This approach may not even be more efficient than a basic backtracking algorithm. Nevertheless, I found it an interesting exercise and hopefully you will too. Any feedback from SAT aficionados would also be much appreciated!
Full source code is available here.
I decided to solve the problem by expressing it as a boolean formula in Conjunctive normal form (CNF) and feeding the result into a SAT solver. My reasons for doing this were as follows:
So what exactly is CNF, and what is a SAT solver? In short, CNF is a way of writing a boolean formula, eg:
\[A \wedge (B \vee \neg (D \vee E))\]as series of ANDs of ORs, where NOTs may only appear directly applied to variables:
\[A \wedge (B \vee \neg D) \wedge (B \vee \neg E)\]Each sequence of ORs is called a clause, and each element of the clause (ie. a possibly negated variable) is called a term. The whole CNF expression can therefore be seen as a sequence of clauses.
A SAT solver is a program which given a boolean formula in CNF, assigns truth values to the variables of the formula such that the formula is true. Each such assignment is a solution to the boolean satisfiability problem. Above,
\[A = True, B = True, \\ C = False, D = True\]is a solution, as is
\[A = True, B = False, \\ D = True, E = True\]for example.
\[A = False, B = True, \\ C = True, D = True\]is not a solution however.
In practice, CNF expressions have many thousands of terms. For example the
Sudoku solver example from the picosat
repository has 11,764 clauses, and
24,076 terms.
SAT solving algorithms have been the subject of intense competition over the past decade due to applications in AI, circuit design, and automatic theorem proving. As such we can leverage these advances just by expressing our problem as CNF.
So, we’re looking to map our puzzle into a CNF expression, with the idea that we’ll be able to read the truth assignments in any solution to determine which grid cells should be filled in. As such it would seem natural to introduce a variable \( shaded_{i,j} \)for each grid cell, which is true iff the corresponding cell should be filled in. In our case the grid is 25x25, so we’d have 625 such variables.
So what should the formula be? Let’s start off by writing our clauses out in English. In the below rules, a “row run” refers to a horizontal sequence of consecutive shaded cells, each of which corresponds with one of the numbers down the left hand side of the original puzzle. Similarly, a “column run” is a vertical sequence of shaded cells, corresponding with the numbers at the top of the puzzle. With that in mind, here are the rules:
With the above in mind, it seems intuitive to introduce new variables \( rowrunpos_{i,j,k} \) and \( colrunpos_{i,j,k} \) with the following semantics:
This means for each number around the edge of the puzzle we’ll have 25 new variables.
Introducing new variables helps constrain the size of the CNF while maintaining equivalence. See the Wikipedia page for CNF for an illustrative example.
The exact choice of where it is best to introduce variables is non-trivial and is beyond the scope of this post.
With our variables established, we can now more or less directly translate the above English clauses into CNF clauses. Here’s the code for rule #1:
This is encoding the expression:
\[\forall i \in rows \; \\ \forall j \in (runs\ in\ row\ i) \\ \; \forall k \in cols \; \\ \forall m \in (cols\ covered\ by\ run_{i,j}\ at\ k) \; : \\ \; (rowrunpos_{i,j,k} \implies shaded_{i,m})\]Which by expanding the implication is equivalent to:
\[\forall i \in rows \\ \forall j \in (runs\ in\ row\ i) \\ \forall k \in cols \\ \forall m \in (cols\ covered\ by\ run_{i,j}\ at\ k) : \\ (\neg rowrunpos_{i,j,k} \vee shaded_{i,m})\]Here’s the first couple of clauses that the above function returns, with annotations:
The first of these clauses says “if the third run in the second row starts at column 8, then cell (1, 8) must be shaded”. There’s one clause for each cell in each possible position of each row run.
Some things to note about CNF expressions expected by pycosat
:
In the above code, row_run_vars[row, run_idx, start_col]
and
shaded_vars[row, col]
correspond with the variables \( rowrunpos_{i,j,k} \)
and \( shaded_{i,k} \) respectively. Each variable is represented by a Var
object (created by me) which exists to associate useful debug information
(Var.__str__
) with the opaque variable index (Var.idx
).
ROW_RUNS
encodes the numbers down the left hand side of the puzzle as a list
of lists.
The remaining clauses follow a similar pattern of translation. See the source code for the full details. The resulting CNF expression has 307,703 clauses, and 637,142 terms.
Having pycosat
solve the problem is simply a case of calling
pycosat.solve()
:
The result encodes a single truth assignment:
It’s then just a case of mapping the integers back to variables, and displaying in grid form.
And here is the result:
…a QR code which when decoded links you to the next stage in the puzzle.