#!/usr/bin/env python # Simple brute force sudoku solver. # Written by Mike Sullivan in June 2008 and released to the public domain. import sys, math class BitSet: """A set of natural numbers represented by a bit array.""" def __init__(self): self._bits = 0 def add(self, x): self._bits |= (1 << x) def remove(self, x): # set it and then remove it: deals with longs where & ~ wouldn't n = 1 << x self._bits = (self._bits | n) ^ n def __contains__(self, x): return (self._bits & (1 << x)) != 0 def union(self, s): t = BitSet() t._bits = self._bits | s._bits return t class Sudoku: """A Sudoku board representation and solver The solver is a simple brute force solver. It uses a bitset per every row, column, and square to keep track of what values are available at any given location.""" def __init__(self, f): """Initialize a Sudoku board from a file. f -- a file object to read from""" self.read_board(f) self.setup_sets() # this could be a one liner, but I wanted to error check :( def read_board(self, f): """Read a Sudoku board from a file. This will determine the initial values and and size of the board. BUG: While this is mostly able to deal with board sizes larger than 9, it can not deal with any initial values other than 1-9. f -- a file object to read from""" size = 0 board = [] for line in f: line = line.strip() if not line: continue if not size: size = len(line) if size != len(line): raise ValueError("expected size %d, got %d" % (size, len(line))) board.append([ int(c) for c in line ]) if (len(board) != size): raise ValueError("expected size %d, got %d" % (size, len(board))) n = int(math.sqrt(size)) if (n*n != size): raise ValueError("size %d not a perfect square" % size) self.n, self.board = n, board def __str__(self): """Format the board as a string for pretty output""" n = self.n s = "" for i in xrange(len(self.board)): for j in xrange(len(self.board)): s += str(self.board[i][j]) if (j+1) % n == 0: s += ' ' if (i+1) % n == 0: s += '\n' s += '\n' return s.strip() def setup_sets(self): """Create and populate the sets for keeping track of numbers used""" n, board = self.n, self.board self.row_sets = [ BitSet() for row in board ] self.col_sets = [ BitSet() for row in board ] self.sqr_sets = [ [ BitSet() for i in xrange(n) ] for j in xrange(n) ] for i in xrange(len(board)): for j in xrange(len(board)): x = board[i][j] if not x: continue self.set_pos(i, j, x) def next_pos(self, i, j): j += 1 if j >= len(self.board): i += 1 j = 0 return i, j def set_pos(self, i, j, x): # print "setting %d, %d to %d" % (i, j, x) self.board[i][j] = x self.row_sets[i].add(x) self.col_sets[j].add(x) self.sqr_sets[i//self.n][j//self.n].add(x) def clear_pos(self, i, j, x): self.board[i][j] = 0 self.row_sets[i].remove(x) self.col_sets[j].remove(x) self.sqr_sets[i//self.n][j//self.n].remove(x) def get_blocked(self, i, j): return self.row_sets[i].union( self.col_sets[j].union( self.sqr_sets[i//self.n][j//self.n])) def solve(self, i=0, j=0): """Attempt to find a single solution to this puzzle. Returns a boolean indicating whether it succeeded and updates the state of the board.""" if i >= len(self.board): return True elif self.board[i][j]: return self.solve(*self.next_pos(i, j)) blocked = self.get_blocked(i, j) for x in xrange(1, len(self.board)+1): if x not in blocked: self.set_pos(i, j, x) if self.solve(*self.next_pos(i, j)): return True self.clear_pos(i, j, x) return False def main(args): game = Sudoku(sys.stdin) print "Puzzle:" print game print if game.solve(): print "Solution:" print game else: print "No solutions found" if __name__ == '__main__': sys.exit(main(sys.argv))