From: W. Trevor King Date: Wed, 13 Oct 2010 11:39:03 +0000 (-0400) Subject: Add Sudoku._slice_completion_solver() to sudoku.py. X-Git-Url: http://git.tremily.us/?a=commitdiff_plain;h=115cc73124fcf9b5690b65ad1afb1385be3c17a8;p=parallel_computing.git Add Sudoku._slice_completion_solver() to sudoku.py. --- diff --git a/assignments/archive/sudoku/soln/sudoku.py b/assignments/archive/sudoku/soln/sudoku.py index 2c08697..9011067 100755 --- a/assignments/archive/sudoku/soln/sudoku.py +++ b/assignments/archive/sudoku/soln/sudoku.py @@ -1,25 +1,48 @@ #!/usr/bin/env python +import itertools + import numpy +TEST_PUZZLE_STRING = '\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', + ]) + + +def power_set(iterable): + """Return the power set of `iterable`. + + >>> for i in power_set([1,2,3]): + ... print i + () + (1,) + (2,) + (3,) + (1, 2) + (1, 3) + (2, 3) + (1, 2, 3) + """ + s = list(iterable) + return itertools.chain.from_iterable( + itertools.combinations(s, r) for r in range(len(s)+1)) + + 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.load(TEST_PUZZLE_STRING) >>> s._num_solved() 30 >>> s.solve() @@ -54,6 +77,8 @@ class Sudoku (object): self._empty = 0 self._external_empty = '-' self.status = None + self.solvers = [self._direct_elimination_solver, + self._slice_completion_solver] def load(self, text): row = 0 @@ -129,41 +154,108 @@ class Sudoku (object): def _col(self, col): return self._puzzle[:,col] - def _cell(self, cell_row, cell_col): + def _cell_bounds(self, cell_row, cell_col): ri = cell_row * 3 rf = ri + 3 ci = cell_col * 3 cf = ci + 3 + return (ri, rf, ci, cf) + + def _cell(self, cell_row, cell_col): + ri,rf,ci,cf = self._cell_bounds(cell_row, cell_col) return self._puzzle[ri:rf,ci:cf] + def _slices(self): + """ + >>> s = Sudoku() + >>> s.load(TEST_PUZZLE_STRING) + >>> for type,slice,index in s._slices(): + ... print type,slice,index # doctest: +ELLIPSIS + row [5 3 0 0 7 0 0 0 0] 0 + ... + row [0 0 0 0 8 0 0 7 9] 8 + col [5 6 0 8 4 7 0 0 0] 0 + ... + col [0 0 0 3 1 6 0 5 9] 8 + cell [[5 3 0] + [6 0 0] + [0 9 8]] (0, 0) + ... + cell [[2 8 0] + [0 0 5] + [0 7 9]] (2, 2) + """ + for row in range(9): + yield ('row', self._row(row), row) + for col in range(9): + yield ('col', self._col(col), col) + for cell_row in range(3): + for cell_col in range(3): + yield ('cell', self._cell(cell_row, cell_col), + (cell_row, cell_col)) + + def _point_to_cell_coords(self, row, col): + """ + >>> s = Sudoku() + >>> s._point_to_cell_coords(4, 6) + (1, 2, 3) + + The point in question: + + - - - - - - - - - + - 0 - - 1 - - 2 - + - - - - - - - - - + + - - - - - - 0 1 2 + - 1 - - - - * - - + - - - - - - - - - + + - - - - - - - - - + - - - - - - - - - + - - - - - - - - - + """ + cell_row_residual = row % 3 + cell_col_residual = col % 3 + return (row/3, col/3, + cell_row_residual*3 + cell_col_residual) + + def _cell_to_point_coords(self, cell_row, cell_col, i): + """ + >>> s = Sudoku() + >>> s._cell_to_point_coords(1, 2, 3) + (4, 6) + + The point in question: + + 0 1 2 3 4 5 6 - - + 1 - - - - - - - - + 2 - - - - - - - - + + 3 - - - - - - - - + 4 - - - - - * - - + - - - - - - - - - + + - - - - - - - - - + - - - - - - - - - + - - - - - - - - - + """ + row = cell_row * 3 + (i / 3) + col = cell_col * 3 + (i % 3) + return (row, col) + 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.load(TEST_PUZZLE_STRING) >>> 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 @@ -171,8 +263,6 @@ class Sudoku (object): 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 @@ -180,40 +270,38 @@ class Sudoku (object): 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): + for type,slice,index in self._slices(): + values = self._nonempty(slice.flat) + if len(values) != len(set(values)): 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): + actions = 0 + trials = self._setup_trials() + while True: + start_actions = actions + for solver in self.solvers: + acts,trials = solver(trials) + self._apply_trials(trials) + actions += acts + if acts > 0: + break # don't use slow solvers unless they're required + if self._num_solved() == 81: + self.status = 'solved in %d steps' % actions + return # puzzle solved + elif actions == start_actions: + self.status = 'aborted after %d steps' % actions + return # puzzle too hard to solve + + def _setup_trials(self): trials = numpy.zeros((9,9,9), dtype=numpy.int) for row in range(9): for col in range(9): @@ -222,42 +310,329 @@ class Sudoku (object): else: x = self._puzzle[row,col] trials[row,col,x-1] = x + return trials + + def _apply_trials(self, trials): + for row in range(9): + for col in range(9): + if len(self._nonempty(trials[row,col,:])) == 1: + self._puzzle[row][col] = ( + self._nonempty(trials[row,col,:])[0]) + assert self._is_valid(), ( + 'error setting [%d,%d] to %d' + % (row, col, self._puzzle[row][col])) + + def _trial_slice(self, trials, type, index): + """Return a slice from the trials array. + + >>> s = Sudoku() + >>> s.load(TEST_PUZZLE_STRING) + >>> trials = s._setup_trials() + >>> t = s._trial_slice(trials, 'row', 0) + >>> t # doctest: +REPORT_UDIFF + array([[0, 0, 0, 0, 5, 0, 0, 0, 0], + [0, 0, 3, 0, 0, 0, 0, 0, 0], + [1, 2, 3, 4, 5, 6, 7, 8, 9], + [1, 2, 3, 4, 5, 6, 7, 8, 9], + [0, 0, 0, 0, 0, 0, 7, 0, 0], + [1, 2, 3, 4, 5, 6, 7, 8, 9], + [1, 2, 3, 4, 5, 6, 7, 8, 9], + [1, 2, 3, 4, 5, 6, 7, 8, 9], + [1, 2, 3, 4, 5, 6, 7, 8, 9]]) + For `row` and `column` slices, the original `trials` array + responds to changes in `t`. + + >>> trials[0,2,:] + array([1, 2, 3, 4, 5, 6, 7, 8, 9]) + >>> t[2,:] = 0 + >>> t[2,3] = 4 + >>> trials[0,2,:] + array([0, 0, 0, 4, 0, 0, 0, 0, 0]) + + `cell` slices don't work with "flat" indexing, because the + stride would not be constant. You'll have to push changes + back to `trials` by hand. + + >>> t = s._trial_slice(trials, 'cell', (0, 0)) + >>> t # doctest: +REPORT_UDIFF + array([[0, 0, 0, 0, 5, 0, 0, 0, 0], + [0, 0, 3, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 4, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 6, 0, 0, 0], + [1, 2, 3, 4, 5, 6, 7, 8, 9], + [1, 2, 3, 4, 5, 6, 7, 8, 9], + [1, 2, 3, 4, 5, 6, 7, 8, 9], + [0, 0, 0, 0, 0, 0, 0, 0, 9], + [0, 0, 0, 0, 0, 0, 0, 8, 0]]) + >>> trials[1,1,:] + array([1, 2, 3, 4, 5, 6, 7, 8, 9]) + >>> t[4,:] = 0 + >>> t[4,6] = 7 + >>> trials[1,1,:] = t[4,:] + >>> trials[1,1,:] + array([0, 0, 0, 0, 0, 0, 7, 0, 0]) + """ + if type == 'row': + t = trials[index,:,:] + elif type == 'col': + t = trials[:,index,:] + else: + assert type == 'cell', type + cell_row,cell_col = index + ri,rf,ci,cf = self._cell_bounds(cell_row, cell_col) + t = trials[ri:rf,ci:cf,:] + t = t.reshape((t.shape[0]*t.shape[1], t.shape[2])).copy() + if type in ['row', 'col']: + assert t.flags.owndata == False, t.flags.owndata + assert t.base is trials, t.base + else: + assert t.flags.owndata == True, t.flags.owndata + return t + + def _direct_elimination_solver(self, trials): + r"""Eliminate trials if a point already has the trial digit in + its row/col/cell. + + >>> puzzle = '\n'.join([ + ... '1 2 3 - - - - - -', + ... '4 5 6 - - - - - -', + ... '7 8 - - - - - - -', + ... '', + ... '- - - - - - - - -', + ... '- - - - - - - - -', + ... '- - - - - - - - -', + ... '', + ... '- - - - - - - - -', + ... '- - - - - - - - -', + ... '- - - - - - - - -', + ... ]) + >>> s = Sudoku() + >>> s.load(puzzle) + >>> trials = s._setup_trials() + >>> actions,trials = s._direct_elimination_solver(trials) + + The solver eliminated three numbers for two columns and rows + and two numbers for three columns and rows, which makes for + 104 = 8 # point 2,2 (the solved point) + + 6*3 # row 0 + + 6*3 # row 1 + + 6*2 # row 2 + + 6*3 # col 0 + + 6*3 # col 1 + + 6*2 # col 2 + eliminations. + + >>> actions + 104 + >>> s._apply_trials(trials) + >>> print s.dump() + 1 2 3 - - - - - - + 4 5 6 - - - - - - + 7 8 9 - - - - - - + + - - - - - - - - - + - - - - - - - - - + - - - - - - - - - + + - - - - - - - - - + - - - - - - - - - + - - - - - - - - - + """ actions = 0 - while True: - start_actions = actions - 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(): + 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 + return (actions, trials) + + def _slice_completion_solver(self, trials): + r"""Eliminate trials if a set of N points have trials drawn + only from a list of N options. + + For example, a slice like + [1, 2, 3, {4,5}, {4,6}, {4,5,6}, {4,5,6,7,8,9}, + {4,5,6,7,8,9}, {4,5,6,7,8,9}] + has three points {4,5}, {4,6}, and {4,5,6}, with three possible + numbers: 4, 5, and 6. That means, 4, 5, and 6 would definitely + occupy the three points and other points should not have those + numbers. + + >>> puzzle = '\n'.join([ + ... '1 2 3 - - - - - -', + ... '- - - 7 - - - - -', + ... '- - - - 8 9 - - -', + ... '', + ... '- - - 6 5 - - - -', + ... '- - - - - - - - -', + ... '- - - - - - - - -', + ... '', + ... '- - - - - - - - -', + ... '- - - - - - - - -', + ... '- - - - - - - - -', + ... ]) + >>> s = Sudoku() + >>> s.load(puzzle) + >>> trials = s._setup_trials() + + Take a first pass through `_direct_elimination_solver()` to + eliminate trial values in the rows/columns/cells blocked by + the initialized points. + + >>> actions,trials = s._direct_elimination_solver(trials) + + The solver eliminated the following possibilities (by number) + 142 = 6 + 6 + 6 # number 1, cell+row+col + + 6 + 6 + 6 # number 2, cell+row+col + + 6 + 6 + 6 # number 3, cell+row+col + + 7 + 6 + 5 # number 5, cell+row+col + + 7 + 6 + 5 # number 6, cell+row+col + + 6 + 6 + 5 # number 7, cell+row+col + + 6 + 6 + 5 # number 8, cell+row+col + + 6 + 6 + 6 # number 9, cell+row+col + + >>> actions + 142 + + However the direct solver was unable to actually solve any new + points. + + >>> s._apply_trials(trials) + >>> print s.dump() + 1 2 3 - - - - - - + - - - 7 - - - - - + - - - - 8 9 - - - + + - - - 6 5 - - - - + - - - - - - - - - + - - - - - - - - - + + - - - - - - - - - + - - - - - - - - - + - - - - - - - - - + >>> trials[0,:,:] # doctest: +REPORT_UDIFF + array([[1, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 2, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 3, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 4, 5, 0, 0, 0, 0], + [0, 0, 0, 4, 0, 6, 0, 0, 0], + [0, 0, 0, 4, 5, 6, 0, 0, 0], + [0, 0, 0, 4, 5, 6, 7, 8, 9], + [0, 0, 0, 4, 5, 6, 7, 8, 9], + [0, 0, 0, 4, 5, 6, 7, 8, 9]]) + + Now we proceed with the slice comparison solver. + + >>> actions,trials = s._slice_completion_solver(trials) + + The solver reduced trials in the following points + 12 = 1 # point 0,6, removed 4,5,6 must be in center of row 0 + + 1 # point 0,7, removed 4,5,6 must be in center of row 0 + + 1 # point 0,8, removed 4,5,6 must be in center of row 0 + + 1 # point 1,4, removed 4,6 must be in top of cell 0,1 + + 1 # point 1,5, removed 4,5,6 must be in top of cell 0,1 + + 1 # point 2,3, removed 4,5 must be in top of cell 0,1 + + 1 # point 1,6, removed 8,9 must be in top of cell 0,2 + + 1 # point 1,7, removed 8,9 must be in top of cell 0,2 + + 1 # point 1,8, removed 8,9 must be in top of cell 0,2 + + 1 # point 2,6, removed 7 must be in top of cell 0,2 + + 1 # point 2,7, removed 7 must be in top of cell 0,2 + + 1 # point 2,8, removed 7 must be in top of cell 0,2 + + >>> actions + 12 + >>> trials[0,:,:] # doctest: +REPORT_UDIFF + array([[1, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 2, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 3, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 4, 5, 0, 0, 0, 0], + [0, 0, 0, 4, 0, 6, 0, 0, 0], + [0, 0, 0, 4, 5, 6, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 7, 8, 9], + [0, 0, 0, 0, 0, 0, 7, 8, 9], + [0, 0, 0, 0, 0, 0, 7, 8, 9]]) + """ + actions = 0 + for _type,slice,index in self._slices(): + assert slice.size == 9, slice + trial_slice = self._trial_slice(trials, _type, index) + missing = set(self._nonempty(trial_slice.flat)) + for possible in power_set(missing): + possible = set(possible) + if possible in [set(), missing]: + continue + points = [] + for k in range(slice.size): + trial_set = set(self._nonempty(trial_slice[k,:])) + if trial_set.issubset(possible): + points.append(k) + if len(points) == len(possible): + possible_trial_slice = [0]*9 + for p in possible: + possible_trial_slice[p-1] = p + for k in range(slice.size): + if k in points: + ts = numpy.array(possible_trial_slice) + for i,p in enumerate(ts): + if trial_slice[k,i] == self._empty: + ts[i] = self._empty + else: + ts = trial_slice[k,:].copy() + for i,p in enumerate(ts): + if p in possible: + ts[i] = self._empty + if _type == 'cell': + cell_row,cell_col = index + row,col = self._cell_to_point_coords( + cell_row, cell_col, k) + if (ts != trials[row,col,:]).any(): 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 + #print _type, index, k, row, col, trial_slice[k,:], ts + trials[row,col,:] = ts + trial_slice[k,:] = ts + else: # row or column + if (ts != trial_slice[k,:]).any(): + actions += 1 + #print _type, index, k, trial_slice[k,:], ts + trial_slice[k,:] = ts + return (actions, trials) + def test(): import doctest doctest.testmod() if __name__ == '__main__': + import optparse import sys - if len(sys.argv) > 1: - assert sys.argv[1] == '--test', sys.argv + p = optparse.OptionParser() + p.add_option('--test', dest='test', default=False, action='store_true', + help='Run unit tests and exit.') + p.add_option('-d', '--disable-direct', dest='direct', default=True, + action='store_false', + help='Disable the direct elimination solver') + p.add_option('-c', '--disable-completion', dest='completion', default=True, + action='store_false', + help='Disable the slice completion solver') + + options,args = p.parse_args() + + if options.test: test() sys.exit(0) s = Sudoku() + if not options.direct: + s.solvers.remove(s._direct_elimination_solver) + if not options.completion: + s.solvers.remove(s._slice_completion_solver) + puzzle = sys.stdin.read() s.load(puzzle) try: