Added Python sudoku solution.
authorW. Trevor King <wking@drexel.edu>
Thu, 7 Oct 2010 11:38:35 +0000 (07:38 -0400)
committerW. Trevor King <wking@drexel.edu>
Thu, 7 Oct 2010 11:38:35 +0000 (07:38 -0400)
assignments/archive/sudoku/1.cfg
assignments/archive/sudoku/2.cfg
assignments/archive/sudoku/3.cfg
assignments/archive/sudoku/soln/sudoku.py [new file with mode: 0755]

index 2fd0850fd1eb3726dffe45257312c4d95cffcb3a..abdfa45e050de28638d18ea8c3346d9282a7c594 100644 (file)
@@ -1,20 +1,11 @@
+- - -  - 2 8  - 5 -
+- 7 4  6 1 -  2 8 -
+- - 2  - 7 -  9 - 1
 
- - - -  - 2 8  - 5 -
+- 8 -  - - -  3 9 5
+- - 5  7 - 6  1 - -
+3 4 1  - - -  - 2 -
 
- - 7 4  6 1 -  2 8 -
-
- - - 2  - 7 -  9 - 1
-
-
- - 8 -  - - -  3 9 5
-
- - - 5  7 - 6  1 - -
-
- 3 4 1  - - -  - 2 -
-
-
- 4 - 9  - 8 -  5 - -
-
- - 2 8  - 5 3  6 7 -
-
- - 5 -  4 6 -  - - -
+4 - 9  - 8 -  5 - -
+- 2 8  - 5 3  6 7 -
+- 5 -  4 6 -  - - -
index 714a72abef82e9b364343f78b1199720e65c58cf..69637bd34ef28cb02c3f9be46be7ecce09158b32 100644 (file)
@@ -1,20 +1,11 @@
+- 6 -  - 3 -  - - -
+- 3 1  9 - 8  - 5 -
+2 7 -  - - -  6 3 -
 
- - 6 -  - 3 -  - - -
+- - -  - 9 1  - - 3
+- 1 -  - 2 -  - 9 -
+9 - -  6 8 -  - - -
 
- - 3 1  9 - 8  - 5 -
-
- 2 7 -  - - -  6 3 -
-
-
- - - -  - 9 1  - - 3
-
- - 1 -  - 2 -  - 9 -
-
- 9 - -  6 8 -  - - -
-
-
- - 9 6  - - -  - 4 7
-
- - 4 -  2 - 9  5 6 -
-
- - - -  - 1 -  - 8 -
+- 9 6  - - -  - 4 7
+- 4 -  2 - 9  5 6 -
+- - -  - 1 -  - 8 -
index 2e217248cbbd0187ddde045c4b1161e5200450b9..6e0b048c17099c7b5a5a76fe7ec9d3ca5eb63116 100644 (file)
@@ -1,20 +1,11 @@
+- 2 -  - - -  6 - 9
+3 - -  9 - 4  - 5 -
+- - -  8 1 -  - - -
 
- - 2 -  - - -  6 - 9
+6 - -  5 - -  - 1 -
+8 1 -  3 - 9  - 6 5
+- 4 -  - - 1  - - 3
 
