I originally found this puzzle in <ahref="https://www.zeit.de/zeit-magazin/2022/32/logelei">an edition of the ZEIT</a>, a weekly German newspaper.
See the previous link for a more difficult puzzle. It is solvable without computer assistance, but quite tricky!
</p>
<h3>Prerequisites</h3>
To follow along, you'll first need to install <ahref="https://www.python.org/">Python</a>.
Then, you need to install the <ahref="https://pypi.org/project/z3-solver/">Z3 Python bindings</a>.
The precise installation steps vary by operating system, check the official documentation for more details.
<h3>Defining the board</h3>
In the following, I will describe the code required to explain the puzzle to Z3.
First, we need to import the z3 Python module. To avoid prefixing every constructor with <spanclass="monospace">z3.</span>, we import all symbols into the local namespace. We also import the math module (we will need it later).
Problems are specified by creating a bunch of variables (booleans, integers, ...) and constraints on these variables.
Z3 will then produce a <i>model</i> that assigns a value to each variable if the constraints are satisfiable.
Since we are interested in the values of the grid cells, we create an integer variable for each cell.
<ahref="https://z3prover.github.io/api/html/z3.z3.html#-FreshInt">FreshInt</a> returns a new integer variable that isn't identical to any other previously created variable.
Empty grid cells will be indicated by a value of zero. All other cells need to be filled with a number between one and five.
To implement this constraint, we iterate over each row and cell of the grid and add the constraint (integer value must be at least 0 and at most 5) to the solver.
For each row/column, the numbers used must be unique.
This is modeled by pairwise inequality of the variables making up that row or column.
Of course, a value of zero (= empty grid cell) may appear more than once.
We define a function <spanclass="monospace">distinct_if_nonzero</span> that, given a list of variables, adds constraints to ensure that any two variables are not equal if both of them are nonzero.
This is done by supplying three parameters to the <spanclass="monospace">If</span> function: the condition, the constraint if the condition is true, the constraint if the condition is false.
Connecting these sums to the cell variables is done by considering each binary pattern <spanclass="monospace">p</span> of numbered / not numbered cells.
The same is done for the columns of the grid. For brevity, that code is omitted from this post. The full source code is linked at the end of the article.
<h3>Finding and displaying a solution</h3>
We still need to encode the off-by-one sums given in the puzzle as constraints.
Note that we used <spanclass="monospace">s.push()</span> to create a nested constraint context.
Any constraints added after this call may be removed by calling <spanclass="monospace">s.pop()</span>.
We will make use of this later to generate new puzzles (with new off-by-one sums).
<p>
Now that we have specified all of our contraints, we may ask Z3 to determine the validity of our model.
<spanclass="monospace">s.check()</span> may return <spanclass="monospace">sat</span>, <spanclass="monospace">unsat</span> or <spanclass="monospace">unknown</span>.
<spanclass="monospace">sat</span> indicates that the problem is <i>satisfiable</i> (there is a variable assignment that fulfills all constraints), <spanclass="monospace">unsat</span> indicates that no such assignment is possible.
Z3 only returns <spanclass="monospace">unknown</span> if it is not able to deduce whether the problem is satisfiable or not (e.g., due to incomplete theories).
</p>
<p>
The loop below uses a simple trick to enumerate all solutions to the puzzle: after one solution is found, add a constraint that at least one grid cell must be different.