Implementing a sudoku-solver in Python

Let's now explore my refactored implementation of the solver. I'm going to present the code to you in steps, as it is quite involved (also, I won't repeat the source name at the top of each snippet, until I move to another module):

# sudoku/algo/solver.py
import os
from itertools import zip_longest, chain
from time import time

def cross_product(v1, v2):
return [w1 + w2 for w1 in v1 for w2 in v2]

def chunk(iterable, n, fillvalue=None):
args = [iter(iterable)] * n
return zip_longest(*args, fillvalue=fillvalue)

We start with some imports, and then we define a couple of useful functions: cross_product and chunk. They do exactly what the names hint at. The first one returns the cross-product between two iterables, while the second one returns a list of chunks from iterable, each of which has n elements, and the last of which might be padded with a given fillvalue, should the length of iterable not be a multiple of n. Then we proceed to define a few structures, which will be used by the solver:

digits = '123456789'
rows = 'ABCDEFGHI'
cols = digits
squares = cross_product(rows, cols)
all_units = (
[cross_product(rows, c) for c in cols]
+ [cross_product(r, cols) for r in rows]
+ [cross_product(rs, cs)
for rs in chunk(rows, 3) for cs in chunk(cols, 3)]
)
units = dict(
(square, [unit for unit in all_units if square in unit])
for square in squares
)
peers = dict(
(square, set(chain(*units[square])) - set([square]))
for square in squares
)

Without going too much into detail, let's hover over these objects. squares is a list of all squares in the grid. Squares are represented by a string such as A3 or C7. Rows are numbered with letters, and columns with numbers, so A3 will indicate the square in the first row, and third column.

all_units is a list of all possible rows, columns, and blocks. Each of those elements is represented as a list of the squares that belong to the row/column/block. units is a more complex structure. It is a dictionary with 81 keys. Each key represents a square, and the corresponding value is a list with three elements in it: a row, a column, and a block. Of course, those are the row, column, and block that the square belongs to.

Finally, peers is a dictionary very similar to units, but the value of each key (which still represents a square), is a set containing all peers for that square. Peers are defined as all the squares belonging to the row, column, and block the square in the key belongs to. These structures will be used in the calculation of the solution, when attempting to solve a puzzle.

Before we take a look at the function that parses the input lines, let me give you an example of what an input puzzle looks like:

1..3.......75...3..3.4.8.2...47....9.........689....4..5..178.4.....2.75.......1.

The first nine characters represent the first row, then another nine for the second row, and so on. Empty squares are represented by dots:

def parse_puzzle(puzzle):
assert set(puzzle) <= set('.0123456789')
assert len(puzzle) == 81

grid = dict((square, digits) for square in squares)
for square, digit in zip(squares, puzzle):
if digit in digits and not place(grid, square, digit):
return False # Incongruent puzzle
return grid

def solve(puzzle):
grid = parse_puzzle(puzzle)
return search(grid)

This simple parse_puzzle function is used to parse an input puzzle. We do a little bit of sanity checking at the beginning, asserting that the input puzzle has to shrink into a set that is a subset of the set of all numbers plus a dot. Then we make sure we have 81 input characters, and finally we define grid, which initially is simply a dictionary with 81 keys, each of which is a square, all with the same value, which is a string of all possible digits. This is because a square in a completely empty grid has the potential to become any number from 1 to 9.
The for loop is definitely the most interesting part. We parse each of the 81 characters in the input puzzle, coupling them with the corresponding square in the grid, and we try to "place" them. I put that in double quotes because, as we'll see in a moment, the place function does much more than simply setting a given number in a given square. If we find that we cannot place a digit from the input puzzle, it means the input is invalid, and we return False. Otherwise, we're good to go and we return the grid.

parse_puzzle is used in the solve function, which simply parses the input puzzle, and unleashes search on it. What follows is therefore the heart of the algorithm:

def search(grid):
if not grid:
return False
if all(len(grid[square]) == 1 for square in squares):
return grid # Solved
values, square = min(
(len(grid[square]), square) for square in squares
if len(grid[square]) > 1
)
for digit in grid[square]:
result = search(place(grid.copy(), square, digit))
if result:
return result

This simple function first checks whether the grid is actually non-empty. Then it tries to see whether the grid is solved. A solved grid will have one value per square. If that is not the case, it loops through each square and finds the square with the minimum amount of candidates. If a square has a string value of only one digit, it means a number has been placed in that square. But if the value is more than one digit, then those are possible candidates, so we need to find the square with the minimum amount of candidates, and try them. Trying a square with "23" candidates is much better than trying one with "23589". In the first case, we have a 50% chance of getting the right value, while in the second one, we only have 20%. Choosing the square with the minimum amount of candidates therefore maximizes the chances for us to place good numbers in the grid.

Once the candidates have been found, we try them in order and if any of them results in being successful, we have solved the grid and we return. You might have noticed the use of the place function in the search too. So let's explore its code:

def place(grid, square, digit):
"""Eliminate all the other values (except digit) from
grid[square] and propagate.
Return grid, or False if a contradiction is detected.
"""
other_vals = grid[square].replace(digit, '')
if all(eliminate(grid, square, val) for val in other_vals):
return grid
return False

This function takes a work-in-progress grid, and tries to place a given digit in a given square. As I mentioned before, "placing" is not that straightforward. In fact, when we place a number, we have to propagate the consequences of that action throughout the grid. We do that by calling the eliminate function, which applies two strategies of the sudoku game:

  • If a square has only one possible value, eliminate that value from the square's peers
  • If a unit has only one place for a value, place the value there

Let me briefly offer an example of both points. For the first one, if you place, say, number 7 in a square, then you can eliminate 7 from the list of candidates for all the squares that belong to the row, column, and block that square belongs to.

For the second point, say you're examining the fourth row and, of all the squares that belong to it, only one of them has number 7 in its candidates. This means that number 7 can only go in that precise square, so you should go ahead and place it there.

The following function, eliminate, applies these two rules. Its code is quite involved, so instead of going line by line and offering an excruciating explanation, I have added some comments, and will leave you with the task of understanding it:

def eliminate(grid, square, digit):
"""Eliminate digit from grid[square]. Propagate when candidates
are <= 2.
Return grid, or False if a contradiction is detected.
"""
if digit not in grid[square]:
return grid # already eliminated
grid[square] = grid[square].replace(digit, '')

## (1) If a square is reduced to one value, eliminate value
## from peers.
if len(grid[square]) == 0:
return False # nothing left to place here, wrong solution
elif len(grid[square]) == 1:
value = grid[square]
if not all(
eliminate(grid, peer, value) for peer in peers[square]
):
return False

## (2) If a unit is reduced to only one place for a value,
## then put it there.
for unit in units[square]:
places = [sqr for sqr in unit if digit in grid[sqr]]
if len(places) == 0:
return False # No place for this value
elif len(places) == 1:
# digit can only be in one place in unit,
# assign it there
if not place(grid, places[0], digit):
return False
return grid

The rest of the functions in the module aren't important for the rest of this example, so I will skip them. You can run this module by itself; it will first perform a series of checks on its data structures, and then it will solve all the sudoku puzzles I have placed in the sudoku/puzzles folder. But that is not what we're interested in, right? We want to see how to solve sudoku using multiprocessing techniques, so let's get to it.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.149.26.246