![](/img/trans.png)
[英]Google Foobar Prepare the Bunnies' Escape (BFS/A* search, Python)
[英]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 進行以下修改來解決該問題:
每條在標准 BFS 算法中全為 0 的路徑都可以有一個 1。
在 BFS 隊列中,每個節點除了跟蹤 x 和 y 坐標外,還將存儲到達那里的步數(路徑長度),以及該節點的路徑是否已經遍歷了 1 或不是。 因此, queue
中的每個節點都將具有格式 - [[row, col], num_steps, one_traversed]
。
因此,雖然看起來我們需要在 BFS 遍歷中存儲整個路徑,但我們實際需要存儲的是是否在路徑中遍歷了 1。 因此,每個節點都會為自己存儲該信息。
我們將維護一個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.