- 3 - -  9 - 4  - 5 -
-
- - - -  8 1 -  - - -
-
-
- 6 - -  5 - -  - 1 -
-
- 8 1 -  3 - 9  - 6 5
-
- - 4 -  - - 1  - - 3
-
-
- - - -  - 7 6  - - -
-
- - 6 -  4 - 5  - - 8
-
- 9 - 1  - - -  - 7 -
+- - -  - 7 6  - - -
+- 6 -  4 - 5  - - 8
+9 - 1  - - -  - 7 -
diff --git a/assignments/archive/sudoku/soln/sudoku.py b/assignments/archive/sudoku/soln/sudoku.py
new file mode 100755 (executable)
index 0000000..126664f
--- /dev/null
@@ -0,0 +1,265 @@
+#!/usr/bin/env python
+
+import numpy
+
+
+class Sudoku (object):
+    r"""
+    >>> puzzle = '\n'.join([
+    ...     '5 3 -   - 7 -   - - -',
+    ...     '6 - -   1 9 5   - - -',
+    ...     '- 9 8   - - -   - 6 -',
+    ...     '',
+    ...     '8 - -   - 6 -   - - 3',
+    ...     '4 - -   8 - 3   - - 1',
+    ...     '7 - -   - 2 -   - - 6',
+    ...     '',
+    ...     '- 6 -   - - -   2 8 -',
+    ...     '- - -   4 1 9   - - 5',
+    ...     '- - -   - 8 -   - 7 9',
+    ...     ])
+    >>> s = Sudoku()
+    >>> s.load(puzzle)
+    >>> s._num_solved()
+    30
+    >>> s.solve()
+    >>> print s.status
+    solved in 408 steps
+    >>> print s.dump()
+    5 3 4   6 7 8   9 1 2
+    6 7 2   1 9 5   3 4 8
+    1 9 8   3 4 2   5 6 7
+    <BLANKLINE>
+    8 5 9   7 6 1   4 2 3
+    4 2 6   8 5 3   7 9 1
+    7 1 3   9 2 4   8 5 6
+    <BLANKLINE>
+    9 6 1   5 3 7   2 8 4
+    2 8 7   4 1 9   6 3 5
+    3 4 5   2 8 6   1 7 9
+    >>> s._num_solved()
+    81
+
+    >>> s._row(0)
+    array([5, 3, 4, 6, 7, 8, 9, 1, 2])
+    >>> s._col(0)
+    array([5, 6, 1, 8, 4, 7, 9, 2, 3])
+    >>> s._cell(0,0)
+    array([[5, 3, 4],
+           [6, 7, 2],
+           [1, 9, 8]])
+    """
+    def __init__(self):
+        self._puzzle = numpy.zeros((9,9), dtype=numpy.int)
+        self._empty = 0
+        self._external_empty = '-'
+        self.status = None
+
+    def load(self, text):
+        row = 0
+        for line in text.splitlines():
+            if len(line) == 0 or line.startswith('#'):
+                continue
+            assert row < 9, row
+            for col,value in enumerate(line.split()):
+                assert col < 9, col
+                self._puzzle[row,col] = self._convert_to_internal_value(value)
+            row += 1
+        self.status = 'loaded puzzle'
+
+    def _convert_to_internal_value(self, value):
+        """
+        >>> s = Sudoku()
+        >>> s._convert_to_internal_value(s._external_empty)
+        0
+        >>> s._convert_to_internal_value('0')
+        Traceback (most recent call last):
+          ...
+        AssertionError: 0
+        >>> s._convert_to_internal_value('1')
+        1
+        >>> s._convert_to_internal_value('9')
+        9
+        >>> s._convert_to_internal_value('10')
+        Traceback (most recent call last):
+          ...
+        AssertionError: 10
+        """
+        if value == self._external_empty:
+            value = self._empty
+        else:
+            value = int(value)
+            assert value >= 1 and value <= 9, value
+        return value
+
+    def dump(self):
+        lines = []
+        for row in range(9):
+            if row in [3, 6]:
+                lines.append('')  # blank rows between cells
+            line = []
+            for col in range(9):
+                if col in [3, 6]:
+                    line.append(' ')  # blank columns between cells
+                line.append(self._convert_to_external_value(
+                        self._puzzle[row,col]))
+            lines.append(' '.join(line))
+        return '\n'.join(lines)
+
+    def _convert_to_external_value(self, value):
+        """
+        >>> s = Sudoku()
+        >>> s._convert_to_external_value(s._empty)
+        '-'
+        >>> s._convert_to_external_value(1)
+        '1'
+        >>> s._convert_to_external_value(9)
+        '9'
+        """
+        if value == self._empty:
+            value = self._external_empty
+        else:
+            assert value >= 1 and value <= 9, value
+            value = str(value)
+        return value
+
+    def _row(self, row):
+        return self._puzzle[row,:]
+
+    def _col(self, col):
+        return self._puzzle[:,col]
+
+    def _cell(self, cell_row, cell_col):
+        ri = cell_row * 3
+        rf = ri + 3
+        ci = cell_col * 3
+        cf = ci + 3
+        return self._puzzle[ri:rf,ci:cf]
+
+    def _nonempty(self, values):
+        return [x for x in values if x != self._empty]
+
+    def _is_valid(self):
+        r"""
+        >>> puzzle = '\n'.join([
+        ...     '5 3 -   - 7 -   - - -',
+        ...     '6 - -   1 9 5   - - -',
+        ...     '- 9 8   - - -   - 6 -',
+        ...     '',
+        ...     '8 - -   - 6 -   - - 3',
+        ...     '4 - -   8 - 3   - - 1',
+        ...     '7 - -   - 2 -   - - 6',
+        ...     '',
+        ...     '- 6 -   - - -   2 8 -',
+        ...     '- - -   4 1 9   - - 5',
+        ...     '- - -   - 8 -   - 7 9',
+        ...     ])
+        >>> s = Sudoku()
+        >>> s.load(puzzle)
+        >>> s._is_valid()
+        True
+
+        Test an invalid row.
+
+        >>> s._puzzle[0,3] = 5
+        >>> s._is_valid_row(0)
+        False
+        >>> s._is_valid()
+        False
+        >>> s._puzzle[0,3] = s._empty
+
+        Test an invalid column.
+
+        >>> s._puzzle[8,0] = 5
+        >>> s._is_valid_col(0)
+        False
+        >>> s._is_valid()
+        False
+        >>> s._puzzle[8,0] = s._empty
+
+        Test and invalid cell.
+
+        >>> s._puzzle[2,0] = 3
+        >>> s._is_valid_cell(0, 0)
+        False
+        >>> s._is_valid()
+        False
+        >>> s._puzzle[2,0] = s._empty
+        """
+        for row in range(9):
+            if not self._is_valid_row(row):
+                return False
+        for col in range(9):
+            if not self._is_valid_col(col):
+                return False
+        for cell_row in range(3):
+            for cell_col in range(3):
+                if not self._is_valid_cell(cell_row, cell_col):
+                    return False
+        return True
+
+    def _is_valid_row(self, row):
+        values = self._nonempty(self._row(row))
+        return len(values) == len(set(values))
+
+    def _is_valid_col(self, col):
+        values = self._nonempty(self._col(col))
+        return len(values) == len(set(values))
+
+    def _is_valid_cell(self, cell_row, cell_col):
+        values = self._nonempty(self._cell(cell_row, cell_col).flatten())
+        return len(values) == len(set(values))
+
+    def _num_solved(self):
+        return len(self._nonempty(self._puzzle.flatten()))
+
+    def solve(self):
+        trials = numpy.zeros((9,9,9), dtype=numpy.int)
+        for row in range(9):
+            for col in range(9):
+                if self._puzzle[row,col] == self._empty:
+                    trials[row,col,:] = range(1,10)
+                else:
+                    x = self._puzzle[row,col]
+                    trials[row,col,x-1] = x
+
+        actions = 0
+        while True:
+            start_actions = 0
+            for row in range(9):
+                for col in range(9):
+                    if self._puzzle[row][col] == self._empty:
+                        for x in self._nonempty(trials[row,col,:]):
+                            self._puzzle[row,col] = x
+                            if not self._is_valid():
+                                actions += 1
+                                trials[row,col,x-1] = self._empty
+                            self._puzzle[row,col] = self._empty
+                        if len(self._nonempty(trials[row,col,:])) == 1:
+                            self._puzzle[row][col] = (
+                                self._nonempty(trials[row,col,:])[0])
+                            if self._num_solved() == 81:
+                                self.status = 'solved in %d steps' % actions
+                                return  # puzzle solved
+            if actions == start_actions:
+                self.status = 'aborted after %d steps' % actions
+                break  # puzzle too hard to solve
+
+def test():
+    import doctest
+    doctest.testmod()
+
+if __name__ == '__main__':
+    import sys
+
+    if len(sys.argv) > 1:
+        assert sys.argv[1] == '--test', sys.argv
+        test()
+        sys.exit(0)
+
+    s = Sudoku()
+    puzzle = sys.stdin.read()
+    s.load(puzzle)
+    s.solve()
+    print >> sys.stderr, s.status
+    print s.dump()