Dijkstra算法解决的是带权重的有向图上单源最短路径问题,该算法要求所有边的权重都为非负值。
Dijstra算法在运行过程中维持的关键信息是一组结点集合 S
。从源结点 s
到该集合中每个结点之间的最短路径已经被找到。算法重复从结点集 V-S
中选择最短路径估计最小的结点 u
,将 u
加入到集合 S
,然后对所有从 u
发出的边进行松弛。在下面给出的实现方式中,我们使用一个最小优先队列 Q
来保存结点集合,每个结点的关键值为其 d
值。
INITIALIZE-SINGLE-SOURCE(G,s): //图结点属性初始化
for each vertex v in G.V:
v.d = INF // 源结点s到结点v的距离v.d初始化为无穷大
v.p = NIL // 结点v的前驱结点初始为空
s.d = 0 // 源结点s的s.d的值初始化为0
RELAX(u,v,w): //松弛操作
if v.d > u.d+w(u,v): //如果一条经过结点u的路径能够使得从源结点s到结点v的最短路径
v.d = u.d + w(u,v) //权重比当前的估计值v.d更小,则我们对v.d的值和前驱v.p的值进行更新。
v.p = u
DIJKSTRA.(G,w,s):
INITIALIZE-SINGLE-SOURCE(G,s)
S = empty
Q = G.V
while Q not empty:
u = EXTRACT-MIN(Q)
S = S U { u }
for each vertex v in G.Adj[u]:
RELAX(u,v,w)
算法第2行执行的是例行的d
值和p
值的初始化,第3行将集合S
初始化为一个空集。算法第4行对最小优先队列Q
进行初始化,将所有的结点V都放在该队列里。算法在每次执行5~9行的while
循环时,第6行从Q
队列中抽取结点u
,第7行将该结点加入到集合S
里。然后,在算法的8~9行,我们对所有结点u
发出的边(u,v)
进行松弛操作。如果一条经过结点u
的路径能够使得从源结点s
到结点v
的最短路径权重比当前的估计值v.d
更小,则我们对v.d
的值和前驱v.p
的值进行更新。注意,在算法的第3行之后,我们再不会在队列Q
中插入任何结点,而每个结点从Q
中被抽取的次数和加入集合S的次数均为一次,因此,算法第5~9行的while
循环的执行次数刚好为|V|
次,而第8~9行的for
循环的执行次数则为图的边数|E|
。
该算法执行了三种优先队列操作来维持最小优先队列:INSERT
(算法第4行所隐含的操作)、EXTRACT-MIN
(算法第6行)和DECREASE-KEY
(隐含在算法第9行所调用的RELAX操作中)。所以,Dijkstra
算法的总运行时间依赖于最小优先队列的实现。INSERT
操作执行了1次,EXTRACT-MIN
操作执行了|V|
次,而DECREASE-KEY
操作执行了|E|
次。这里考虑两种最小优先队列实现方式进行复杂度分析:二叉堆和斐波那契堆。
二叉堆(O((V+E)lgV)
):每次EXTRACT-MIN
操作的执行时间为O(lgV)
,一共有|V|
次这样的操作。INSERT
操作的成本为O(V)
。每次DECREASE-KEY
的操作的执行时间为O(lgV)
,而最多有|E|
次这样的操作。
斐波那契堆(O(VlgV+E)
):每次EXTRACT-MIN
操作的摊还代价为O(lgV)
,每次DECREASE-KEY
操作的摊还代价为O(1)
。从历史的角度上看,斐波那契堆提出的动机就是因为人们观察到Dijkstra
算法调用的DECREASE-KEY
操作通常比EXTRACT-MIN
操作更多,因此任何能够次DECREASE-KEY
操作的摊还代价降低到O(lgV)
而不增加EXTRACT-MIN
操作的摊还代价的方法都将产生比二叉堆的渐近性能更优的实现。