穿越一个障碍物的路径规划(Google Foobar: 准备兔子的逃生计划)

6

我遇到了一个涉及路径查找的Google Foobar问题,但是我的解决方案无法通过2个测试用例,这些输入和输出被隐藏。

问题描述:

你有一些空间站的地图,每个地图都从监狱出口开始并以逃生舱门结束。地图由0和1的矩阵表示,其中0表示可通过的空间,1表示不可通过的墙壁。监狱的出口在左上角(0,0),逃生舱门在右下角(w-1,h-1)。

编写一个函数answer(map),生成从监狱门到逃生舱门的最短路径长度,其中您可以删除一堵墙作为改造计划的一部分。路径长度是您通过的节点总数,包括入口和出口节点。起始和结束位置始终可通过(0)。地图始终是可解的,尽管您可能需要删除墙壁。地图的高度和宽度可以从2到20。只能进行基本方向的移动;不允许对角线移动。

测试用例

输入: (int) maze = [[0, 1, 1, 0], [0, 0, 0, 1], [1, 1, 0, 0], [1, 1, 1, 0]]

输出: (int) 7

输入: (int) maze = [[0, 0, 0, 0, 0, 0], [1, 1, 1, 1, 1, 0], [0, 0, 0, 0, 0, 0], [0, 1, 1, 1, 1, 1], [0, 1, 1, 1, 1, 1], [0, 0, 0, 0, 0, 0]]

输出: (int) 11

我的代码:

from queue import PriorityQueue

# Grid class
class Grid:
    # Initialized with dimensions to check later if all neighbor points are actually within the graph
    def __init__(self, width, height):
        self.width = width
        self.height = height
        self.walls = []
        self.weights = {}
        self.wall_count = 0

    # Find the cost of a certain destination node
    # Cost is reported as a tuple to account for going across a wall: (# of moves through a wall, # of normal moves)
    def cost(self, from_node, to_node):
        if to_node in self.walls:
            return self.weights.get(to_node, (1, 0))
        else:
            return self.weights.get(to_node, (0, 1))

    # Check if the location is actually within the graph
    def in_bounds(self, id):
        (x, y) = id
        return 0 <= x < self.width and 0 <= y < self.height

    # Find the adjacent nodes of a node (ie. the places it can go to)
    # Filters out any result which isn't on the graph using self.in_bounds
    def neighbors(self, id):
        (x, y) = id
        results = [(x+1, y), (x, y-1), (x-1, y), (x, y+1)]
        if (x + y) % 2 == 0: results.reverse() # aesthetics
        results = filter(self.in_bounds, results)
        return results

# Find the dimensions of the 2D list by finding the lengths of the outer and inner lists
def dimensions_2d(xs):
    width = len(xs)
    height = len(xs[0])
    return (width, height)

# Returns all the positions of an element in a 2D list
# In this case it's used to find all walls (occurences of 1) to pass to the Grid object
def index_2d(xs, v):
    results = [(x, y) for y, ls in enumerate(xs) for x, item in enumerate(ls) if item == v]
    return results

# Djikstra search algorithm; mistakenly named "a_star" before
# Returns both a dictionary of "destination" locations to "start" locations (tuples) as well as a dictionary of the calculated cost of each location on the grid
def djikstra_search(graph, start, goal):
    # Priority Queue to select nodes from
    frontier = PriorityQueue()
    # Place our starting cost in
    frontier.put(start, (0, 0))

    came_from = {}
    cost_so_far = {}
    came_from[start] = None
    cost_so_far[start] = (0, 0)

    while not frontier.empty():
        # Get the element with the highest priority from the queue
        current = frontier.get()

        if current == goal:
            break

        # For every neighbor of the selected node
        for next in graph.neighbors(current):
            # The new cost of the neighbor node is current cost plus cost of this node - (1, 0) if it goes through a wall, (0, 1) otherwise
            new_cost = (cost_so_far[current][0] + graph.cost(current, next)[0], cost_so_far[current][1] + graph.cost(current, next)[1])
            # If the node has not cost currently
            # OR if the number of walls traveled through is less than the current cost
            # AND if the number of normal steps taken is less than or the same as the current number
            if next not in cost_so_far or (new_cost[0] < cost_so_far[next][0] and sum(new_cost) <= sum(cost_so_far[next])):
                # Record it in both the cost and came_from dicts
                cost_so_far[next] = new_cost
                # Place the cost in the queue
                priority = new_cost
                frontier.put(next, priority)
                came_from[next] = current

    return came_from, cost_so_far

