问题
I'm trying to adapt a Depth-First Search algorithm in Python to solve the Knight's Tour puzzle. I think I've nearly succeeded, by producing a dictionary of predecessors for all the visited squares.
However, I'm stuck on how to find the autual path for the Knight. Currently, using the current
return value from tour()
in path()
gives a disappointing path of [(0, 0), (2, 1)]
.
I think the crux of the issue is determining at what point inside tour()
all squares are visited and at that point returning the current square, and returning None
if no solution is possible.
Can anyone please help me to adjust my code to produce a correct solution?
offsets = (
(-1, -2), (1, -2),
(-2, -1), (2, -1),
(-2, 1), (2, 1),
(-1, 2), (1, 2),
)
def is_legal_pos(board, pos):
i, j = pos
rows = len(board)
cols = len(board[0])
return 0 <= i < rows and 0 <= j < cols
def path(predecessors, start, goal):
current = goal
path = []
while current != start:
path.append(current)
current = predecessors[current]
path.append(start)
path.reverse()
return path
def tour(board, start):
stack = []
stack.append(start)
discovered = set()
discovered.add(start)
predecessors = dict()
predecessors[start] = None
while stack:
current = stack.pop()
for move in offsets:
row_offset, col_offset = move
next = (current[0] + row_offset, current[1] + col_offset)
if is_legal_pos(board, next) and next not in discovered:
stack.append(next)
discovered.add(next)
predecessors[next] = current
return predecessors, current
board = [[" "] * 5 for row in range(5)]
start = (0, 0)
predecessors, last = tour(board, start)
print(predecessors)
print(path(predecessors, start, last))
回答1:
Your approach has these issues:
- The algorithm merely performs a traversal (not really a search) and when all nodes have been visited (discovered) the stack will unwrap until the last square is popped from it. That last square is the first one that was ever pushed on the stack, so certainly not a node that represents the end of a long path.
- It does not include the logic for detecting a tour.
- Based on the "discovered" array, you'll only analyse a square once, which means you expect to find a tour without backtracking, as after the first backtrack you'll not be able to reuse already visited squares again in some variant path.
- The predecessor array is an idea that is used with breadth-first searches, but has not really a good use for depth-first searches
- You assume there is a solution, but you call the function with a 5x5 grid, and there is no closed knight's tour on odd sized boards
Trying to implement this with a stack instead of recursion, is making things harder for yourself than needed.
I altered your code to use recursion, and deal with the above issues.
offsets = (
(-1, -2), (1, -2),
(-2, -1), (2, -1),
(-2, 1), (2, 1),
(-1, 2), (1, 2),
)
# We don't need the board variable. Just the number of rows/cols are needed:
def is_legal_pos(rows, cols, pos):
i, j = pos
return 0 <= i < rows and 0 <= j < cols
def tour(rows, cols, start):
discovered = set()
n = rows * cols
def dfs(current): # Use recursion
discovered.add(current)
for move in offsets:
row_offset, col_offset = move
# Don't name your variable next, as that is the name of a native function
neighbor = (current[0] + row_offset, current[1] + col_offset)
# Detect whether a closed tour was found
if neighbor == start and len(discovered) == n:
return [start, current] # If so, create an extendable path
if is_legal_pos(rows, cols, neighbor) and neighbor not in discovered:
path = dfs(neighbor)
if path: # Extend the reverse path while backtracking
path.append(current)
return path
# The choice of "current" did not yield a solution. Make it available
# for a later choice, and return without a value (None)
discovered.discard(current)
return dfs(start)
# No need for a board variable. Just number of rows/cols is enough.
# As 5x5 has no solution, call the function for a 6x6 board:
print(tour(6, 6, (0, 0)))
To do this with an explicit stack, you need to also put the state of the for
loop on the stack, i.e. you should somehow know when a loop ends. For this you could make the stack such that one element on it is a list of neighbors that still need to be visited, including the one that is "current". So the stack will be a list of lists:
offsets = (
(-1, -2), (1, -2),
(-2, -1), (2, -1),
(-2, 1), (2, 1),
(-1, 2), (1, 2),
)
# We don't need the board variable. Just the number of rows/cols are needed:
def is_legal_pos(rows, cols, pos):
i, j = pos
return 0 <= i < rows and 0 <= j < cols
def tour(rows, cols, start):
discovered = set()
n = rows * cols
stack = [[start]]
while stack:
neighbors = stack[-1]
if not neighbors: # Need to backtrack...
stack.pop()
# remove the node that ended this path, and unmark it
neighbors = stack[-1]
current = neighbors.pop(0)
discovered.discard(current)
continue
while neighbors:
current = neighbors[0]
discovered.add(current)
neighbors = [] # Collect the valid neighbors
for move in offsets:
row_offset, col_offset = move
# Don't name your variable next, as that is the name of a native function
neighbor = (current[0] + row_offset, current[1] + col_offset)
# Detect whether a closed tour was found
if neighbor == start and len(discovered) == n:
path = [start] # If so, create the path from the stack
while stack:
path.append(stack.pop()[0])
return path
if is_legal_pos(rows, cols, neighbor) and neighbor not in discovered:
neighbors.append(neighbor)
# Push the collection of neighbors: deepening the search
stack.append(neighbors)
# No need for a board variable. Just number of rows/cols is enough.
# As 5x5 has no solution, call the function for a 6x6 board:
print(tour(6, 6, (0, 0)))
I personally find this code much more confusing than the recursive version. You should really go for recursion.
Note that this naive approach is far from efficient. In fact, we are a bit lucky with the 6x6 board. If you would have listed the offsets in a different order, chances are that it would take much longer to find the solution.
Please check Wikipedia's article Kinght's Tour, which lists a few algorithms that are far more efficient.
来源:https://stackoverflow.com/questions/61245303/knights-tour-in-python-getting-path-from-predecessors