#!/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()
syntax highlighted by Code2HTML, v. 0.93pm6