# Find the length of the calculated path
# Using the returned list of edges from djikstra_search, move backwards from the target end and increment the length until the start element is reached
def path(grid, start, end):
    # Perform the search
    path = djikstra_search(grid, start, end)
    search = path[0]

    # If the end element's cost travels through more than 1 wall return 0
    if path[1].get(end)[0] > 1:
        return 0

    # Otherwise move backwards from the end element and increment length each time
    # Once the start element has been reached, we have our final length
    length = 1
    last = end
    while last != start:
        last = search.get(last)
        length += 1

    return length

# The "main" function
def answer(maze):
    # Find all occurences of walls (1) in the 2D list
    walls = index_2d(maze, 1)
    # Find the x and y dimensions of the maze (required for the Grid object)
    dims = dimensions_2d(maze)
    # Create a new grid with our found dimensions
    grid = Grid(dims[0], dims[1])

    # The start point will always be at (0,0) and the end will always be at the bottom-right so we define those here
    start = (0, 0)
    end   = (dims[0] - 1, dims[1] - 1)

    # the walls variable's locations are flipped, so reverse each of them to get the right wall positions
    grid.walls = [(y, x) for x, y in walls]

    # Return the length
    return path(grid, start, end)

在我的测试中(网格大小最多为7x7),这个解决方案似乎没有问题。

非常感谢任何帮助(或失败的情况)!


我期望看到广度优先搜索/ Dijkstra 算法,但在你的代码中找不到相应的关键字。也许如果你解释一下代码背后的思想,会激励更多人帮你调试它;-) - Alfe
1
我有另一个想法,可以(自动)找到失败的测试用例:编写一个(希望更简单的)迷宫解决方案,不包括墙体移除选项,创建随机迷宫,测量它们的成本(路径长度),在这个解决方案中插入一个随机障碍物,将新的迷宫交给你的算法,并查看它是否仍然具有相同的成本(因为它应该能够移除一个障碍物)。成本不同的情况很有趣(不一定是你要搜索的测试用例,但可能是)。 - Alfe
@Alfe 我尝试制作一个空白的网格,然后在出口旁边添加2堵墙-两者的步数相同(也许我需要更复杂的迷宫?)。有趣的是,使用简单的成本计算,每当通过一堵墙时将成本增加大量(例如1000)可以通过其中一个测试用例,但不能通过另一个... - dcao
1
@cmdd 我曾经遇到同样的问题,然后在这里提问了:http://codereview.stackexchange.com/questions/143152/bfs-shortest-path我得到的答案帮助我让解决方案更快、更高效。你可以看一下,也许正是你所需要的。 - oxtay
@oxtay,你在回答者的解决方案基础上添加了任何额外的优化吗? - dcao
你用过哪些测试类别?你发布的两个相对简单。例如,使用一个迷宫,其中有三个准备好的解决方案:一个不移除任何墙壁,另外两个更短的解决方案分别移除一面墙。更短的解决方案将包括在结果路径的每个端点附近进行移除。为了进行第二次测试,反转迷宫。你的算法应该在每种情况下找到相同的墙壁移除。 - Prune
1个回答

0

逻辑:

