## backtracking ## we start with following state of puzzle ## + - Element present ## 0 - Hole is empty ## + ## + + ## + + + ## + + + + ## + + 0 + + ## valid moves are from a point to next to next in same line removing the ## immediate next element. Go till there is only one element left ############ design - starts ## 1. every point is represented as a node and PuzzleState class is a ## collection of such nodes ## 2. in each node, we can place a pebble or remove it ## 3. a notion of neighborhood is used to represent a collection of nodes ## from which a node can be reached. ############ design - ends import copy import time import random class Node(object): " node class corresponding to a point" def __init__(self, name): self.element = True self.name = name def removeElement(self): self.element = False def setElement(self): self.element = True def isEmpty(self): return not self.element def getName(self): return self.name def __eq__(self, right): return self.name == right.name def __str__(self): return (str(self.name) + \': Not Empty\' if self.element else str(self.name) + \': Empty\') class PuzzleState(object): " state of the puzzle at any point of time " def __init__(self): self.nodes = [] def __copy__(self): new = PuzzleState() new.nodes = self.nodes[:] return new def addNode(self, node): assert type(node) == Node self.nodes.append(node) def emptyNodes(self): return [x for x in self.nodes if x.isEmpty()] def setElement(self, node): assert type(node) == Node [x.setElement() for x in self.nodes if x == node] def removeElement(self, node): assert type(node) == Node [x.removeElement() for x in self.nodes if x == node] def noOfNodes(self): return len(self.nodes) def isEmpty(self, node): return any((x.isEmpty() for x in self.nodes if x == node)) def __str__(self): return \'\\n\'.join(str(node) for node in self.nodes) # a map of nodes to their neighbors # """ generates neighbors of the form node: [(via, neighbor_node), ...] eg: neighbors = { "11": [("21", "31"), ("22", "33")], "21": [("31", "41"), ("32", "43")], ... } """ def generateNeighbors(length): neighbors = {} # generating downward neighbors for x in xrange(length-2): x = x+1 f = x # from node p = 1 # start position while f > 0: k = 10*x+p neighbors[k] = [(10*(x+1)+p,10*(x+2)+p)] neighbors[k].append((10*(x+1)+p+1,10*(x+2)+p+2)) p = p+1 f = f-1 # right ward neighbors for x in xrange(3, length+1): p = 1 while True: # loop and a half k = 10*x+p if k in neighbors: neighbors[k].append((k+1, k+2)) else: neighbors[k] = [(k+1, k+2)] p = p+1 if p+2 > x: break # generating upward and leftward neighbors # this can be built out of the existing neighbors dictionary itself for node in neighbors.keys(): for v,n in neighbors[node]: if n in neighbors: if (v,node) not in neighbors[n]: neighbors[n].append((v,node)) else: neighbors[n] = [(v,node)] # done ! return neighbors def buildPuzzle(length): "builds initial state of puzzle" puzzle = PuzzleState() for x in xrange(length): x = x+1 r = x p = 1 while x > 0: k = 10*r+p n = Node(k) puzzle.addNode(n) x = x-1 p = p+1 k = 10*length+((length/2)+1) puzzle.removeElement(Node(k)) return puzzle class SolvePuzzle(object): "solves a given puzzle state using back tracking" def __init__(self, length=5, verbose=False): self.neighbors = generateNeighbors(length) self.puzzle = buildPuzzle(length) self.verbose = verbose if verbose: import pprint print "Neighbors: " pprint.pprint(self.neighbors) print "Puzzle state: " print self.puzzle def solve(self, puzzle, moves=[]): if puzzle is None: puzzle = self.puzzle # we are done when all but one nodes are empty if puzzle.noOfNodes() - len(puzzle.emptyNodes()) == 1: print \'Done with all the movements !\' print \'Puzzle state: \' print puzzle print "Moves: " print moves return True # if we are in dead end, we return False if all((puzzle.isEmpty(Node(n)) or puzzle.isEmpty(Node(v)) for x in puzzle.emptyNodes() if x.getName() in self.neighbors for v, n in self.neighbors[x.getName()])): if self.verbose: print \'Dead End !!\' if self.verbose: print puzzle return False # choose an empty node, one of its neighbors, make the movement and call # solve with new state puzzle empty = puzzle.emptyNodes() random.shuffle(empty) for node in empty: if node.getName() in self.neighbors: neighbors = self.neighbors[node.getName()] random.shuffle(neighbors) for v,n in neighbors: if puzzle.isEmpty(Node(v)) or puzzle.isEmpty(Node(n)): continue # we are making a movement here. let\'s copy puzzle object to avoid # writing the original object and thus messin\' up backtracking newPuzzle = copy.deepcopy(puzzle) if self.verbose: print "Moving from ", n, " via ", v, " to ", node.getName() newMoves = moves[:] newMoves.append((n, node.getName())) newPuzzle.removeElement(Node(n)) # from node newPuzzle.removeElement(Node(v)) # middle node (which is actually removed) newPuzzle.setElement(node) # to node # sleep for a second before proceeding - for monitoring # time.sleep(1) r = self.solve(newPuzzle, newMoves) if self.verbose: print \'solve returned: \', r if r: return True else: continue # we have failed ! if self.verbose: print "Searched exhaustively and failed !" return False # unit tests def tests(): # Node class n = Node("A1") assert n.name == "A1" assert n.isEmpty() == False n.removeElement() assert n.isEmpty() == True t = Node("A1") assert n == t # PuzzleState class a = Node("A1") b = Node("A2") c = Node("A3") p = PuzzleState() p.addNode(a) p.addNode(b) p.addNode(c) p.removeElement(b) assert p.emptyNodes() == [b] assert p.isEmpty(b) == True p.setElement(b) assert p.isEmpty(b) == False # neighbors neighbors = generateNeighbors(5) assert set(neighbors[33]) == set([(22, 11), (43, 53), (32, 31), (44, 55)]) assert set(neighbors[55]) == set([(54, 53), (44, 33)]) assert set(neighbors[41]) == set([(31, 21), (42, 43)]) print \'all tests passed !\' # some unit tests tests() solver = SolvePuzzle(length=7, verbose=False) # verbose object s = time.clock() solver.solve(None) # start with solving initial state print "Time taken: ", time.clock() - s
Yet another blog from yet another software engineer - a collection of my thoughts and some snippets of code I write (mainly for my later reference). If you find this useful, lets discuss in comments.
December 05, 2012
Solving Peg Puzzles
Used backtracking and randomized approach - This doesn't really scale well. Takes long time for #pegs > 21. Working on improving it. Till then, beta version :)
Subscribe to:
Post Comments (Atom)
No comments:
Post a Comment