簡體   English   中英

通過一個障礙尋找路徑(Google Foobar:准備兔子的逃生)

[英]Path-finding through one obstacle (Google Foobar: Prepare the Bunnies' Escape)

我在解決涉及尋路的 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),這個解決方案似乎沒有問題。

任何幫助(或失敗的案例)將不勝感激!

邏輯:

可以使用 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表示被一條沒有 1s(全 0s)的路徑訪問過
    • 1表示被具有 1 的路徑訪問過
    • 2表示兩種類型的路徑都訪問過

    原因:

    在通常的 BFS 問題中,我們避免在一個節點被訪問一次后訪問它,在這個問題中, maze每個節點都可以被訪問兩次,然后被標記為不再訪問。 這是因為:

    • 如果一個節點第一次被一條僅由 0 組成的路徑訪問,那么它不應被任何僅由 0 組成的其他路徑再次訪問,因為該另一條路徑要么具有相同的長度(在這種情況下它不會) t 問題)或更長時間(在這種情況下,我們無論如何都不想考慮它,因為該路徑正在到達一個節點,該節點已經被一條由全 0 組成的較短路徑遍歷)。

      相同的邏輯適用於一個節點,該節點已經被一條穿過 1 的路徑訪問,現在又被一條具有 1 的路徑訪問(我們將拒絕后面的路徑,因為它與第一條路徑相同或更長)。

    • 然而,如果一個節點之前被一條 1 的路徑訪問過,現在被一條只有 0 的路徑訪問,我們也想考慮這條后面的路徑。 為什么? 因為有可能在遍歷 1 后到達該節點的較早路徑可能無法到達目的地,因為它可能到達需要遍歷額外 1 才能到達目的地的點,但因為它已經遍歷了 1 ,不能再進一步了。 因此,這條新路徑可能比之前的路徑更長,但還沒有遍歷 1,也許可以遍歷額外的 1。

      例子

      示例圖像

      在這個例子中,雖然紅色路徑首先到達節點[2, 0] ,但它不是正確的路徑,因為它在[3, 0]處被阻塞。 因此,即使綠色路徑比紅色路徑長,我們也必須考慮通過[2, 0]的綠色路徑。

最后,基本情況是當您到達maze右下節點時停止。 (問題說明總會有解決方案,因此無需檢查無解決方案的情況。)

代碼:

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])

示例測試用例:

在此處輸入圖片說明

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM