#+BEGIN_abstract If you have a taste for NP-completeness, Sudoku, or literate programming, then this one's for you. #+END_abstract #+TITLE: Making and Slaying Monster Sudoku #+DATE: 2020-05-05 #+FILETAGS: sudoku:np-complete:backtracking:search #+PROPERTY: header-args :noweb no-export :noweb-sep "\n" :session :eval no-export :noweb-sep "\n\n\n" :mkdirp yes :comments link /All of the code described in this article is available [[https://github.com/reindeereffect/reindeereffect.github.io/tree/master/2020/05/05][here]]./ * 0xdeadbeef :noexport: ** todo - ** code #+NAME: install.sh #+BEGIN_SRC shell :exports none :results none :tangle install.sh :shebang "#! /bin/bash" ./setup.py sdist virtualenv -p `which python3` $HOME/test . $HOME/test/bin/activate pip install dist/sudoku* mkdir -p images #+END_SRC #+NAME: sdtx #+BEGIN_SRC shell :exports none :results output export PATH=$HOME/test/bin:$PATH function sudoset() { out=images/$1; shift sudoku2img -- $@ > $out echo -n $out } #+END_SRC #+RESULTS: sdtx * Introduction Quarantine is in full swing, and after watching a disturbing amount of TV, you plowed through a book of Sudoku puzzles that turned up in the basement. Now they're gone, and you're looking for something more. Bigger. Monstrous, you might say. Unfortunately, the giant Sudoku puzzles (think 16\times16 or 25\times25) are not quite so easy to find in any quantity. It turns out, though, that with a little effort, you can make machines churn them out while you sleep. In this article we will - consider the structure of the Sudoku board; - use that structure to capture, in a general way, common heuristics for solving Sudoku puzzles; - employ both constraint propagation and backtracking to create a suitably fast Sudoku solver; and - use that solver as building block for generating Sudoku puzzles of any size we might desire. By the end, we'll have implemented a library for dealing with Sudoku, as well as a small collection of command line tools for generating, solving, and formatting puzzles. * The Board Before endeavoring to operate on Sudoku boards, we should first pin down some useful representations, as well as conversions between them. ** Board Divisions The classic Sudoku board consists of a 9\times9 grid of cells, with a 3\times3 grid of boxes, each containing a 3\times3 grid of cells, overlaid: #+BEGIN_SRC shell :results file :exports results <> seq 0 80 | sudoset cells.png #+END_SRC #+RESULTS: [[file:images/cells.png]] where the cells have been numbered in row-major order. Each cell is a member of a set of three divisions, namely - rows: #+BEGIN_SRC shell :results file :exports results <> for i in {0..8}; do for j in {0..8}; do echo $i; done done | sudoset row-divs.png #+END_SRC #+RESULTS: [[file:images/row-divs.png]] - columns: #+BEGIN_SRC shell :results file :exports results <> for i in {0..8}; do for j in {0..8}; do echo $j; done done | sudoset col-divs.png #+END_SRC #+RESULTS: [[file:images/col-divs.png]] - boxes: #+BEGIN_SRC shell :results file :exports results <> for i in {1..9}; do for j in {1..9}; do echo -n "$(( ($i-1)/3 * 3 + ($j-1)/3 )) " done echo done | sudoset box-divs.png #+END_SRC #+RESULTS: [[file:images/box-divs.png]] Of these, the most interesting division of the board is into boxes. The board board can be viewed as a 3\times3 grid of boxes, each a 3\times3 grid of cells, resulting in a grid of 3^4 cells arranged into 3^2 rows and 3^2 columns, for a grand total of 3\times3^2 distinct divisions. When the board is complete, each cell will contain a number from 1 to 9 (or 3^2) inclusive. Every interesting dimension is describable in terms of powers of 3, which we can think of as the /order/ of a standard board. We can now think of Sudoku (or Sudoku-like) boards a bit more generally. Given a board of order $\omega$, for any cell $c\in [1, \omega^4]$, we can find the row number $I$ as $$I(c) = \left\lfloor\frac{c}{\omega^2}\right\rfloor$$ the column number $J$ as $$J(c) = c\bmod \omega^2$$ and the box number $B$ as $$B(c) = \omega\times\left\lfloor\frac{I(c)}{\omega}\right\rfloor + \left\lfloor\frac{J(c)}{\omega}\right\rfloor.$$ This generalization allows us to handle larger boards with ease. For simplicity, the discussion and examples below will center on conventional order 3 boards unless otherwise specified; even so, the principles remain the same for other orders. If we sequentially number divisions like | division | start | end | |----------+-------------+-----------------| | rows | 0 | $\omega^2 - 1$ | | columns | $\omega^2$ | $2\omega^2 - 1$ | | boxes | $2\omega^2$ | $3\omega^2 - 1$ | then we can write a function to compute a mapping from cells to divisions, as well as an inverse mapping from divisions to cells: #+NAME: functions #+BEGIN_SRC python :results none def board_divs(order): ''' generates a dictionary (cell2divs) mapping cells to their various divisions in boards of the given order. Also generates a complementary mapping, div2cells. Returns (cell2divs, div2cells). ''' n = order**2 box = lambda i, j: i//order * order + j//order cell2divs = dict(enumerate({i, n + j, 2*n + box(i, j)} for i in range(n) for j in range(n))) return cell2divs, transpose(cell2divs) #+END_SRC where #+NAME: functions #+BEGIN_SRC python :results none def transpose(m): ''' given a binary matrix represented as a dictionary whose values are sets, and where a 1 at (i,j) is indicated by j in m[i] return the transpose of m. ''' t = {} for i, js in m.items(): for j in js: t.setdefault(j, set()).add(i) return t #+END_SRC Besides allowing more concise expression of algorithms operating on Sudoku boards, thinking in terms of cells and divisions opens the door to adapting some of what we develop here to Sudoku variants featuring irregularly-shaped divisions (like [[http://www.dailysudoku.com/sudoku/archive.shtml?type=squiggly][squiggly Sudoku]]). ** Logical Representation We'll need a convenient representation of the board state at any given time, as well as a ways to sensibly change that state. For that, we'll define a simple class: #+NAME: data types #+BEGIN_SRC python :results none class board: 'Utility class for representing and tracking board state.' <> <> <> #+END_SRC Each cell is either known or unknown. For the known cells, we need only track their values. For the unknown cells, however, we need to either track or compute the values that they may possibly take. Since the requirements for the two cell classes are different, we handle them separately. #+NAME: board initialization #+BEGIN_SRC python :results none def __init__(self, known, unknown, cell2divs, div2cells): ''' known dictionary mapping known cells to their respective values unknown dictionary mapping unknown cells to sets of possible values cell2divs, div2cells complementary mappings describing the board structure, such as those produced by board_divs ''' assert not set(known) & set(unknown) self.known = known self.unknown = unknown self.cell2divs = cell2divs self.div2cells = div2cells #+END_SRC Solving a Sudoku involves repeatedly /marking/ the board until no empty cells remain, subject to the constraint that each division contains one each of the numbers from 1 to 9 inclusive. With each marking, we assert knowledge about a previously unknown cell, and the possible values that can be taken by unknown cells sharing a division become more constrained. To track this, #+NAME: cell marking #+BEGIN_SRC python :results none def mark(self, cell, val): 'set cell to val, updating unknowns as necessary' self.known[cell] = val self.unknown.pop(cell, None) for div in self.cell2divs[cell]: for cell2 in self.div2cells[div]: self.elim(cell2, val) def elim(self, cell, val): "remove val from cell's possibilities" self.unknown.get(cell, set()).discard(val) #+END_SRC This is the basic mechanism of /constraint propagation/ that ultimately allows us to develop usefully fast solution techniques. For brevity, whenever we speak of marking a cell, we'll assume that the possibilities for other cells are updated as necessary, too. Sometimes we may not know that a given marking will work out---perhaps we're guessing---so we should support marking cells speculatively and recovering when we realize how wrong we are. The simplest method is to mark a copy of the current board state: #+NAME: cell marking #+BEGIN_SRC python :results none def marked(self, cell, val): 'returns a new board, with cell marked as val and possibilities eliminated' new = self.copy() new.mark(cell, val) return new #+END_SRC #+NAME: imports #+BEGIN_SRC python :results none import copy #+END_SRC #+NAME: copying #+BEGIN_SRC python :results none def copy(self): 'copies board' return self.__class__(copy.deepcopy(self.known), copy.deepcopy(self.unknown), self.cell2divs, self.div2cells) #+END_SRC ** Textual Representation Humans hardly want to look at Python dictionaries when there are better representations available, so let's work out a textual representation for our boards, and let's make it flexible enough to handle boards of any order. *** Converting from Strings We'll impose the following requirements on strings that represent Sudoku boards of any order $\omega$: - Each cell will be represented by an integer (if known) or a '.' (if unknown). - The number of cells must be $\omega^4$, where $\omega$ is some integer. - Cells can be separated by any other character. - Values for known cells must be in $[1, \omega^2]$. These rules will allow us to handle #+BEGIN_EXAMPLE 1 3 | . . . . | 3 1 ----+---- 3 1 | . . . 2 | 1 3 #+END_EXAMPLE as easily as #+BEGIN_EXAMPLE 1 3 . . . . 3 1 3 1 . . . 2 1 3 #+END_EXAMPLE or #+BEGIN_EXAMPLE 1 3 . . . . 3 1 3 1 . . . 2 1 3 #+END_EXAMPLE They also allow us to compute the order directly from the number of cells. #+NAME: functions #+BEGIN_SRC python :results none def load_board(s, validate_vals=True): ''' given a string representing a board, returns a board object. For a board of a given order: - Order is computed as the fourth root of board length, and it must be an integer. - Each cell must be represented by an integer in [1, order**2] inclusive, or `.' to denote unknown cells. This check can be disabled by setting validate_vals to False. - Cells must be separated from each other by any sequences of characters in /[^0-9.]+/. On failure, raises ValueError. ''' vals = [cell for cell in ''.join(c if c in '0123456789.' else ' ' for c in s).strip().split() if cell.isdigit() or cell == '.'] order = int(len(vals) ** 0.25) n = order**2 if len(vals) != order**4: raise ValueError bd = blank(order) for (cell, val_) in enumerate(vals): if val_ == '.': continue val = int(val_) if validate_vals and (val < 1 or val > n): raise ValueError bd.mark(cell, val) return bd #+END_SRC where #+NAME: functions #+BEGIN_SRC python :results none def blank(order): 'generate a blank board' n = order**2 possible_vals = set(range(1, n + 1)) return board({}, {i:set(possible_vals) for i in range(n**2)}, ,*board_divs(order)) #+END_SRC It would also be good know whether a board brought in from the outside world is indeed valid, in the sense of having no conflicting cell values in any division. #+NAME: functions #+BEGIN_SRC python :results none def isvalid(bd): ''' returns True if - no known cells' values conflict - no unknown cell's possibilities conflict with any known cell's value ''' return not any(val0 in {bd.known.get(cell)} | bd.unknown.get(cell, set()) for (cell0, val0) in bd.known.items() for cell in neighbors(bd, cell0) if cell in bd.known and cell != cell0) def neighbors(bd, cell0): return union(bd.div2cells[div] for div in bd.cell2divs[cell0]) def union(xss): return {x for xs in xss for x in xs} #+END_SRC *** Converting to Strings Once we've solved a puzzle or otherwise modified a board, we'd like to get a readable representation back out. Given that there are further use cases for a completed Sudoku board, like deriving Sudoku puzzles of varying difficulty, it should be loadable via =load_board=, like: #+BEGIN_EXAMPLE 8 3 7 | 1 2 6 | 9 5 4 9 5 4 | 3 8 7 | 1 6 2 2 1 6 | 4 5 9 | 3 7 8 ------+-------+------ 7 . 9 | . 4 5 | 8 1 3 3 4 5 | 9 1 8 | 6 2 7 1 . 8 | . 7 3 | 4 9 5 ------+-------+------ 4 8 1 | 5 6 2 | 7 . 9 5 9 3 | 7 . 1 | 2 8 6 6 7 2 | 8 9 4 | 5 3 1 #+END_EXAMPLE #+NAME: functions #+BEGIN_SRC python :results none def dump_board(bd): 'returns a "pretty printed" string representation of board bd' order = int((len(bd.known) + len(bd.unknown)) ** 0.25) n = order**2 svals = [str(bd.known[i] if i in bd.known else '.') for i in range(n**2)] width = max(map(len, svals)) fmt = lambda cell: ('%%%ds' % width) % cell n_x_n = [svals[i*n : i*n + n] for i in range(n)] cols_grpd = [' | '.join(' '.join(map(fmt, row[j*order : j*order + order])) for j in range(order)) for row in n_x_n] rows_grpd = ['\n'.join(cols_grpd[i*order : i*order + order]) for i in range(order)] rule = '\n' + ''.join('+' if c == '|' else '-' for c in cols_grpd[0]) + '\n' return rule.join(rows_grpd) #+END_SRC * Solving Sudoku Having a suitable representation of the board state, we can now work out how to solve a Sudoku puzzle. All of the techniques discussed here rely on the constraint propagation that [[cell marking][=board.mark=]] performs automatically. ** Deductive Techniques Consider how a human might approach a grid like #+BEGIN_SRC shell :results file :exports results <> sudoset ex-1-1.png <> sudoset ex-1-2.png -p 43 <> sudoset ex-1-4.png -p 42 <> sudoset ex-1-5.png -p 10 53 <