Gareth Rees , 2016-06-04
The story so far: I wrote a solver for a generic exact cover problem in python , and used it to find polyomino tilings andSudoku solutions. Now I’m going to look at a couple of problems that don’t translate so easily into exact cover instances.
1. The eight queens problem
The eight queens problem is to place eight queens on a chessboard so that no queen is attacked by any other. (That is, so that there is at most one queen in each row, column, and diagonal.) The problem is well-known to computer programmers because it was used as the main example in Dijkstra’s influential ‘ Notes on Structured Programming ’ (1970), where he shows how to solve it using a backtracking algorithm, developing the program through a series of stepwise refinements.
Instead of using structured programming, let’s solve it by translating it to an exact cover instance. As in the previous examples, I can specify an encoding of constraints and choices as Python objects:
Constraint Python representation Queen on the \(i\)th row. The tuple ('row', i) Queen on the \(j\)th column. The tuple ('column', j) Queen on the \(k\)th leading diagonal. The tuple ('leading', k) Queen on the \(k\)th trailing diagonal. The tuple ('trailing', k) Choice Python representation Place a queen at \((i, j)\). The tuple ('queen', i, j)Here’s a function that encodes the \(n\)-queens problem as an exact cover instance using these choices and constraints:
from exactcover import ExactCover from itertools import product def queens(n=8, random=False): """Return an iterator that yields the solutions to the n-queens problem. Arguments: n -- The size of the problem. (Default: 8) random -- Generate solutions in random order? (Default: False.) """ constraints = { ('queen', i, j): (('row', i), ('column', j), ('leading', i - j + n - 1), ('trailing', i + j)) for i, j in product(range(n), repeat=2) } return ExactCover(constraints, random=random)But this does not work!
>>> next(queens()) Traceback (most recent call last): File " ", line 1, in File "exactcover.py", line 70, in __next__ return next(self.iter) StopIterationWhy is that? Tracing the execution of the exact cover solver, the first thing that happens is that it decides to satisfy the constraint ('leading', 0) . The only choice that satisfies this constraint is ('queen', 0, 7) . This choice also satisfies the constraint ('row', 0) , which causes the choice ('queen', 0, 0) to be eliminated (since it would be in the same row). But now there are no choices that satisfy the constraint ('trailing', 0) and further progress is impossible.
In an exact cover problem, all the constraints must be satisfied. But in this problem there aren’t enough queens to cover all the diagonals: on an 8×8 board there are 15 leading diagonals and 8 queens, but each queen can only cover one leading diagonal. So 7 leading diagonal constraints must go unsatisfied (and similarly for trailing diagonals).
I can get around this difficulty by adding additional choices whose only purpose is to ensure that all the constraints can be satisfied:
Choice Python representation Cover the \(k\)th leading diagonal. The tuple ('leading', k) Cover the \(k\)th trailing diagonal. The tuple ('trailing', k)The idea is that after placing the eight queens, the remaining 14 diagonal constraints can be satisfied using 14 of these extra choices. They will appear in the solution, but they are redundant and can be ignored. Artificial devices like these that help to translate one problem into another are called ‘ gadgets ’. With these gadgets in hand, it’s easy to construct the exact cover instances:
from exactcover import ExactCover from itertools import product def queens(n=8, random=False): """Return an iterator that yields the solutions to the n-queens problem. Arguments: n -- The size of the problem. (Default: 8) random -- Generate solutions in random order? (Default: False.) """ def constraints(): for i, j in product(range(n), repeat=2): yield ('queen', i, j), (('row', i), ('column', j), ('leading', i - j + n - 1), ('trailing', i + j)) for k in range(2 * n - 1): yield ('leading', k), (('leading', k),) yield ('trailing', k), (('trailing', k),) return ExactCover(dict(constraints()), random=random)Here’s a simple decoder for the solutions. Note that the decoder ignores the gadgets (the choices representing unattacked diagonals) and only looks at the positions of the queeens.
def decode_queens(n, solution): grid = [['+'] * n for _ in range(n)] for choice in solution: if choice[0] == 'queen': _, i, j = choice[:3] grid[i][j] = '' return '\n'.join(map(''.join, grid))Here’s the solver in operation:
>>> print(decode_queens(12, next(queens(12)))) +++++++++++ +++++++++++ +++++++++++ +++++++++++ +++++++++++ +++++++++++ +++++++++++ +++++++++++ +++++++++++ +++++++++++ +++++++++++ +++++++++++The solver can count the solutions for small board sizes (sequence A000170 in the OEIS ):
>>> [sum(1 for _ in queens(n)) for n in range(1, 11)] [1, 0, 0, 2, 10, 4, 40, 92, 352, 724]Here’s code for drawing solutions as SVG images, using the svgwrite package:
from svgwrite import Drawing def solutions_svg(n, solutions, filename, columns=10, size=20, margin=10, fg_colour='black', bg_colour='#eee'): """Format n-queens solutions as an SVG image. Required arguments: n -- size of board. solutions -- iterable of solutions to the n-queens problem, each of which is a sequence of choices, some of which are tuples ('queen', i, j) giving the locations of the queens. filename -- where to save the SVG drawing. Optional arguments: columns -- number of solutions per row (default: 1). size -- width and height of each board square (default: 20). margin -- margin between boards (default: 10). fg_colour --