diff options
author | Tim Peters <tim.peters@gmail.com> | 2001-07-13 09:12:12 (GMT) |
---|---|---|
committer | Tim Peters <tim.peters@gmail.com> | 2001-07-13 09:12:12 (GMT) |
commit | 9a8c8e270bd2293a21160872d792dcc6a3ddb46b (patch) | |
tree | 2df21ca9398955f518ba2d0dd6782a7eabb6986c /Lib | |
parent | 7eea271464dccc48525cae2b677a68dd7f442c5d (diff) | |
download | cpython-9a8c8e270bd2293a21160872d792dcc6a3ddb46b.zip cpython-9a8c8e270bd2293a21160872d792dcc6a3ddb46b.tar.gz cpython-9a8c8e270bd2293a21160872d792dcc6a3ddb46b.tar.bz2 |
Having fun on my own time: quicker flat_conjoin; Knights Tour solver
simplified and generalized to rectangular boards.
Diffstat (limited to 'Lib')
-rw-r--r-- | Lib/test/test_generators.py | 236 |
1 files changed, 123 insertions, 113 deletions
diff --git a/Lib/test/test_generators.py b/Lib/test/test_generators.py index db6d66f..9aa462f 100644 --- a/Lib/test/test_generators.py +++ b/Lib/test/test_generators.py @@ -903,33 +903,42 @@ def conjoin(gs): # conjoin too. # NOTE WELL: This allows large problems to be solved with only trivial # demands on stack space. Without explicitly resumable generators, this is -# much harder to achieve. +# much harder to achieve. OTOH, this is much slower (up to a factor of 2) +# than the fancy unrolled recursive conjoin. def flat_conjoin(gs): # rename to conjoin to run tests with this instead n = len(gs) values = [None] * n iters = [None] * n + _StopIteration = StopIteration # make local because caught a *lot* i = 0 - while i >= 0: - # Need a fresh iterator. - if i >= n: - yield values - # Backtrack. - i -= 1 + while 1: + # Descend. + try: + while i < n: + it = iters[i] = gs[i]().next + values[i] = it() + i += 1 + except _StopIteration: + pass else: - iters[i] = gs[i]().next + assert i == n + yield values - # Need next value from current iterator. + # Backtrack until an older iterator can be resumed. + i -= 1 while i >= 0: try: values[i] = iters[i]() - except StopIteration: - # Backtrack. - i -= 1 - else: - # Start fresh at next level. + # Success! Start fresh at next level. i += 1 break + except _StopIteration: + # Continue backtracking. + i -= 1 + else: + assert i < 0 + break # A conjoin-based N-Queens solver. @@ -986,31 +995,21 @@ class Queens: # A conjoin-based Knight's Tour solver. This is pretty sophisticated # (e.g., when used with flat_conjoin above, and passing hard=1 to the # constructor, a 200x200 Knight's Tour was found quickly -- note that we're -# creating 10s of thousands of generators then!), so goes on at some length +# creating 10s of thousands of generators then!), and is lengthy. class Knights: - def __init__(self, n, hard=0): - self.n = n + def __init__(self, m, n, hard=0): + self.m, self.n = m, n - def coords2index(i, j): - return i*n + j - - offsets = [( 1, 2), ( 2, 1), ( 2, -1), ( 1, -2), - (-1, -2), (-2, -1), (-2, 1), (-1, 2)] - succs = [] - for i in range(n): - for j in range(n): - s = [coords2index(i+io, j+jo) for io, jo in offsets - if 0 <= i+io < n and - 0 <= j+jo < n] - succs.append(s) - del s - del offsets + # solve() will set up succs[i] to be a list of square #i's + # successors. + succs = self.succs = [] - free = [0] * n**2 # 0 if occupied, 1 if visited - nexits = free[:] # number of free successors + # Remove i0 from each of its successor's successor lists, i.e. + # successors can't go back to i0 again. Return 0 if we can + # detect this makes a solution impossible, else return 1. - def decrexits(i0): + def remove_from_successors(i0, len=len): # If we remove all exits from a free square, we're dead: # even if we move to it next, we can't leave it again. # If we create a square with one exit, we must visit it next; @@ -1021,159 +1020,170 @@ class Knights: # the other one a dead end. ne0 = ne1 = 0 for i in succs[i0]: - if free[i]: - e = nexits[i] - 1 - nexits[i] = e - if e == 0: - ne0 += 1 - elif e == 1: - ne1 += 1 + s = succs[i] + s.remove(i0) + e = len(s) + if e == 0: + ne0 += 1 + elif e == 1: + ne1 += 1 return ne0 == 0 and ne1 < 2 - def increxits(i0): + # Put i0 back in each of its successor's successor lists. + + def add_to_successors(i0): for i in succs[i0]: - if free[i]: - nexits[i] += 1 + succs[i].append(i0) # Generate the first move. def first(): - if n < 1: + if m < 1 or n < 1: return - # Initialize board structures. - for i in xrange(n**2): - free[i] = 1 - nexits[i] = len(succs[i]) - # Since we're looking for a cycle, it doesn't matter where we # start. Starting in a corner makes the 2nd move easy. - corner = coords2index(0, 0) - free[corner] = 0 - decrexits(corner) + corner = self.coords2index(0, 0) + remove_from_successors(corner) self.lastij = corner yield corner - increxits(corner) - free[corner] = 1 + add_to_successors(corner) # Generate the second moves. def second(): - corner = coords2index(0, 0) + corner = self.coords2index(0, 0) assert self.lastij == corner # i.e., we started in the corner - if n < 3: + if m < 3 or n < 3: return - assert nexits[corner] == len(succs[corner]) == 2 - assert coords2index(1, 2) in succs[corner] - assert coords2index(2, 1) in succs[corner] + assert len(succs[corner]) == 2 + assert self.coords2index(1, 2) in succs[corner] + assert self.coords2index(2, 1) in succs[corner] # Only two choices. Whichever we pick, the other must be the - # square picked on move n**2, as it's the only way to get back + # square picked on move m*n, as it's the only way to get back # to (0, 0). Save its index in self.final so that moves before # the last know it must be kept free. for i, j in (1, 2), (2, 1): - this, final = coords2index(i, j), coords2index(3-i, 3-j) - assert free[this] and free[final] + this = self.coords2index(i, j) + final = self.coords2index(3-i, 3-j) self.final = final - nexits[final] += 1 # it has an exit back to 0,0 - free[this] = 0 - decrexits(this) + remove_from_successors(this) + succs[final].append(corner) self.lastij = this yield this - increxits(this) - free[this] = 1 + succs[final].remove(corner) + add_to_successors(this) - nexits[final] -= 1 - - # Generate moves 3 thru n**2-1. - def advance(): + # Generate moves 3 thru m*n-1. + def advance(len=len): # If some successor has only one exit, must take it. # Else favor successors with fewer exits. candidates = [] for i in succs[self.lastij]: - if free[i]: - e = nexits[i] - assert e > 0, "else decrexits() pruning flawed" - if e == 1: - candidates = [(e, i)] - break - candidates.append((e, i)) + e = len(succs[i]) + assert e > 0, "else remove_from_successors() pruning flawed" + if e == 1: + candidates = [(e, i)] + break + candidates.append((e, i)) else: candidates.sort() for e, i in candidates: if i != self.final: - if decrexits(i): - free[i] = 0 + if remove_from_successors(i): self.lastij = i yield i - free[i] = 1 - increxits(i) + add_to_successors(i) - # Generate moves 3 thru n**2-1. Alternative version using a + # Generate moves 3 thru m*n-1. Alternative version using a # stronger (but more expensive) heuristic to order successors. - # Since the # of backtracking levels is n**2, a poor move early on - # can take eons to undo. Smallest n for which this matters a lot is - # n==52. - def advance_hard(midpoint=(n-1)/2.0): + # Since the # of backtracking levels is m*n, a poor move early on + # can take eons to undo. Smallest square board for which this + # matters a lot is 52x52. + def advance_hard(vmid=(m-1)/2.0, hmid=(n-1)/2.0, len=len): # If some successor has only one exit, must take it. # Else favor successors with fewer exits. # Break ties via max distance from board centerpoint (favor # corners and edges whenever possible). candidates = [] for i in succs[self.lastij]: - if free[i]: - e = nexits[i] - assert e > 0, "else decrexits() pruning flawed" - if e == 1: - candidates = [(e, 0, i)] - break - i1, j1 = divmod(i, n) - d = (i1 - midpoint)**2 + (j1 - midpoint)**2 - candidates.append((e, -d, i)) + e = len(succs[i]) + assert e > 0, "else remove_from_successors() pruning flawed" + if e == 1: + candidates = [(e, 0, i)] + break + i1, j1 = self.index2coords(i) + d = (i1 - vmid)**2 + (j1 - hmid)**2 + candidates.append((e, -d, i)) else: candidates.sort() for e, d, i in candidates: if i != self.final: - if decrexits(i): - free[i] = 0 + if remove_from_successors(i): self.lastij = i yield i - free[i] = 1 - increxits(i) + add_to_successors(i) # Generate the last move. def last(): assert self.final in succs[self.lastij] yield self.final - if n <= 1: - self.rowgenerators = [first] + if m*n < 4: + self.squaregenerators = [first] else: - self.rowgenerators = [first, second] + \ - [hard and advance_hard or advance] * (n**2 - 3) + \ + self.squaregenerators = [first, second] + \ + [hard and advance_hard or advance] * (m*n - 3) + \ [last] + def coords2index(self, i, j): + assert 0 <= i < self.m + assert 0 <= j < self.n + return i * self.n + j + + def index2coords(self, index): + assert 0 <= index < self.m * self.n + return divmod(index, self.n) + + def _init_board(self): + succs = self.succs + del succs[:] + m, n = self.m, self.n + c2i = self.coords2index + + offsets = [( 1, 2), ( 2, 1), ( 2, -1), ( 1, -2), + (-1, -2), (-2, -1), (-2, 1), (-1, 2)] + rangen = range(n) + for i in range(m): + for j in rangen: + s = [c2i(i+io, j+jo) for io, jo in offsets + if 0 <= i+io < m and + 0 <= j+jo < n] + succs.append(s) + # Generate solutions. def solve(self): - for x in conjoin(self.rowgenerators): + self._init_board() + for x in conjoin(self.squaregenerators): yield x def printsolution(self, x): - n = self.n - assert len(x) == n**2 - w = len(str(n**2 + 1)) + m, n = self.m, self.n + assert len(x) == m*n + w = len(str(m*n)) format = "%" + str(w) + "d" - squares = [[None] * n for i in range(n)] + squares = [[None] * n for i in range(m)] k = 1 for i in x: - i1, j1 = divmod(i, n) + i1, j1 = self.index2coords(i) squares[i1][j1] = format % k k += 1 sep = "+" + ("-" * w + "+") * n print sep - for i in range(n): + for i in range(m): row = squares[i] print "|" + "|".join(row) + "|" print sep @@ -1269,7 +1279,7 @@ Solution 2 And run a Knight's Tour on a 10x10 board. Note that there are about 20,000 solutions even on a 6x6 board, so don't dare run this to exhaustion. ->>> k = Knights(10) +>>> k = Knights(10, 10) >>> LIMIT = 2 >>> count = 0 >>> for x in k.solve(): |