8 TEST_PUZZLE_STRING = '\n'.join([
23 def power_set(iterable):
24 """Return the power set of `iterable`.
26 >>> for i in power_set([1,2,3]):
38 return itertools.chain.from_iterable(
39 itertools.combinations(s, r) for r in range(len(s)+1))
42 class Sudoku (object):
45 >>> s.load(TEST_PUZZLE_STRING)
67 array([5, 3, 4, 6, 7, 8, 9, 1, 2])
69 array([5, 6, 1, 8, 4, 7, 9, 2, 3])
76 self._puzzle = numpy.zeros((9,9), dtype=numpy.int)
78 self._external_empty = '-'
80 self.solvers = [self._direct_elimination_solver,
81 self._slice_completion_solver]
85 for line in text.splitlines():
86 if len(line) == 0 or line.startswith('#'):
89 for col,value in enumerate(line.split()):
91 self._puzzle[row,col] = self._convert_to_internal_value(value)
93 self.status = 'loaded puzzle'
95 def _convert_to_internal_value(self, value):
98 >>> s._convert_to_internal_value(s._external_empty)
100 >>> s._convert_to_internal_value('0')
101 Traceback (most recent call last):
104 >>> s._convert_to_internal_value('1')
106 >>> s._convert_to_internal_value('9')
108 >>> s._convert_to_internal_value('10')
109 Traceback (most recent call last):
113 if value == self._external_empty:
117 assert value >= 1 and value <= 9, value
124 lines.append('') # blank rows between cells
128 line.append(' ') # blank columns between cells
129 line.append(self._convert_to_external_value(
130 self._puzzle[row,col]))
131 lines.append(' '.join(line))
132 return '\n'.join(lines)
134 def _convert_to_external_value(self, value):
137 >>> s._convert_to_external_value(s._empty)
139 >>> s._convert_to_external_value(1)
141 >>> s._convert_to_external_value(9)
144 if value == self._empty:
145 value = self._external_empty
147 assert value >= 1 and value <= 9, value
152 return self._puzzle[row,:]
155 return self._puzzle[:,col]
157 def _cell_bounds(self, cell_row, cell_col):
162 return (ri, rf, ci, cf)
164 def _cell(self, cell_row, cell_col):
165 ri,rf,ci,cf = self._cell_bounds(cell_row, cell_col)
166 return self._puzzle[ri:rf,ci:cf]
171 >>> s.load(TEST_PUZZLE_STRING)
172 >>> for type,slice,index in s._slices():
173 ... print type,slice,index # doctest: +ELLIPSIS
174 row [5 3 0 0 7 0 0 0 0] 0
176 row [0 0 0 0 8 0 0 7 9] 8
177 col [5 6 0 8 4 7 0 0 0] 0
179 col [0 0 0 3 1 6 0 5 9] 8
189 yield ('row', self._row(row), row)
191 yield ('col', self._col(col), col)
192 for cell_row in range(3):
193 for cell_col in range(3):
194 yield ('cell', self._cell(cell_row, cell_col),
195 (cell_row, cell_col))
197 def _point_to_cell_coords(self, row, col):
200 >>> s._point_to_cell_coords(4, 6)
203 The point in question:
217 cell_row_residual = row % 3
218 cell_col_residual = col % 3
219 return (row/3, col/3,
220 cell_row_residual*3 + cell_col_residual)
222 def _cell_to_point_coords(self, cell_row, cell_col, i):
225 >>> s._cell_to_point_coords(1, 2, 3)
228 The point in question:
242 row = cell_row * 3 + (i / 3)
243 col = cell_col * 3 + (i % 3)
246 def _nonempty(self, values):
247 return [x for x in values if x != self._empty]
252 >>> s.load(TEST_PUZZLE_STRING)
258 >>> s._puzzle[0,3] = 5
261 >>> s._puzzle[0,3] = s._empty
263 Test an invalid column.
265 >>> s._puzzle[8,0] = 5
268 >>> s._puzzle[8,0] = s._empty
270 Test and invalid cell.
272 >>> s._puzzle[2,0] = 3
275 >>> s._puzzle[2,0] = s._empty
277 for type,slice,index in self._slices():
278 values = self._nonempty(slice.flat)
279 if len(values) != len(set(values)):
283 def _num_solved(self):
284 return len(self._nonempty(self._puzzle.flatten()))
288 trials = self._setup_trials()
290 start_actions = actions
291 for solver in self.solvers:
292 acts,trials = solver(trials)
293 self._apply_trials(trials)
296 break # don't use slow solvers unless they're required
297 if self._num_solved() == 81:
298 self.status = 'solved in %d steps' % actions
299 return # puzzle solved
300 elif actions == start_actions:
301 self.status = 'aborted after %d steps' % actions
302 return # puzzle too hard to solve
304 def _setup_trials(self):
305 trials = numpy.zeros((9,9,9), dtype=numpy.int)
308 if self._puzzle[row,col] == self._empty:
309 trials[row,col,:] = range(1,10)
311 x = self._puzzle[row,col]
312 trials[row,col,x-1] = x
315 def _apply_trials(self, trials):
318 if len(self._nonempty(trials[row,col,:])) == 1:
319 self._puzzle[row][col] = (
320 self._nonempty(trials[row,col,:])[0])
321 assert self._is_valid(), (
322 'error setting [%d,%d] to %d'
323 % (row, col, self._puzzle[row][col]))
325 def _trial_slice(self, trials, type, index):
326 """Return a slice from the trials array.
329 >>> s.load(TEST_PUZZLE_STRING)
330 >>> trials = s._setup_trials()
331 >>> t = s._trial_slice(trials, 'row', 0)
332 >>> t # doctest: +REPORT_UDIFF
333 array([[0, 0, 0, 0, 5, 0, 0, 0, 0],
334 [0, 0, 3, 0, 0, 0, 0, 0, 0],
335 [1, 2, 3, 4, 5, 6, 7, 8, 9],
336 [1, 2, 3, 4, 5, 6, 7, 8, 9],
337 [0, 0, 0, 0, 0, 0, 7, 0, 0],
338 [1, 2, 3, 4, 5, 6, 7, 8, 9],
339 [1, 2, 3, 4, 5, 6, 7, 8, 9],
340 [1, 2, 3, 4, 5, 6, 7, 8, 9],
341 [1, 2, 3, 4, 5, 6, 7, 8, 9]])
343 For `row` and `column` slices, the original `trials` array
344 responds to changes in `t`.
347 array([1, 2, 3, 4, 5, 6, 7, 8, 9])
351 array([0, 0, 0, 4, 0, 0, 0, 0, 0])
353 `cell` slices don't work with "flat" indexing, because the
354 stride would not be constant. You'll have to push changes
355 back to `trials` by hand.
357 >>> t = s._trial_slice(trials, 'cell', (0, 0))
358 >>> t # doctest: +REPORT_UDIFF
359 array([[0, 0, 0, 0, 5, 0, 0, 0, 0],
360 [0, 0, 3, 0, 0, 0, 0, 0, 0],
361 [0, 0, 0, 4, 0, 0, 0, 0, 0],
362 [0, 0, 0, 0, 0, 6, 0, 0, 0],
363 [1, 2, 3, 4, 5, 6, 7, 8, 9],
364 [1, 2, 3, 4, 5, 6, 7, 8, 9],
365 [1, 2, 3, 4, 5, 6, 7, 8, 9],
366 [0, 0, 0, 0, 0, 0, 0, 0, 9],
367 [0, 0, 0, 0, 0, 0, 0, 8, 0]])
369 array([1, 2, 3, 4, 5, 6, 7, 8, 9])
372 >>> trials[1,1,:] = t[4,:]
374 array([0, 0, 0, 0, 0, 0, 7, 0, 0])
377 t = trials[index,:,:]
379 t = trials[:,index,:]
381 assert type == 'cell', type
382 cell_row,cell_col = index
383 ri,rf,ci,cf = self._cell_bounds(cell_row, cell_col)
384 t = trials[ri:rf,ci:cf,:]
385 t = t.reshape((t.shape[0]*t.shape[1], t.shape[2])).copy()
386 if type in ['row', 'col']:
387 assert t.flags.owndata == False, t.flags.owndata
388 assert t.base is trials, t.base
390 assert t.flags.owndata == True, t.flags.owndata
393 def _direct_elimination_solver(self, trials):
394 r"""Eliminate trials if a point already has the trial digit in
397 >>> puzzle = '\n'.join([
398 ... '1 2 3 - - - - - -',
399 ... '4 5 6 - - - - - -',
400 ... '7 8 - - - - - - -',
402 ... '- - - - - - - - -',
403 ... '- - - - - - - - -',
404 ... '- - - - - - - - -',
406 ... '- - - - - - - - -',
407 ... '- - - - - - - - -',
408 ... '- - - - - - - - -',
412 >>> trials = s._setup_trials()
413 >>> actions,trials = s._direct_elimination_solver(trials)
415 The solver eliminated three numbers for two columns and rows
416 and two numbers for three columns and rows, which makes for
417 104 = 8 # point 2,2 (the solved point)
428 >>> s._apply_trials(trials)
445 if self._puzzle[row][col] == self._empty:
446 for x in self._nonempty(trials[row,col,:]):
447 self._puzzle[row,col] = x
448 if not self._is_valid():
450 trials[row,col,x-1] = self._empty
451 self._puzzle[row,col] = self._empty
452 return (actions, trials)
454 def _slice_completion_solver(self, trials):
455 r"""Eliminate trials if a set of N points have trials drawn
456 only from a list of N options.
458 For example, a slice like
459 [1, 2, 3, {4,5}, {4,6}, {4,5,6}, {4,5,6,7,8,9},
460 {4,5,6,7,8,9}, {4,5,6,7,8,9}]
461 has three points {4,5}, {4,6}, and {4,5,6}, with three possible
462 numbers: 4, 5, and 6. That means, 4, 5, and 6 would definitely
463 occupy the three points and other points should not have those
466 >>> puzzle = '\n'.join([
467 ... '1 2 3 - - - - - -',
468 ... '- - - 7 - - - - -',
469 ... '- - - - 8 9 - - -',
471 ... '- - - 6 5 - - - -',
472 ... '- - - - - - - - -',
473 ... '- - - - - - - - -',
475 ... '- - - - - - - - -',
476 ... '- - - - - - - - -',
477 ... '- - - - - - - - -',
481 >>> trials = s._setup_trials()
483 Take a first pass through `_direct_elimination_solver()` to
484 eliminate trial values in the rows/columns/cells blocked by
485 the initialized points.
487 >>> actions,trials = s._direct_elimination_solver(trials)
489 The solver eliminated the following possibilities (by number)
490 142 = 6 + 6 + 6 # number 1, cell+row+col
491 + 6 + 6 + 6 # number 2, cell+row+col
492 + 6 + 6 + 6 # number 3, cell+row+col
493 + 7 + 6 + 5 # number 5, cell+row+col
494 + 7 + 6 + 5 # number 6, cell+row+col
495 + 6 + 6 + 5 # number 7, cell+row+col
496 + 6 + 6 + 5 # number 8, cell+row+col
497 + 6 + 6 + 6 # number 9, cell+row+col
502 However the direct solver was unable to actually solve any new
505 >>> s._apply_trials(trials)
518 >>> trials[0,:,:] # doctest: +REPORT_UDIFF
519 array([[1, 0, 0, 0, 0, 0, 0, 0, 0],
520 [0, 2, 0, 0, 0, 0, 0, 0, 0],
521 [0, 0, 3, 0, 0, 0, 0, 0, 0],
522 [0, 0, 0, 4, 5, 0, 0, 0, 0],
523 [0, 0, 0, 4, 0, 6, 0, 0, 0],
524 [0, 0, 0, 4, 5, 6, 0, 0, 0],
525 [0, 0, 0, 4, 5, 6, 7, 8, 9],
526 [0, 0, 0, 4, 5, 6, 7, 8, 9],
527 [0, 0, 0, 4, 5, 6, 7, 8, 9]])
529 Now we proceed with the slice comparison solver.
531 >>> actions,trials = s._slice_completion_solver(trials)
533 The solver reduced trials in the following points
534 12 = 1 # point 0,6, removed 4,5,6 must be in center of row 0
535 + 1 # point 0,7, removed 4,5,6 must be in center of row 0
536 + 1 # point 0,8, removed 4,5,6 must be in center of row 0
537 + 1 # point 1,4, removed 4,6 must be in top of cell 0,1
538 + 1 # point 1,5, removed 4,5,6 must be in top of cell 0,1
539 + 1 # point 2,3, removed 4,5 must be in top of cell 0,1
540 + 1 # point 1,6, removed 8,9 must be in top of cell 0,2
541 + 1 # point 1,7, removed 8,9 must be in top of cell 0,2
542 + 1 # point 1,8, removed 8,9 must be in top of cell 0,2
543 + 1 # point 2,6, removed 7 must be in top of cell 0,2
544 + 1 # point 2,7, removed 7 must be in top of cell 0,2
545 + 1 # point 2,8, removed 7 must be in top of cell 0,2
549 >>> trials[0,:,:] # doctest: +REPORT_UDIFF
550 array([[1, 0, 0, 0, 0, 0, 0, 0, 0],
551 [0, 2, 0, 0, 0, 0, 0, 0, 0],
552 [0, 0, 3, 0, 0, 0, 0, 0, 0],
553 [0, 0, 0, 4, 5, 0, 0, 0, 0],
554 [0, 0, 0, 4, 0, 6, 0, 0, 0],
555 [0, 0, 0, 4, 5, 6, 0, 0, 0],
556 [0, 0, 0, 0, 0, 0, 7, 8, 9],
557 [0, 0, 0, 0, 0, 0, 7, 8, 9],
558 [0, 0, 0, 0, 0, 0, 7, 8, 9]])
561 for _type,slice,index in self._slices():
562 assert slice.size == 9, slice
563 trial_slice = self._trial_slice(trials, _type, index)
564 missing = set(self._nonempty(trial_slice.flat))
565 for possible in power_set(missing):
566 possible = set(possible)
567 if possible in [set(), missing]:
570 for k in range(slice.size):
571 trial_set = set(self._nonempty(trial_slice[k,:]))
572 if trial_set.issubset(possible):
574 if len(points) == len(possible):
575 possible_trial_slice = [0]*9
577 possible_trial_slice[p-1] = p
578 for k in range(slice.size):
580 ts = numpy.array(possible_trial_slice)
581 for i,p in enumerate(ts):
582 if trial_slice[k,i] == self._empty:
585 ts = trial_slice[k,:].copy()
586 for i,p in enumerate(ts):
590 cell_row,cell_col = index
591 row,col = self._cell_to_point_coords(
592 cell_row, cell_col, k)
593 if (ts != trials[row,col,:]).any():
595 #print _type, index, k, row, col, trial_slice[k,:], ts
596 trials[row,col,:] = ts
597 trial_slice[k,:] = ts
598 else: # row or column
599 if (ts != trial_slice[k,:]).any():
601 #print _type, index, k, trial_slice[k,:], ts
602 trial_slice[k,:] = ts
603 return (actions, trials)
610 if __name__ == '__main__':
614 p = optparse.OptionParser()
615 p.add_option('--test', dest='test', default=False, action='store_true',
616 help='Run unit tests and exit.')
617 p.add_option('-d', '--disable-direct', dest='direct', default=True,
618 action='store_false',
619 help='Disable the direct elimination solver')
620 p.add_option('-c', '--disable-completion', dest='completion', default=True,
621 action='store_false',
622 help='Disable the slice completion solver')
624 options,args = p.parse_args()
631 if not options.direct:
632 s.solvers.remove(s._direct_elimination_solver)
633 if not options.completion:
634 s.solvers.remove(s._slice_completion_solver)
636 puzzle = sys.stdin.read()
640 except KeyboardInterrupt, e:
641 s.status = 'interrupted'
642 print >> sys.stderr, s.status