Knight's Tour in Python - Getting Path from Predecessors

谁都会走 提交于 2021-02-11 18:23:47

问题


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

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!