如何在广度优先搜索中追踪路径?
Posted
技术标签:
【中文标题】如何在广度优先搜索中追踪路径?【英文标题】:How to trace the path in a Breadth-First Search? 【发布时间】:2012-02-13 21:01:54 【问题描述】:如何跟踪广度优先搜索的路径,例如以下示例:
如果搜索键 11
,返回连接 1 到 11 的 最短 列表。
[1, 4, 7, 11]
【问题讨论】:
这实际上是我几个月前帮助朋友完成的一项旧任务,基于凯文培根法则。我的最终解决方案非常草率,我基本上做了另一个广度优先搜索来“倒带”和回溯。我不想找到更好的解决方案。 优秀。我考虑重新审视一个老问题,试图找到一个更好的答案,这是工程师的一个令人钦佩的特质。祝你在学习和职业生涯中一切顺利。 谢谢夸奖,我只相信现在不学的话,我会再次面临同样的问题。 How to get the path between 2 nodes using Breadth-First Search? 的可能重复项 【参考方案1】:你应该先看看http://en.wikipedia.org/wiki/Breadth-first_search。
下面是一个快速实现,其中我使用列表列表来表示路径队列。
# graph is in adjacent list representation
graph =
'1': ['2', '3', '4'],
'2': ['5', '6'],
'5': ['9', '10'],
'4': ['7', '8'],
'7': ['11', '12']
def bfs(graph, start, end):
# maintain a queue of paths
queue = []
# push the first path into the queue
queue.append([start])
while queue:
# get the first path from the queue
path = queue.pop(0)
# get the last node from the path
node = path[-1]
# path found
if node == end:
return path
# enumerate all adjacent nodes, construct a
# new path and push it into the queue
for adjacent in graph.get(node, []):
new_path = list(path)
new_path.append(adjacent)
queue.append(new_path)
print bfs(graph, '1', '11')
打印:['1', '4', '7', '11']
另一种方法是维护从每个节点到其父节点的映射,并在检查相邻节点时记录其父节点。搜索完成后,只需根据父映射进行回溯即可。
graph =
'1': ['2', '3', '4'],
'2': ['5', '6'],
'5': ['9', '10'],
'4': ['7', '8'],
'7': ['11', '12']
def backtrace(parent, start, end):
path = [end]
while path[-1] != start:
path.append(parent[path[-1]])
path.reverse()
return path
def bfs(graph, start, end):
parent =
queue = []
queue.append(start)
while queue:
node = queue.pop(0)
if node == end:
return backtrace(parent, start, end)
for adjacent in graph.get(node, []):
if node not in queue :
parent[adjacent] = node # <<<<< record its parent
queue.append(adjacent)
print bfs(graph, '1', '11')
以上代码基于没有循环的假设。
【讨论】:
这太棒了!我的思维过程使我相信创建某种类型的表格或矩阵,但我还没有学习图表。谢谢。 我也尝试过使用回溯方法,尽管这看起来更简洁。如果您只知道开始和结束但不知道中间的节点,是否可以制作图表?或者甚至是图表之外的另一种方法? 是否可以调整第一个算法,使其返回从 1 到 11 的 all 路径(假设不止一个)? @l19 当您找到路径 (node==end
) 时,将该路径添加到包含您找到的所有路径的另一个列表中,然后添加 continue
而不是 return
。如果您使用访问集来防止循环,请永远不要将结束节点添加到访问集(否则只有一条路径可以拥有该结束节点)。
建议使用 collections.deque 而不是列表。 list.pop(0) 的复杂度是 O(n) 而 deque.popleft() 的复杂度是 O(1)【参考方案2】:
我想我会为了好玩而尝试编写代码:
graph =
'1': ['2', '3', '4'],
'2': ['5', '6'],
'5': ['9', '10'],
'4': ['7', '8'],
'7': ['11', '12']
def bfs(graph, forefront, end):
# assumes no cycles
next_forefront = [(node, path + ',' + node) for i, path in forefront if i in graph for node in graph[i]]
for node,path in next_forefront:
if node==end:
return path
else:
return bfs(graph,next_forefront,end)
print bfs(graph,[('1','1')],'11')
# >>>
# 1, 4, 7, 11
如果你想要循环,你可以添加这个:
for i, j in for_front: # allow cycles, add this code
if i in graph:
del graph[i]
【讨论】:
在构建 next_for_front 之后。一个后续问题,如果图表包含循环怎么办?例如。如果节点 1 有一条边连接回自身?如果图在两个节点之间有多条边怎么办?【参考方案3】:很喜欢qiao的第一个回答! 这里唯一缺少的是将顶点标记为已访问。 为什么我们需要这样做? 让我们假设有另一个节点 13 从节点 11 连接。现在我们的目标是找到节点 13。 运行一段时间后,队列将如下所示:
[[1, 2, 6], [1, 3, 10], [1, 4, 7], [1, 4, 8], [1, 2, 5, 9], [1, 2, 5, 10]]
请注意,最后有两条节点编号为 10 的路径。 这意味着来自节点号 10 的路径将被检查两次。在这种情况下,它看起来并没有那么糟糕,因为 10 号节点没有任何子节点。但它可能真的很糟糕(即使在这里,我们也会无缘无故地检查该节点两次。) 节点号 13 不在这些路径中,因此程序在到达最后节点号为 10 的第二条路径之前不会返回。我们将重新检查它。
我们所缺少的只是一组标记访问过的节点而不是再次检查它们.. 这是qiao修改后的代码:
graph =
1: [2, 3, 4],
2: [5, 6],
3: [10],
4: [7, 8],
5: [9, 10],
7: [11, 12],
11: [13]
def bfs(graph_to_search, start, end):
queue = [[start]]
visited = set()
while queue:
# Gets the first path in the queue
path = queue.pop(0)
# Gets the last node in the path
vertex = path[-1]
# Checks if we got to the end
if vertex == end:
return path
# We check if the current node is already in the visited nodes set in order not to recheck it
elif vertex not in visited:
# enumerate all adjacent nodes, construct a new path and push it into the queue
for current_neighbour in graph_to_search.get(vertex, []):
new_path = list(path)
new_path.append(current_neighbour)
queue.append(new_path)
# Mark the vertex as visited
visited.add(vertex)
print bfs(graph, 1, 13)
程序的输出将是:
[1, 4, 7, 11, 13]
没有不必要的重新检查..
【讨论】:
将collections.deque
用于queue
可能很有用,因为list.pop(0) 会导致O(n)
内存移动。此外,为了后代,如果你想做 DFS,只需设置 path = queue.pop()
在这种情况下,变量 queue
实际上就像 stack
。
这将重新访问邻居访问节点的节点,例如在三个节点 1-2-3 的情况下,它会访问 1,将 2 添加到队列中,然后将 1 和 3 添加到队列中。 if vertex not in visited
检查应该在 for 循环中而不是在它之外。然后可以删除外部检查,因为如果节点已被访问,则不会将任何内容添加到队列中。【参考方案4】:
我喜欢@Qiao 第一个答案和@Or 的添加。为了减少处理,我想添加到 Or 的答案。
在@Or 的回答中,跟踪访问的节点很棒。我们还可以让程序比现在更快地退出。在 for 循环中的某个时刻,current_neighbour
必须是 end
,一旦发生这种情况,就会找到最短路径并且程序可以返回。
我会修改方法如下,注意for循环
graph =
1: [2, 3, 4],
2: [5, 6],
3: [10],
4: [7, 8],
5: [9, 10],
7: [11, 12],
11: [13]
def bfs(graph_to_search, start, end):
queue = [[start]]
visited = set()
while queue:
# Gets the first path in the queue
path = queue.pop(0)
# Gets the last node in the path
vertex = path[-1]
# Checks if we got to the end
if vertex == end:
return path
# We check if the current node is already in the visited nodes set in order not to recheck it
elif vertex not in visited:
# enumerate all adjacent nodes, construct a new path and push it into the queue
for current_neighbour in graph_to_search.get(vertex, []):
new_path = list(path)
new_path.append(current_neighbour)
queue.append(new_path)
#No need to visit other neighbour. Return at once
if current_neighbour == end
return new_path;
# Mark the vertex as visited
visited.add(vertex)
print bfs(graph, 1, 13)
输出和其他一切都是一样的。但是,代码将花费更少的时间来处理。这在较大的图表上特别有用。我希望这对将来的某人有所帮助。
【讨论】:
【参考方案5】:非常简单的代码。每次发现节点时,您都会继续附加路径。
graph =
'A': set(['B', 'C']),
'B': set(['A', 'D', 'E']),
'C': set(['A', 'F']),
'D': set(['B']),
'E': set(['B', 'F']),
'F': set(['C', 'E'])
def retunShortestPath(graph, start, end):
queue = [(start,[start])]
visited = set()
while queue:
vertex, path = queue.pop(0)
visited.add(vertex)
for node in graph[vertex]:
if node == end:
return path + [end]
else:
if node not in visited:
visited.add(node)
queue.append((node, path + [node]))
【讨论】:
与其他答案相比,我发现您的代码非常易读。非常感谢!【参考方案6】:如果图中包含循环,这样的方法不是更好吗?
from collections import deque
graph =
1: [2, 3, 4],
2: [5, 6, 3],
3: [10],
4: [7, 8],
5: [9, 10],
7: [11, 12],
11: [13]
def bfs1(graph_to_search, start, end):
queue = deque([start])
visited = start
trace =
while queue:
# Gets the first path in the queue
vertex = queue.popleft()
# Checks if we got to the end
if vertex == end:
break
for neighbour in graph_to_search.get(vertex, []):
# We check if the current neighbour is already in the visited nodes set in order not to re-add it
if neighbour not in visited:
# Mark the vertex as visited
visited.add(neighbour)
trace[neighbour] = vertex
queue.append(neighbour)
path = [end]
while path[-1] != start:
last_node = path[-1]
next_node = trace[last_node]
path.append(next_node)
return path[::-1]
print(bfs1(graph,1, 13))
这样只会访问新节点,而且避免循环。
【讨论】:
以上是关于如何在广度优先搜索中追踪路径?的主要内容,如果未能解决你的问题,请参考以下文章