Created
June 22, 2020 06:08
-
-
Save tarvaina/31238ceac823ca07967c287e170b822e to your computer and use it in GitHub Desktop.
Recreating Peter Norvig's Sudoku solver from memory
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
""" | |
I've always found Peter Norvig's Python solver for Sudoku at https://norvig.com/sudoku.html very elegant. | |
It had been a few years since I visited the page and I decided I would try to recreate it from memory. | |
I got pretty close! | |
The major difference is that my solution is missing one of the two propagation strategies. | |
Search works without it so I thought Norvig's solution might not use it. | |
Turns out I was wrong and it did have it. | |
Another difference is that Norvig's code is much better documented at the page https://norvig.com/sudoku.html . | |
So if you have trouble understanding the code below, be sure to visit the original. | |
""" | |
def _run_examples(): | |
# From https://en.wikipedia.org/wiki/Sudoku | |
problem_1 = """ | |
53_ _7_ ___ | |
6__ 195 ___ | |
_98 ___ _6_ | |
8__ _6_ __3 | |
4__ 8_3 __1 | |
7__ _2_ __6 | |
_6_ ___ 28_ | |
___ 419 __5 | |
___ _8_ _79 | |
""" | |
# From https://www.youtube.com/watch?v=EciHY-dxIIE | |
problem_2 = """ | |
_21 __4 98_ | |
4__ __2 _6_ | |
__5 _3_ 2__ | |
___ __7 ___ | |
_5_ _1_ _9_ | |
___ 2__ ___ | |
__4 _2_ 1__ | |
_9_ 8__ __7 | |
_16 9__ 83_ | |
""" | |
print(Sudoku.from_str(problem_1).solve()) | |
print() | |
print() | |
print(Sudoku.from_str(problem_2).solve()) | |
def _str_product(a_str, b_str): | |
return {a + b for a in a_str for b in b_str} | |
def _chunk(str, chunk_length): | |
return [str[i:i+chunk_length] for i in range(0, len(str), chunk_length)] | |
NUMBERS = set("123456789") | |
X_COORDS = "123456789" | |
Y_COORDS = "ABCDEFGHI" | |
ALL_COORDS = _str_product(Y_COORDS, X_COORDS) | |
ROWS = [_str_product(y, X_COORDS) for y in Y_COORDS] | |
COLUMNS = [_str_product(Y_COORDS, x) for x in X_COORDS] | |
BOXES = [ | |
_str_product(y_chunk, x_chunk) | |
for y_chunk in _chunk(Y_COORDS, 3) | |
for x_chunk in _chunk(X_COORDS, 3) | |
] | |
# Each cell belongs to one row, one column and one box. | |
SETS = { | |
coord: [set for set in (ROWS + COLUMNS + BOXES) if coord in set] | |
for coord in ALL_COORDS | |
} | |
class NoSolutionError(Exception): | |
def __init__(self, sudoku): | |
self.sudoku = sudoku | |
class Sudoku: | |
def __init__(self, copy=None): | |
if copy is None: | |
self._available = { | |
loc: NUMBERS.copy() | |
for loc in ALL_COORDS | |
} | |
else: | |
self._available = copy._available.copy() | |
def __getitem__(self, coord): | |
if len(self._available[coord]) == 1: | |
number, = self._available[coord] | |
return number | |
else: | |
return None | |
def __setitem__(self, coord, number): | |
self._constrain(coord, {number}) | |
def _constrain(self, coord, constraint): | |
old = self._available[coord] | |
new = old & constraint | |
self._available[coord] = new | |
if len(new) == 0: | |
raise NoSolutionError(self) | |
elif old != new and len(new) == 1: | |
for set in SETS[coord]: | |
for c in set - {coord}: | |
self._constrain(c, NUMBERS - new) | |
def solve(self): | |
for coord in ALL_COORDS: | |
if len(self._available[coord]) == 0: | |
raise NoSolutionError(self) | |
elif len(self._available[coord]) > 1: | |
return self._try_each_number(coord) | |
# For each coordinate only one choice, so this is the solution | |
return self | |
def _try_each_number(self, coord): | |
for number in self._available[coord]: | |
try: | |
sudoku = Sudoku(self) | |
sudoku[coord] = number | |
return sudoku.solve() | |
except NoSolutionError: | |
continue | |
raise NoSolutionError(self) | |
@classmethod | |
def from_str(cls, board_str): | |
sudoku = cls() | |
row_strs = [r.strip() | |
for r in board_str.replace(" ", "").strip().split("\n") | |
if r.strip() != ""] | |
assert [len(r) for r in row_strs] == [9] * 9, str(row_strs) | |
for y, row in zip(Y_COORDS, row_strs): | |
for x, number in zip(X_COORDS, row): | |
assert number in NUMBERS | set("_") | |
if number != "_": | |
sudoku[y+x] = number | |
return sudoku | |
def __str__(self): | |
return "\n\n".join( | |
"\n".join( | |
" ".join( | |
"".join( | |
self[y + x] or '_' for x in x_chunk | |
) for x_chunk in _chunk(X_COORDS, 3) | |
) for y in y_chunk | |
) for y_chunk in _chunk(Y_COORDS, 3) | |
) | |
if __name__ == "__main__": | |
_run_examples() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment