双向迪杰斯特拉算法Python实现
简介
双向迪杰斯特拉算法(Bi Directional Dijkstra Algorithm)是一种用于在加权图中查找两个顶点之间最短路径的算法,是Dijkstra算法的一个变种,基本思想是:从两个搜索方向同时开始搜索——从起点到终点方向和从终点到起点方向同时进行迪杰斯特拉算法搜索,如果存在路径,那么最终两个方向的搜索会在某点相遇并终止,而这条路径就是最短距离路径。在某些情况下,双向迪杰斯特拉算法可以减少搜索空间大小,从而提高算法效率。其中也有分治法的思想
双向迪杰斯特拉算法优势
与迪杰斯特拉算法相比,双向迪杰斯特拉算法在以下情况更有优势:
-
大规模图搜索:如果图的规模很大,双向迪杰斯特拉算法很可能会比迪杰斯特拉算法更快。
-
稀疏图:在稀疏图中,双向迪杰斯特拉算法很可能会比迪杰斯特拉算法更快。
最主要是因为双向搜索可以减少一定的搜索空间,从终点开始搜索和从起点开始搜索,相当于做了一次剪枝。
(我觉得这张图很形象的解释了为什么双向迪杰斯特拉算法能够减少搜索空间,而单项迪杰斯特拉算法在大规模图中,会越来越发散)
局限性
- 复杂性:双向迪杰斯特拉算法需要更复杂的数据结构来跟踪记录两个方向的搜索路径。算法需要更多时间维护两个方向的队列。
- 权重不均等的图、负权重图:对于边权重差异比较大和负权重的图,双向迪杰斯特拉算法可能不会表现得很好。
- 当终点和起点距离比较近的时候,双向迪杰斯特拉算法算法可能不如单项迪杰斯特拉算法算法。
但是双向迪杰斯特拉还是具有减少搜索空间和更快搜索到最短路径的优点。
算法的基本步骤
终止条件
双向迪杰斯特拉算法最重要的是,终止条件,算法在什么时候应该终止,如何确定相遇的点是应该终止算法的。双向迪杰斯特拉算法需要维护两个队列,一个是从起点到终点方向的队列queue_from_start
和queue_from_target
,设:
t
o
p
f
top_f
topf是queue_from_start
优先级队列的队头元素,
t
o
p
r
top_r
topr是queue_from_target
优先队列的队头元素,
μ
\mu
μ用来记录相遇点构成的路径值,初始化
μ
=
∞
\mu = \infty
μ=∞。在进行路径搜索的时候,当存在一条边
(
u
,
v
)
(u,v)
(u,v)满足
u
u
u在前向搜索中,而
v
v
v在反向搜索中,如果
d
f
(
u
)
+
c
(
u
,
v
)
+
d
r
(
v
)
<
μ
d_f(u)+c(u,v)+d_r(v) < \mu
df(u)+c(u,v)+dr(v)<μ,则更新
μ
\mu
μ值。
终止条件: t o p f + t o p r ≥ μ top_f + top_r \ge \mu topf+topr≥μ
基本步骤
- 初始化:将起点加入到正向搜索的待处理队列中,将终点加入到反向搜索的待处理队列中。
- 迭代搜索:在每一次迭代中,算法分别对正向和反向的待处理队列中的顶点进行处理,选择当前距离最小的顶点进行扩展。
- 扩展顶点:对于选中的顶点,算法更新其邻接顶点的最短路径估计,并将这些邻接顶点加入到相应的待处理集合中。
- 检查相遇:在每次扩展后,算法检查正向和反向的搜索是否在某个顶点上相遇。相遇的条件通常是检查某个顶点是否同时出现在正向和反向的访问表中。
- 路径重构:一旦搜索相遇,算法使用正向和反向的路径信息来重构出从起点到终点的最短路径。
- 终止条件:如果两个搜索在中间某处相遇,或者一个方向的搜索已经找不到新的顶点进行扩展,算法终止。
伪代码
程序结构:
function BidirectionalDijkstra(graph, start, end)
create priority queues queueFromStart, queueFromEnd
add start to queueFromStart with priority 0
add end to queueFromEnd with priority 0
create distance maps distanceFromStart, distanceFromEnd and set all distances to infinity
set distanceFromStart[start] to 0
set distanceFromEnd[end] to 0
create parent maps parentFromStart, parentFromEnd and set all parents to null
set parentFromStart[start] to start
set parentFromEnd[end] to end
while queueFromStart and queueFromEnd are not empty
nodeFromStart = extract minimum from queueFromStart
for each neighbor of nodeFromStart
if distance through nodeFromStart to neighbor is less than distanceFromStart[neighbor]
update distanceFromStart[neighbor]
update parentFromStart[neighbor]
add neighbor to queueFromStart with priority distanceFromStart[neighbor]
nodeFromEnd = extract minimum from queueFromEnd
for each neighbor of nodeFromEnd
if distance through nodeFromEnd to neighbor is less than distanceFromEnd[neighbor]
update distanceFromEnd[neighbor]
update parentFromEnd[neighbor]
add neighbor to queueFromEnd with priority distanceFromEnd[neighbor]
if any node v is in both queueFromStart and queueFromEnd
path = shortest path from start to v according to parentFromStart
path = path + reverse of shortest path from v to end according to parentFromEnd
return path
return no path
Python 实现
def bidirectional_dijkstra_b(graph, start, target, i):
queue_from_start = []
hq.heappush(queue_from_start, (0.0, start))
distance_from_start = {node: float('infinity') for node in graph}
distance_from_start[start] = 0.0
parents_of_start = {start: None}
queue_from_target = []
hq.heappush(queue_from_target, (0.0, target))
distance_from_target = {node: float('infinity') for node in graph}
distance_from_target[target] = 0.0
parents_of_target = {target: None}
close_of_start = set() # 访问禁闭表
close_of_target = set() # 访问禁闭表
miu = math.inf
global node
node = None
while queue_from_start and queue_from_target:
if queue_from_start[0][0] + queue_from_target[0][0] >= miu:
return reverse_traversal(node, parents_of_start, parents_of_target)
cur_dist, cur_node = hq.heappop(queue_from_start)
close_of_start.add(cur_node)
for adjacent, weight in graph[cur_node].items():
if adjacent in close_of_start:
continue
distance = cur_dist + weight["weight"]
if distance < distance_from_start[adjacent]:
distance_from_start[adjacent] = distance
parents_of_start[adjacent] = cur_node
hq.heappush(queue_from_start, (distance, adjacent))
# 更新miu值
if adjacent in close_of_target:
dist = distance + distance_from_target[adjacent]
if miu > dist:
miu = dist
node = adjacent
cur_dist, cur_node = hq.heappop(queue_from_target)
close_of_target.add(cur_node)
for adjacent, weight in graph[cur_node].items():
if adjacent in close_of_target:
continue
distance = cur_dist + weight["weight"]
if distance < distance_from_target[adjacent]:
distance_from_target[adjacent] = distance
parents_of_target[adjacent] = cur_node
hq.heappush(queue_from_target, (distance, adjacent))
if adjacent in close_of_start:
dist = distance + distance_from_start[adjacent]
if miu > dist:
miu = dist
node = adjacent
return []
双向迪杰斯特拉与单向迪杰斯特拉算法比较
双向迪杰斯特拉算法写了两个版本,主要是控制方式不同。
实验中生成稀疏图标准是, e = n ∗ log n e=n*\log{n} e=n∗logn
可以看出,随着节点和边的数量增多,算法的耗时越大,在节点数=50000,边数=252959时,迪杰斯特拉算法的速度远远高于双向迪杰斯特拉算法。
从上图来看,在50,500数量级的节点数下,第二种控制流的迪杰斯特拉算法更快,但是随着节点数数量级增大,第一种控制流双向迪杰斯特拉算法更优。推测结论:双向迪杰斯特拉算法是内含分治法思想的,如果两侧探索数量越均等越好。