偷一份算法导论 dj 算法的伪代码:
DIJKSTRA(G, w, s)
1 INITIALIZE-SINGLE-SOURCE(G, s)
2 S ← Ø
3 Q ← V[G] //V*O(1)
4 while Q ≠ Ø
5 do u ← EXTRACT-MIN(Q) //EXTRACT-MIN,V*O(V),V*O(lgV)
6 S ← S ∪{u}
7 for each vertex v ∈ Adj[u]
8 do RELAX(u, v, w) //松弛技术,E*O(1),E*O(lgV)。
当仅使用线性查找算法实现 EXTRACT-MIN
时,查找的时间复杂度为 O(V),所以很垃圾,直接导致最后结果出现 O(V^2)
,这不太好。
有些人用最小堆(优先队列)来实现 EXTRACT-MIN
,企图把 O(V^2)
降低到 O(VlogV)
会有一个问题:
当使用松弛技术时,会破坏最小堆的结构。有些人每次 EXTRACT-MIN
之前进行一次建堆操作,但这实际上又引入了 O(V)
的单次查找复杂度,比线性查找只慢不快。
所以我们需要一个算法把最小堆的结构恢复过来。这在 Q
中建立 elem
到 index
的映射,以便每次 RELAX
知道我们碰了谁,然后进行局部恢复,保持 O(logV)
的单次查找复杂度,但是这样的缺点是需要用到优先队列内部封装的结构,而且维护索引的常数开销也很大,特别是 Java PriorityQueue
这种轮子唾手可得时,我们不愿意去自己写一个最小堆(就是懒)
但是换个角度,我们可以这样操作:
RELAX
后,立即将节点插入 Q
中。u = EXTRACT-MIN(Q)
后如果 u
是 visited
,那么跳过它。
这样等价于 “每次松弛后维护了 Q
的结构”,缺点是 Q
会变得庞大起来,每次查找是 O(logE)
,考虑到 E <= V^2
,O(logE) ~ O(logV)
,还不算太坏。循环的次数也会增多,但是如果碰到 visited == true
就会很快进入下一循环,可以认为O(logE)
因子仍然是 O(V)
,因为跳过循环时不进行 O(logE)
的循环内操作。