#!/usr/bin/env python """ $ ./backtrack.py --n=8 Solving n-queens problem with n=8 : * . . . . . . . . . . . * . . . . . . . . . . * . . . . . * . . . . * . . . . . . . . . . . * . . * . . . . . . . . . * . . . . Number of solutions found = 92. Backtrack positions for first solution = 114. Backtrack positions for all solutions = 2057. Estimated backtrack tree = 2024 +- 206 (2 sigma). Brute force positions = 8**8 = 1.678e+07. Solves the N-queens problem with a backtracking algorithm written in Python. While only the first solution is printed, the whole backtree tree is searched. An estimate of the size of the search tree is also computed; see Levitin pg 422. $ ./backtrack.py --start=5 --end=8 --plot # n solns first all estimated sigma brute 5 10 6 54 55.4 2.414 3125 6 4 32 153 151.4 9.222 4.666e+04 7 40 10 552 564 49.99 8.235e+05 8 92 114 2057 1952 207.8 1.678e+07 $ ./backtrack.py --start=5 --end=30 --delta=5 --plot --no-search # n solns first all estimated sigma brute 5 0 0 0 53.5 2.629 3125 10 0 0 0 3.789e+04 4611 1e+10 15 0 0 0 1.815e+08 3.44e+07 4.379e+17 20 0 0 0 3.577e+12 1.082e+12 1.049e+26 25 0 0 0 1.845e+17 5.163e+16 8.882e+34 30 0 0 0 5.755e+22 2.277e+22 2.059e+44 $ ./backtrack.py -v 3 items passed all tests: 12 tests in __main__.Board 1 tests in __main__.average 1 tests in __main__.sigma 14 tests in 16 items. 14 passed and 0 failed. Test passed. Verbose test output only. (The tests are always run, but usually don't print anything if they're successful.) To use backtrack.py interactively from within python, # $ python # >>> from backtrack import Board # >>> b = Board(8) # >>> b.backtrack() For more information, see * http://en.wikipedia.org/wiki/Eight_queens_puzzle for more on this problem, and * the backtrack.py source code for the implementation details. """ # # Finding one solution runs quickly at n=16; slow n=20 it takes a while... # Searching all solutions slows down much more quickly; I haven't tested past n=12. # Jim Mahoney, April 30, for the Algorithms class. # __version__ = "$Id: backtrack.py 13262 2007-05-02 13:24:10Z mahoney $" debug = False import math import random random.seed() class Board: """ A board for the n-queens problem with one queen per row. >>> b = Board(n=4) >>> b.n # Size of board 4 >>> b.place_queen(0,1) # Put a queen at row=0, column=1. >>> b.ok_above(0) # See if board up to row 0 is OK True >>> b.place_queen(1,2) # Put a queen at row=1, column=2. >>> b.ok_above(1) # See if board up to row 1 is OK. False >>> b.place_queen(1,3) # Move queen in row=1 to column 3 ... >>> b.ok_above(1) # and now see if its OK in rows 0 and 1. True >>> print b . * . . . . . * . . . . . . . . >>> b.search(0) . * . . . . . * * . . . . . * . True >>> Board(2).backtrack() Solving n-queens problem with n=2 : No solution found. >>> Board(5).backtrack() Solving n-queens problem with n=5 : * . . . . . . * . . . . . . * . * . . . . . . * . """ def __init__(self, n=8): self.n = n # board size self.quiet_search = False # don't print output while searching self.data = [-1]*n # columns where queens are in each row self.positions_all = 0 # number of positions examined during search self.positions_first_soln = 0 # number of positions before first sol'n self.seen_first_soln = False self.solution_count = 0 def __str__(self): result = "" for row in range(0, self.n): if self.data[row] < 0: result += ". " * self.n + "\n" else: result += ". " * self.data[row] + "* " + \ ". " * (self.n - self.data[row] - 1) + "\n" return result[0:-1] # omit last newline def place_queen(self, row=0, column=0): """ Put a queen at (row, column), replacing any other in that row.""" if column < 0 or column >= self.n or row < 0 or row >= self.n: raise Exception("tried to put queen off the board") self.data[row] = column def ok_above(self, row=0): """ Return True if given row is consistent with rows above. """ for r in range(0, row): # Check to see if queen columns in two rows # match vertically or on either of the two diagonals. if self.data[r] == self.data[row] or \ self.data[r] == self.data[row] + (row - r) or \ self.data[r] == self.data[row] - (row - r): return False return True def backtrack(self, quiet=False): """ Entry point for the backtrack search. """ self.quiet_search = quiet self.positions_first_soln = 0 self.positions_all = 0 self.solution_count = 0 self.seen_first_soln = False if not self.quiet_search: print "Solving n-queens problem with n=" + str(self.n) + " : " if not self.search(0) and not self.quiet_search: print "No solution found." def search(self, row): """ Recursive search for a n-queens solution; print first one found. """ self.positions_all += 1 if not self.seen_first_soln: self.positions_first_soln += 1 if row >= self.n: # If all n queens have been placed, if not self.seen_first_soln: # then we have a solution. if not self.quiet_search: print self self.seen_first_soln = True self.solution_count += 1 return True else: # Otherwise, try to place one in the next row. result = False for column in range(0, self.n): self.place_queen(row, column) if debug: print str((row,column)) print self print if self.ok_above(row): if self.search(row+1): result = True return result def estimate_random_tree(self): """ Entry point for computing estimate of size of backtrack tree; uses random_tree_recur to do the work. The idea is that going down the tree randomly and counting how many children consistent with solution are seen at each level, we can estimate the size of the tree as 1 + c1 + c1*c2 + c1*c2*c3 + ... + c1*...*cN . """ trials = 100 totals = [] for i in range(0, trials): self.tree_size_estimate = 1 self.random_tree_recur(0, 1) totals.append(self.tree_size_estimate) self.avg_estimated_tree = average(totals) self.avg_estimated_sigma = sigma(totals)/math.sqrt(trials) def random_tree_recur(self, row, positions): """ Recursive estimate of backtrack tree size; see estimate_random_tree. """ if row >= self.n: pass else: children = [] for column in range(0, self.n): self.place_queen(row, column) if self.ok_above(row): children.append(column) positions *= len(children) self.tree_size_estimate += positions if len(children) > 0: child = children[random.randint(0, len(children)-1)] self.place_queen(row, child) self.random_tree_recur(row+1, positions) def analysis(self, plot_style=False): """ Print an analysis of a search. """ self.estimate_random_tree() if plot_style: print " %10g %10g %10g %10g %10.4g %10.4g %10.4g" \ % (self.n, self.solution_count, self.positions_first_soln, self.positions_all, self.avg_estimated_tree, 2*self.avg_estimated_sigma, (1.0*self.n)**self.n, ) else: if (self.solution_count > 0): print "Number of solutions found = %g." % self.solution_count print "Backtrack positions for first solution = %g." \ % self.positions_first_soln print "Backtrack positions for all solutions = %g." \ % self.positions_all print "Estimated backtrack tree = %.4g +- %.4g (at 2 sigma)" \ % (self.avg_estimated_tree, 2*self.avg_estimated_sigma) print "Brute force positions = %s**%s = %.4g" \ % (self.n, self.n, (1.0*self.n)**self.n) print "" def plot_header(): """ Print headers for the plot output. """ print "# %10s %10s %10s %10s %10s %10s %10s" \ % ('n', 'solns', 'first', 'all', 'estimated', 'sigma', 'brute') def average(list): """ Return average of a list of numbers. >>> average([1.,2.,3.,4.]) 2.5 """ total = 0.0 for i in list: total += i return total/len(list) def sigma(list): """ Return standard deviation of a list of numbers. >>> "%.3f " % sigma([1.,2.,3.,4.]) # rounding it off to 3 decimals '1.118 ' """ total_square = 0.0 for i in list: total_square += i**2 avg = average(list) return math.sqrt(total_square/len(list) - avg**2) def run(): """ Run the program from the command line. """ import sys from optparse import OptionParser parser = OptionParser(usage=__doc__) # A more typical string for OptionParser would be something like # usage="usage: %prog [options]\n\n Backtracking solution to n-queens" parser.add_option("-n", "--n", type="int", dest="n", help="size of board") parser.add_option("-v", action="store_true", default=False, \ dest="verbose_tests", help="verbose tests only") parser.add_option("--start", type="int", dest="start", help="starting n") parser.add_option("--end", type="int", dest="end", help="ending n") parser.add_option("--delta", type="int", dest="delta", help="step size") parser.add_option("--no-search", action="store_false", default=True, \ dest="search", help="no search; estimate tree size only") parser.add_option("--quiet-search", action="store_true", default=False, \ dest="quiet", help="don't print solutions") parser.add_option("--plot", action="store_true", default=False, \ dest="plot", help="plot style output (implies quiet)") (options, args)=parser.parse_args() if options.n: (n_start, n_end) = (options.n, options.n) elif options.start and options.end: (n_start, n_end) = (options.start, options.end) else: (n_start, n_end) = (0, 0) if n_start > 0: if options.delta: delta = options.delta else: delta = 1 if options.plot: plot_header() for n in range(n_start, n_end + 1, delta): b = Board(n) if options.search: b.backtrack(options.quiet or options.plot) b.analysis(options.plot) else: if not options.verbose_tests: parser.print_help() sys.exit(2) def _test(): """ Run tests in comment strings. If the command line arguments include "-v",the tests generate verbose output; otherwise, they don't print anything unless they fail. """ import doctest doctest.testmod() if __name__ == "__main__": _test() run()