180 lines
4.9 KiB
Python
180 lines
4.9 KiB
Python
#!/usr/bin/python3
|
|
# -*- coding: utf-8 -*-
|
|
|
|
"""
|
|
:mod:`sudoku_solver` module
|
|
:author: `Éric W`
|
|
:date: 2016, october
|
|
|
|
:Provides:
|
|
|
|
* a function `solve` to solve sudoku grids
|
|
|
|
:Example: with the following sudoku grid
|
|
|
|
.. code-block:: text
|
|
|
|
+-------+-------+-------+
|
|
| 9 . 6 | . 2 8 | . . 3 |
|
|
| . 4 . | 5 . . | . . 1 |
|
|
| . 8 . | 9 . . | . 4 . |
|
|
+-------+-------+-------+
|
|
| 6 . . | . . . | . 7 . |
|
|
| . . 8 | 2 . 6 | 9 . . |
|
|
| . 3 . | . . . | . . 5 |
|
|
+-------+-------+-------+
|
|
| . 5 . | . . 3 | . 6 . |
|
|
| 1 . . | . . 2 | . 8 . |
|
|
| 2 . . | 8 7 . | 5 . . |
|
|
+-------+-------+-------+
|
|
|
|
and second with the grid
|
|
|
|
.. code-block:: text
|
|
|
|
+-------+-------+-------+
|
|
| 9 . 6 | . 2 8 | . . 3 |
|
|
| . 4 . | 5 . . | . . 1 |
|
|
| . 8 . | 9 . . | . 4 . |
|
|
+-------+-------+-------+
|
|
| 6 . . | . . . | . 7 . |
|
|
| . . 8 | 2 . 6 | 9 . . |
|
|
| . 3 . | . . . | . . 5 |
|
|
+-------+-------+-------+
|
|
| . 5 . | . . 3 | . 6 . |
|
|
| 1 . . | . . 2 | . 8 . |
|
|
| 2 . . | 8 7 . | . . . |
|
|
+-------+-------+-------+
|
|
|
|
>>> from sudoku_grid import SudokuGrid
|
|
>>> strgrid = '906028003040500001080900040600000070008206900030000005050003060100002080200870500'
|
|
>>> grid = SudokuGrid(strgrid)
|
|
>>> solved_grid = solve(grid)
|
|
>>> len(solved_grid)
|
|
1
|
|
>>> solved_grid[0].pretty_print()
|
|
+-------+-------+-------+
|
|
| 9 1 6 | 4 2 8 | 7 5 3 |
|
|
| 3 4 2 | 5 6 7 | 8 9 1 |
|
|
| 7 8 5 | 9 3 1 | 2 4 6 |
|
|
+-------+-------+-------+
|
|
| 6 2 9 | 3 4 5 | 1 7 8 |
|
|
| 5 7 8 | 2 1 6 | 9 3 4 |
|
|
| 4 3 1 | 7 8 9 | 6 2 5 |
|
|
+-------+-------+-------+
|
|
| 8 5 7 | 1 9 3 | 4 6 2 |
|
|
| 1 9 4 | 6 5 2 | 3 8 7 |
|
|
| 2 6 3 | 8 7 4 | 5 1 9 |
|
|
+-------+-------+-------+
|
|
"""
|
|
|
|
import time
|
|
from sudoku_grid import SudokuGrid, EMPTY_SYMBOL
|
|
|
|
def __possible_values(s):
|
|
"""
|
|
:param s:
|
|
:type s: str
|
|
:return: a substring of '123456789' containing no character in s
|
|
:rtype: str
|
|
"""
|
|
return "".join(c for c in '123456789' if c not in s)
|
|
|
|
|
|
def __grid_exam(grid):
|
|
"""
|
|
:param grid: a Sudoku grid
|
|
:type grid: SudokuGrid
|
|
:return: a tuple (s, coord) where
|
|
* coord is a tuple of coordinate of a case
|
|
* poss is a string containing value's candidates for the case
|
|
no other case has less possibilities
|
|
:rtype: tuple
|
|
"""
|
|
ROWS = [grid.get_row(row) for row in range(9)]
|
|
COLS = [grid.get_col(col) for col in range(9)]
|
|
SQUARES = [grid.get_square(3*c, 3*r) for r in range(3) for c in range(3)]
|
|
poss_min, coord_min = '0123456789', (-1, -1)
|
|
for row in range(9):
|
|
for col in range(9):
|
|
if grid.is_empty_cell(col, row):
|
|
poss = __possible_values(ROWS[row] + COLS[col] + SQUARES[3 * (row // 3) + (col // 3)])
|
|
if len(poss) < len(poss_min):
|
|
poss_min = poss
|
|
coord_min = (col, row)
|
|
return poss_min, coord_min
|
|
|
|
def __is_solved(grid):
|
|
"""
|
|
:param grid: a Sudoku grid
|
|
:typr grid: sudoku_grid.grid
|
|
:return: `True` if the grid is solved, `False` otherwise
|
|
:rtype: bool
|
|
:UC: none
|
|
"""
|
|
return not any(grid.is_empty_cell(col, row) for row in range(9) for col in range(9))
|
|
|
|
def solve(grid, out_channel=None, node=None):
|
|
"""
|
|
:param grid: a Sudoku grid to solve
|
|
:type grid: SudokuGrid
|
|
:return: list of solutions (solved grids)
|
|
:rtype: list
|
|
:UC: none
|
|
"""
|
|
if __is_solved(grid):
|
|
if not out_channel is None:
|
|
out_channel.write('\t"{:s}"[shape=hexagon, fillcolor="#00FF00"];\n'.format(node))
|
|
return [grid.copy()]
|
|
else:
|
|
maybe, (col, row) = __grid_exam(grid)
|
|
res = []
|
|
for val in maybe:
|
|
if not out_channel is None:
|
|
label = str((val, col, row))
|
|
next_node = node + '{:s}{:d}{:d}'.format(val, col, row)
|
|
out_channel.write('\t"{:s}"[label="{:s}"];\n'.format(next_node, label))
|
|
out_channel.write('\t"{:s}" -> "{:s}";\n'.format(node, next_node))
|
|
else:
|
|
next_node = None
|
|
grid[col, row] = val
|
|
res += solve(grid, out_channel, next_node)
|
|
grid[col, row] = EMPTY_SYMBOL
|
|
return res
|
|
|
|
def solve_one(grid, pause=0, out_channel=None):
|
|
"""
|
|
:param grid: a Sudoku grid to solve
|
|
:type grid: sudoku_grid.grid
|
|
:param pause: delay between two steps of resolution
|
|
:type pause: float
|
|
:return: a solution of grid
|
|
:rtype: sudoku_grid.grid
|
|
:UC: none
|
|
"""
|
|
time.sleep(pause)
|
|
print("\x1b[2J", end='')
|
|
grid.pretty_print()
|
|
if out_channel:
|
|
out_channel.write(str(grid)+'\n')
|
|
if __is_solved(grid):
|
|
return [grid]
|
|
else:
|
|
maybe, (col, row) = __grid_exam(grid)
|
|
res = None
|
|
for val in maybe:
|
|
grid[col, row] = val
|
|
res = solve_one(grid, pause, out_channel)
|
|
grid[col, row] = EMPTY_SYMBOL
|
|
if not res is None:
|
|
return res
|
|
return res
|
|
|
|
|
|
|
|
if __name__ == '__main__':
|
|
import doctest
|
|
doctest.testmod ()
|
|
|
|
|