这个问题可以使用BFS解决,需要进行以下修改:

  1. 标准 BFS 算法中本应全为 0 的每个路径都可以有一个 1。

  2. 在 BFS 队列中,除了跟踪 x 和 y 坐标外,每个节点还将存储到达该节点所需的步数(路径长度)以及该节点的路径是否已经遍历过 1。因此,queue 中的每个节点都具有以下格式 - [[row, col], num_steps, one_traversed]

    因此,虽然看起来我们需要在 BFS 遍历中存储整个路径,但实际上我们需要存储的是路径中是否已经遍历过 1。因此,每个节点将为自己存储该信息。

  3. 我们将维护一个 visited 网格,用于跟踪所有已访问的节点(以避免循环),但这里有一个棘手的部分 - 与其仅使用 1 和 0 表示已访问或未访问,此网格将具有 4 种可能的值:

    • -1 是初始值
    • 0 表示由没有 1(全部为 0)的路径访问
    • 1 表示由包含 1 的路径访问
    • 2 表示由两种类型的路径访问

    原因:

    在通常的 BFS 问题中,我们避免在其被访问一次后再次访问节点,但在此问题中,maze 中的每个节点在标记为不再访问之前可以被访问两次。这是因为:

    • 如果一个节点首次被只包含 0 的路径访问,那么它不应该再被只包含 0 的其他路径访问,因为另一条路径要么与其长度相同(在这种情况下无关紧要),要么更长(在这种情况下,我们无论如何都不想考虑它,因为该路径到达了已经被全部为 0 的较短路径遍历过的节点)。

      对于已被一条路径遍历过 1 并且现在又被一条包含 1 的路径访问的节点,同样适用上述逻辑(我们将拒绝后者的路径,因为它要么与第一条路径相同,要么比第一条路径更长)。

    • 但是,如果曾经被包含 1 的路径访问过的节点现在又被只包含 0 的路径访问,我们也希望考虑这条后来的路径。为什么?因为可能出现这样的情况:先前到达该节点的路径在到达目的地时需要遍历额外的 1,但由于它已经遍历过一个 1,因此无法继续前进。因此,这条新路径可能比先前的路径更长,但尚未遍历 1,因此可能能够遍历额外的 1。

      示例

      Example Image

      在此示例中,红色路径虽然首先到达节点 [2, 0],但它不是正确的路径,因为它在 [3, 0] 处被阻塞。因此,即使绿色

      最后,基本情况是在迷宫的右下节点停止。(问题说明总会有解决方案,因此不需要检查无解情况。)

      代码:

      grid = [[0, 0, 0, 0],
      [1, 1, 1, 0],
      [0, 0, 0, 0],
      [1, 1, 1, 1],
      [0, 1, 1, 1],
      [0, 0, 0, 0]]  # using the name grid instead of maze
      
      num_rows = len(grid)
      num_cols = len(grid[0])
      
      def is_valid(r, c):
          return True if (0 <= r < num_rows and 0 <= c < num_cols) else False
      
      def get_neighbours(r, c):
          up = [r - 1, c]
          down = [r + 1, c]
          left = [r, c - 1]
          right = [r, c + 1]
      
          neighbours = [down, right, up, left]
          valid_neighbour = list()
      
          for neighbour in neighbours:
              if is_valid(*neighbour):
                  valid_neighbour.append(neighbour)
      
          return valid_neighbour
      
      # queue format is [[row, col], num_steps, one_traversed]
      queue = [[[0, 0], 1, 0]]
      
      cols = list()
      visited = list()
      
      # visited matrix is used to keep track of visited nodes:
      # -1 is default
      # 0 means visited by a path having no 1s
      # 1 means visited by a path having a 1
      # 2 means visited by both paths - having 1 and 0s
      for j in range(num_rows):
          visited.append([-1] * num_cols)
      
      visited[0][0] = 0
      
      # BFS
      while queue:
          current_node = queue.pop(0)
      
          r, c, num_steps, one_traversed = current_node[0][0], current_node[0][
              1], current_node[1], current_node[2]
      
          # Base Case
          if r == num_rows - 1 and c == num_cols - 1:
              print(num_steps)
      
          neighbours = get_neighbours(r, c)
      
          for neighbour in neighbours:
              if visited[neighbour[0]][neighbour[1]] in [0, 1]:
                  # the current node was previously visited with opposite one_traversed value, so consider it
                  if visited[neighbour[0]][neighbour[1]] != one_traversed:
                      one_traversed_now = 1 if grid[neighbour[0]][neighbour[1]] == 1 else one_traversed
                      visited[neighbour[0]][neighbour[1]] = 2
      
                      queue.append([[neighbour[0], neighbour[1]], num_steps + 1, one_traversed_now])
              elif visited[neighbour[0]][neighbour[1]] == -1:
                  if grid[neighbour[0]][neighbour[1]] == 1 and one_traversed == 1:
                      continue
      
                  one_traversed_now = 1 if grid[neighbour[0]][neighbour[1]] == 1 else one_traversed
      
                  visited[neighbour[0]][neighbour[1]] = one_traversed_now
                  queue.append([[neighbour[0], neighbour[1]], num_steps + 1, one_traversed_now])
      

      示例测试用例:

      enter image description here


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接