#!/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