#!/usr/bin/env python """ Recursive depth-first search with backtracking on a TopSpin puzzle. With N=8, the solved puzzle is a list of integers : solved : [0, 1, 2, 3, 4, 5, 6, 7] which can be moved in one of three ways: rotate right : [7, 0, 1, 2, 3, 4, 5, 6] rotate left : [1, 2, 3, 4, 5, 6, 7, 0] flip 1st four : [3, 2, 1, 0, 4, 5, 6, 7] The problem is to find a sequence of these three "generators" (using a term from group theory) that restores the solved position (the identity permutation) from a random starting point. (c) Jim Mahoney | Sep 26 2011 cs.marlboro.edu | opensource.org/licenses/AFL-3.0 ------------------ status : This works with iterative deepening and recursive depth-first. However, the memory/cache/hash code, trying to avoid looking at the same positions many times, isn't right yet: when enabled, the search fails completely. One issue is that the depth limit can interact with caching in an awkward way. With a depth limit (say of 10, a position near the solution is found at depth 9, stored in the cache, but the nearby solution is past the depth horizon. Then even if that position exists at a lower depth via an alternate route, the it will be ignored. I've tried to fix that issue by only leaving out a position when it's been seen at a lower or equal depth, and updating the memory cache appropriately ... but it felt like that was getting a bit too clever, and I haven't spent enough time debugging to understand whether (a) it's doing what I expect, or (b) that fixes the problem, or (c) whether that was really the problem in the first place. With N = 8, the iterative deepening recursive depth-first finds solutions in a few seconds, typically around depth 18. Another approach would be to use iterative deepening (or breadth-first for that matter) to cache the whole solution space. That would also tell us what the furthest apart puzzles are and what their separation is. There are a few doctests for the foundation routines, but that hasn't been extended to the searches. Here's some sample output of the topspin() function with the defaults. $ time python search_topspin.py iterative deepening search max_depth=20 print_every=50000 start=(6, 3, 5, 1, 4, 0, 7, 2) -- to depth 1 -- -- to depth 2 -- -- to depth 3 -- -- to depth 4 -- -- to depth 5 -- -- to depth 6 -- -- to depth 7 -- -- to depth 8 -- -- to depth 9 -- -- to depth 10 -- -- to depth 11 -- -- to depth 12 -- -- to depth 13 -- recur 50000 | unique 0 | depth 13 | (0, 6, 2, 5, 3, 4, 1, 7) | right -- to depth 14 -- recur 100000 | unique 0 | depth 15 | (7, 1, 4, 5, 2, 6, 0, 3) | flip recur 150000 | unique 0 | depth 15 | (1, 7, 0, 4, 3, 5, 6, 2) | right -- to depth 15 -- recur 200000 | unique 0 | depth 12 | (2, 7, 1, 4, 6, 0, 3, 5) | right recur 250000 | unique 0 | depth 13 | (7, 1, 5, 3, 0, 4, 6, 2) | right recur 300000 | unique 0 | depth 11 | (7, 5, 3, 4, 0, 6, 2, 1) | left recur 350000 | unique 0 | depth 16 | (0, 1, 4, 3, 5, 2, 6, 7) | left -- to depth 16 -- recur 400000 | unique 0 | depth 14 | (6, 4, 1, 3, 5, 2, 7, 0) | left recur 450000 | unique 0 | depth 16 | (3, 7, 4, 2, 0, 1, 5, 6) | flip recur 500000 | unique 0 | depth 14 | (3, 0, 4, 6, 2, 7, 1, 5) | left recur 550000 | unique 0 | depth 15 | (0, 1, 5, 6, 4, 7, 2, 3) | left recur 600000 | unique 0 | depth 17 | (3, 5, 2, 1, 7, 0, 6, 4) | flip recur 650000 | unique 0 | depth 16 | (3, 5, 7, 6, 1, 2, 4, 0) | flip recur 700000 | unique 0 | depth 17 | (4, 3, 5, 0, 7, 6, 2, 1) | left recur 750000 | unique 0 | depth 16 | (4, 3, 6, 5, 2, 1, 7, 0) | flip -- to depth 17 -- recur 800000 | unique 0 | depth 16 | (6, 4, 5, 2, 7, 1, 3, 0) | left recur 850000 | unique 0 | depth 18 | (7, 5, 3, 6, 0, 4, 1, 2) | flip recur 900000 | unique 0 | depth 17 | (7, 5, 1, 0, 4, 2, 6, 3) | flip recur 950000 | unique 0 | depth 17 | (2, 6, 4, 1, 3, 0, 7, 5) | flip recur 1000000 | unique 0 | depth 17 | (6, 2, 7, 0, 4, 1, 5, 3) | right recur 1050000 | unique 0 | depth 17 | (5, 0, 7, 6, 4, 1, 2, 3) | right recur 1100000 | unique 0 | depth 16 | (3, 6, 4, 7, 2, 5, 1, 0) | left recur 1150000 | unique 0 | depth 18 | (2, 4, 1, 5, 7, 0, 3, 6) | left recur 1200000 | unique 0 | depth 17 | (5, 2, 0, 6, 3, 7, 1, 4) | flip recur 1250000 | unique 0 | depth 17 | (4, 1, 0, 7, 6, 3, 5, 2) | flip recur 1300000 | unique 0 | depth 17 | (6, 0, 4, 2, 7, 5, 3, 1) | flip recur 1350000 | unique 0 | depth 18 | (0, 4, 3, 6, 5, 7, 2, 1) | flip recur 1400000 | unique 0 | depth 18 | (7, 6, 2, 4, 3, 0, 5, 1) | left recur 1450000 | unique 0 | depth 17 | (1, 5, 3, 0, 7, 2, 6, 4) | right recur 1500000 | unique 0 | depth 17 | (1, 2, 4, 0, 7, 3, 6, 5) | flip recur 1550000 | unique 0 | depth 18 | (2, 3, 6, 4, 1, 5, 0, 7) | right -- to depth 18 -- recur 1600000 | unique 0 | depth 18 | (6, 2, 7, 1, 4, 5, 3, 0) | left recur 1650000 | unique 0 | depth 18 | (3, 4, 1, 0, 2, 5, 7, 6) | flip recur 1700000 | unique 0 | depth 19 | (4, 0, 7, 5, 3, 6, 2, 1) | flip recur 1750000 | unique 0 | depth 19 | (5, 2, 7, 0, 1, 4, 3, 6) | left recur 1800000 | unique 0 | depth 19 | (1, 0, 4, 7, 5, 3, 6, 2) | right recur 1850000 | unique 0 | depth 18 | (3, 1, 4, 0, 6, 2, 5, 7) | flip recur 1900000 | unique 0 | depth 18 | (0, 3, 2, 5, 7, 6, 4, 1) | flip recur 1950000 | unique 0 | depth 19 | (1, 4, 7, 2, 5, 3, 0, 6) | flip recur 2000000 | unique 0 | depth 19 | (2, 7, 0, 4, 6, 3, 5, 1) | left recur 2050000 | unique 0 | depth 17 | (5, 0, 7, 2, 3, 6, 4, 1) | right recur 2100000 | unique 0 | depth 18 | (3, 5, 0, 7, 6, 4, 1, 2) | left recur 2150000 | unique 0 | depth 18 | (4, 2, 1, 7, 5, 3, 6, 0) | flip recur 2200000 | unique 0 | depth 18 | (2, 4, 6, 3, 7, 0, 1, 5) | flip recur 2250000 | unique 0 | depth 18 | (6, 1, 2, 3, 5, 7, 4, 0) | flip recur 2300000 | unique 0 | depth 19 | (1, 5, 7, 2, 6, 3, 0, 4) | left recur 2350000 | unique 0 | depth 18 | (7, 4, 6, 5, 3, 1, 2, 0) | right recur 2400000 | unique 0 | depth 19 | (5, 6, 4, 7, 0, 2, 1, 3) | left Success! (6, 3, 5, 1, 4, 0, 7, 2) right (2, 6, 3, 5, 1, 4, 0, 7) flip (5, 3, 6, 2, 1, 4, 0, 7) left (3, 6, 2, 1, 4, 0, 7, 5) flip (1, 2, 6, 3, 4, 0, 7, 5) left (2, 6, 3, 4, 0, 7, 5, 1) left (6, 3, 4, 0, 7, 5, 1, 2) flip (0, 4, 3, 6, 7, 5, 1, 2) left (4, 3, 6, 7, 5, 1, 2, 0) flip (7, 6, 3, 4, 5, 1, 2, 0) right (0, 7, 6, 3, 4, 5, 1, 2) flip (3, 6, 7, 0, 4, 5, 1, 2) left (6, 7, 0, 4, 5, 1, 2, 3) flip (4, 0, 7, 6, 5, 1, 2, 3) left (0, 7, 6, 5, 1, 2, 3, 4) flip (5, 6, 7, 0, 1, 2, 3, 4) left (6, 7, 0, 1, 2, 3, 4, 5) left (7, 0, 1, 2, 3, 4, 5, 6) left (0, 1, 2, 3, 4, 5, 6, 7) real 0m12.403s user 0m12.363s sys 0m0.037s thirty-two:search$ """ from random import shuffle def left_rotate(seq): """ Return a sequence rotated leftwards. >>> left_rotate((0,1,2,3,4,5,6,7)) (1, 2, 3, 4, 5, 6, 7, 0) """ return seq[1:] + (seq[0],) def right_rotate(seq): """ Return a sequence rotated rightwards. >>> right_rotate((0,1,2,3,4,5,6,7)) (7, 0, 1, 2, 3, 4, 5, 6) """ return (seq[-1],) + seq[:Position.N-1] def flip_first_four(seq): """ Return a sequence with the first four entries reversed. >>> flip_first_four((0,1,2,3,4,5,6,7)) (3, 2, 1, 0, 4, 5, 6, 7) """ return (seq[3], seq[2], seq[1], seq[0]) + seq[4:] class Position(object): """ A position of the TopSpin puzzle. >>> solved = Position() >>> print(solved) (0, 1, 2, 3, 4, 5, 6, 7) >>> (left, right, flip) = solved.children() >>> print(left) (1, 2, 3, 4, 5, 6, 7, 0) >>> left.depth 1 >>> left.parent.depth 0 """ N = 8 # puzzle is (0 .. N-1) with N>4 ### THIS DOESN'T WORK YET : ### It runs, but fails to find the solution, even though I'm ### trying to keep a lowest-depth version of each cached position. ### Needs more debuggin. children_are_unique = False # only new positions? do_remember = False solved_numbers = tuple(range(N)) # (0, 1, 2, ..., N-1) remembered = {} # a hash of number sequences seen moves = ('left', 'right', 'flip') # names of allowed moves action = {'left' : left_rotate, # sequence modification functions 'right' : right_rotate, 'flip' : flip_first_four } inverse = {'left' : 'right', # moves to undo a move 'right': 'left', 'flip' : 'flip'} @classmethod def is_new(cls, position): """ Return False if we've seen this before at or above this depth. """ if Position.remembered.has_key(position.numbers): previous_depth = Position.remembered[position.numbers] if position.depth < previous_depth: Position.remembered[position.numbers] = position.depth return True else: return False else: return True @classmethod def remember(cls, positions): """ Add these to the collection of positions we've seen. """ # or if we've seen it but at a lower depth, update it. if not Position.do_remember: return for position in positions: if (not Position.remembered.has_key(position.numbers)) \ or (Position.remembered[position.numbers] > position.depth): Position.remembered[position.numbers] = position.depth @classmethod def clear_remembered(cls): Position.remembered = {} @classmethod def count_remembered(cls): """ Return number of positions remembered. """ return len(Position.remembered) def __init__(self, parent=None, move=None, random=False): if random: integers = list(Position.solved_numbers) shuffle(integers) # in place: blech self.numbers = tuple(integers) self.depth = 0 self.parent = None self.move = None else: self.parent = parent self.move = move # 'right', 'left', 'flip', or None if parent and move: self.depth = parent.depth + 1 self.numbers = Position.action[move](parent.numbers[:]) else: self.depth = 0 self.numbers = Position.solved_numbers def __str__(self): return str(self.numbers) def __eq__(self, other): return self.numbers == other.numbers def solved(self): return self.numbers == Position.solved_numbers def child(self, move): return Position(parent=self, move=move) def children(self): """ Return a list of new positions below this in the search tree. """ kids = [self.child(m) for m in Position.moves if (not self.move or m != Position.inverse[self.move])] if Position.children_are_unique: kids = filter(Position.is_new, kids) Position.remember(kids) return kids def history(self): """ Return string of moves and parent positions. """ if self.parent: return self.parent.history() + \ " %6s %s \n" % (self.move, str(self)) else: return " %6s %s \n" % ('', str(self)) # count number of times through recursive search recursive_count = [0] def search(position, max_depth=16, print_every=1000): """ Recursive depth first search.n """ recursive_count[0] += 1 unique = Position.count_remembered() depth = position.depth if recursive_count[0] % print_every == 0: print " recur %7i | unique %7i | depth %3i | %s | %s " \ % (recursive_count[0], unique, depth, str(position), position.move) if depth > max_depth: return False if position.solved(): return position else: for child in position.children(): result = search(child, max_depth, print_every) if result: return result return False def iterative_deepening(start, max_depth=20, print_every=5e4, iteration_header=True): """ Recursive depth first at successively deeper levels """ print "iterative deepening search max_depth=%i print_every=%i start=%s" \ % (max_depth, print_every, str(start)) depth = 1 while depth <= max_depth: if iteration_header: print "-- to depth %i --" % depth result = search(start, depth, print_every) if result: print "Success!" print result.history() return result else: depth += 1 print "iterative deepening failed." def topspin_search(start, max_depth=20, print_every=1000): """ Solve the topspin puzzle from a given starting position. """ recursive_count[0] = 0 print "topspin search max_depth=%i print_every=%i start=%s" \ % (max_depth, print_every, str(start)) finish = search(start, max_depth, print_every) if finish: print "Success!" print finish.history() else: print "Search failed." def topspin_default(): """ The typical case: solve from a random position. """ topspin_search(Position(random=True)) def test_topspin(): """ A test case: solve from a known position, with lots of output. """ start = Position() for move in ['flip', 'left', 'flip', 'right']: start = start.child(move) start.move = None start.parent = None start.depth = 0 topspin_search(start, print_every=1, max_depth=5) def topspin(): """ A random topspin with iterative deepening. """ iterative_deepening(Position(random=True)) if __name__ == '__main__': import doctest doctest.testmod() #test_topspin() topspin()