#!/usr/bin/env python """ pegs.py Puzzle Pegs * problem 12.1 #10 from Leviti'sn "Design & Analysis of Algorithms" * part of homework for the Spring 2007 algorithms course * initial puzzle position * * * = peg * * * . = hole * * * * * * . * * Legal moves consist of jumping one peg over another to an empty square. (a) Find a shortest sequence of moves that eliminates all but one, leaving the last peg anywhere. (b) Same but leave the last peg in the orginal empty hole. For reasons that I don't really understand, doing (a) finds the (b) solution first anyway, though there are other solutions. Also, sorting (by move number) the list of possible moves gives a solution quicker. Again, I'm not sure why. Having it search over all positions and count how many solutions there are, and how many for each final peg position might answer this, particularly if it turns out there are a many more solutions with the final peg in the bottom center than other places. An example of what the program does when it runs is at the end of this file. Jim Mahoney | GPL """ __version__ = "pegs.py 2010-11-22" class Board: """ Puzzle Peg board implementation >>> b = Board(5) >>> b.size 5 >>> b.n_pegs 14 >>> print b * * * * * * * * * * * * . * * >>> len(b.moves) # number of current legal moves 36 >>> len(Board(3).moves[0]) # move is 3 coords: (start, skip, land) 3 >>> len(Board(3).moves[0][0]) # coord is 2 integers 2 >>> good_move = ( (2,0),(3,1),(4,2) ) # a legal move on board b >>> bad_move = ( (2,0),(2,1),(2,2) ) # an illegal move on board b >>> b.can_move(good_move) True >>> b.can_move(bad_move) False >>> b.do_move(good_move) >>> print b * * * . * * * . * * * * * * * >>> b.n_pegs 13 >>> b.undo_move(good_move) >>> print b * * * * * * * * * * * * . * * >>> b.n_pegs 14 """ def __init__(self, size=5): self.size = size self.n_pegs = size*(size+1)/2 - 1 # e.g. 14 pegs (1 hole) for size=5 self.data = self.get_initial_data() self.moves = self.get_moves() # possible move locations self.solution_goal = False # last peg (row,col) ; False => any self.n_positions = 0 # positions searched self.history = [] # moves made to reach this position def get_initial_data(self): """ Create and return the data for the intial board position. """ # # The board data is stored as a list of lists, top to bottom, # with True,False values for filled,empty cells. # # For a board of size 5, the initial position would be # # [[True], # [True, True], # [True, True, True], # [True, True, True, True], # [True, True, False, True, True]] # # This means that the coordinate positions on the board are # (0,0) # (1,0) (1,1) # (2,0) (2,1) (2,2) # (3,0) (3,1) (3,2) (3,2) # and so on. # data = [] for i in range(1, self.size+1): data.append([True]*i) hole = self.get_bottom_center() data[hole[0]][hole[1]] = False return data def get_bottom_center(self): """ Return (row,col) of starting hole. """ return ((self.size-1), (self.size-1)/2) def get_moves(self): """ Return the coordinate triples where moves might occur, e.g. [ ((0,0),(1,0),(2,0)), ((2,0),(1,0),(0,0)), ... ] Each move consists of consistent (start, skip, land) coordinates. """ moves = [] for row in range(0, self.size-2): # from top to 2 up from bottom for col in range(0, row+1): # full width of each row # move moves in triangle from below and right of (row,col) # * <---(row,col) * down * slant # * * * * # * * * * * * * across * down = ((row,col), (row+1,col), (row+2,col)) across = ((row+2,col), (row+2,col+1), (row+2,col+2)) slant = ((row,col), (row+1,col+1), (row+2,col+2)) for move in (down, across, slant): moves.append(move) moves.append(tuple(reversed(move))) return moves def __str__(self): result = "" for row in range(0, self.size): result += " "*(self.size - row) for col in range(0, row+1): if self.data[row][col]: result += "* " else: result += ". " result += "\n" return result[0:-1] # omit last newline def get_peg(self, coord): """ Return True if there's a peg at the given coordinate. """ return self.data[coord[0]][coord[1]] def set_peg(self, coord, value): """ Put or remove (i.e. value=True or False) a peg on the board """ self.data[coord[0]][coord[1]] = value def can_move(self, move): """ Return True if the current board allows the given move, which requires that the (start,skip,land) positions be (True, True, False) """ (start, skip, land) = move return self.get_peg(start) and \ self.get_peg(skip) and \ not self.get_peg(land) def do_move(self, move): """ Apply the given move to the board. """ (start, skip, land) = move self.set_peg(start, False) self.set_peg(skip, False) self.set_peg(land, True) self.history.append(move) self.n_pegs -= 1 def undo_move(self, move): """ Undo the given move on the board. """ (start, skip, land) = move self.set_peg(start, True) self.set_peg(skip, True) self.set_peg(land, False) self.history.pop(-1) self.n_pegs += 1 def search(self): """ Recursive backtrack search for a solution from current position. """ self.n_positions += 1 if self.n_pegs == 1: if self.solution_goal: if self.get_peg(self.solution_goal): return True else: return False else: return True else: result = False # for move in self.moves # size=5 sol'n at 14812 # for move in reversed(self.moves): # size=5 sol'n at 545 for move in sorted(self.moves): # size=5 son'n at 380 if self.can_move(move): self.do_move(move) if self.search(): result = True break self.undo_move(move) return result def print_playback(self): """ Print each position that led to this position. """ board = Board(self.size) print board for move in self.history: board.do_move(move) print print board def analyze(self, goal=False): """ Do full search and print solution history. """ self.solution_goal = goal if goal: print "Searching for 1 peg at " + str(goal) + " ..." else: print "Searching for 1 peg anywhere ..." result = self.search() if result: print "Solution found after " + str(self.n_positions) + \ " positions examined." self.print_playback() else: print "No solution found." def search_all_goals(size=5): print "Searching for solutions with a peg in each position." n_yes = 0 n_no = 0 for row in range(0, size): for col in range(0, row+1): board = Board(size) board.solution_goal = (row,col) print " " + str((row,col)) + " ... ", if board.search(): n_yes += 1 print "yes", else: n_no += 1 print "no", print " (" + str(board.n_positions) + " positions)" print "Done. Possible=" + str(n_yes) + "; impossible=" + str(n_no) def solve(size=5, goal=False): """Solve the peg puzzle with the given final position goal.""" print board = Board(size) if goal==True: goal = board.get_bottom_center() board.analyze(goal) def run(): """ Run the program from the command line. """ import sys from optparse import OptionParser parser = OptionParser( usage="usage: %prog [options]\n\n Solve the puzzle peg problem.") parser.add_option("-s", "--size", type="int", dest="size", help="size of board") parser.add_option("-v", action="store_true", default=False, \ dest="verbose_tests", help="verbose tests only") parser.add_option("-a", "--all", action="store_true", default=False, dest="all_goals", help="all goals searched") (options, args)=parser.parse_args() if options.verbose_tests: sys.exit(0) # The tests are always run; -v is caught by _test() elif options.all_goals and options.size: search_all_goals(options.size) elif options.size: solve(options.size, False) solve(options.size, True) solve(options.size, (0,0)) else: parser.print_help() sys.exit(2) def _test(): """ Run tests in comment strings. If the command line arguments include "-v", then verbose test output is printed; otherwise, nothing is printed if the tests are all successful. """ import doctest doctest.testmod() if __name__ == "__main__": _test() run() # ----------------------------------------------------------- """ mahoney@cs homework$ ./pegs.py -h usage: pegs.py [options] Solve the puzzle peg problem. options: -h, --help show this help message and exit --size=SIZE size of board (default 5) -v verbose tests only mahoney@cs homework$ ./pegs.py --size=3 Searching for 1 peg anywhere ... No solution found. Searching for 1 peg at (2, 1) ... No solution found. Searching for 1 peg at (0, 0) ... No solution found. mahoney@cs homework$ ./pegs.py --size=4 Searching for 1 peg anywhere ... Solution found after 37 positions examined. * * * * * * * . * * * * * * * * * * . . * * * * * * . . * . * . * . * * * . * . * * * . . * * . . . . . * * . * * . . . * . . * . . * . . . * * . . . . . . . . . . . * . . . . . . Searching for 1 peg at (3, 1) ... No solution found. Searching for 1 peg at (0, 0) ... No solution found. mahoney@cs homework$ ./pegs.py --size=5 Searching for 1 peg anywhere ... Solution found after 380 positions examined. * * * * * * * * * * * * . * * * * * . * * * . * * * * * * * . . * * * * * . * * * * * * * . . . * . * * * * * * * * * * . * . . . * . * * * * * * * * . * * . . . . * * . * * * * * . * * . * . . . * . * . * * * . * * . * * . . . . * . . * * . . * . . * . . * . * . . * * . . . . . . . . * * * . . * * . . . . . * . . * . * . . * . . . . . . . . . . . * . * * . . . . . . . . . . . * * . . . . . . . . . . . . . . . * . . Searching for 1 peg at (4, 2) ... Solution found after 380 positions examined. * * * * * * * * * * * * . * * * * * . * * * . * * * * * * * . . * * * * * . * * * * * * * . . . * . * * * * * * * * * * . * . . . * . * * * * * * * * . * * . . . . * * . * * * * * . * * . * . . . * . * . * * * . * * . * * . . . . * . . * * . . * . . * . . * . * . . * * . . . . . . . . * * * . . * * . . . . . * . . * . * . . * . . . . . . . . . . . * . * * . . . . . . . . . . . * * . . . . . . . . . . . . . . . * . . Searching for 1 peg at (0, 0) ... Solution found after 579 positions examined. * * * * * * * * * * * * . * * * * * . * * * . * * * * * * * . . * * * * * . * * * * * * * . . . * . * * * * * * * * * * . * . . . * . * * * * * * * * . * * . . . . * * . * * * * * . * * . * . . . * . * . * * * . * * . * . . . * . * * . . * . * * . * . . . * . . . * . * . * * . * * . . . . . . . . * . * . . * . . . . * . . . . * . * . . * * . . . . . . . . . . * . * . . . . . . . . . . . * . . . . . . . . . . . . . . mahoney@cs homework$ time (./pegs.py --size=5 --all) Searching for solutions with a peg in each position. (0, 0) ... yes (579 positions) (1, 0) ... no (2592133 positions) (1, 1) ... no (2592133 positions) (2, 0) ... no (2592133 positions) (2, 1) ... yes (1640691 positions) (2, 2) ... no (2592133 positions) (3, 0) ... yes (578 positions) (3, 1) ... no (2592133 positions) (3, 2) ... no (2592133 positions) (3, 3) ... yes (609 positions) (4, 0) ... no (2592133 positions) (4, 1) ... no (2592133 positions) (4, 2) ... yes (380 positions) (4, 3) ... no (2592133 positions) (4, 4) ... no (2592133 positions) Done. Possible=5; impossible=10 real 47m45.111s user 47m45.127s sys 0m0.044s i.e. Y n n n Y n Y n n Y n n Y n n """