最小生成樹(MST)問題
- 對象: 該問題總是針對連通無向圖G = (V, E);
- 總體算法
- 這個算法理出大概的思路, 真正實現還分為點和邊兩種方式;
Generic-MST(G, w) //w是權重函數
A = ?
while A does not form a spanning tree
find an edge(u,v) that is safe for A
A = A∪(u,v)
return A //the minimum spanning tree
- 總體性質: A是圖G=(V, E)中E的在SpanningTree中的邊的集合, 如果(u,v)是橫跨切割(S, V-S)的輕量級邊, 其中S是SpanningTree的意思, 那么邊(u, v)對于集合A是安全的;
證明: (分類討論)
--如果(u, v)在STree中, 那么命題得證(安全邊才能被收入A中);
--如果(u,v)不在STree中, 因為u,v兩個點都在STree中, 那么必然在A中已經通過某種路徑相連, 這條路徑加上(u,v)邊會形成一個環路, 且此時至少有兩條邊跨越了切割, 一個是(u, v), 另外一個不妨寫作(x, y). 若斷掉(x,y)留下(u, v) 則此時T'.w = T.w + w(u, v) - w(x, y), 因為(u, v)是輕量級邊, 所以T'.w <= T.w, 所以說(u,v)邊是安全的;
prim算法
- 思路: 并點, 不斷找新生成樹的切割面的最短邊, 是典型的貪心算法;
MST-Prim(G, w, r) //Graph, weight, root
for each u ∈ G.V
u.key = ∞
u.parent = null
r.key = 0
Q = G.V //initiate a priority queue, 初始化結束
while Q != ?:
u = Extract-Min(Q) //依據的是u.key是Q中最小的;
for each v∈G.Adj[u]:
if v∈Q and w(u, v)<v.key:
v.parent = u
v.key = w(u, v)
Note: 獲取MST的時候, 只要讓每個非root的結點u輸出(u, u.parent)邊;
- 時間復雜度分析:
(1) 使用二維數組W存儲weight, 一維數組V存儲結點: 初始化操作Θ(V);Extra-Min操作V次, 每次耗時Θ(V), 所以這個環節是Θ(V^2); 對邊的操作, 總共有E條邊, 每次耗時都是Θ(1), 因此這個環節Θ(E); 總體耗時因此是Θ(V^2+E);
(2) 使用二叉堆構建最小優先隊列, Extract-Min耗時總共O(VlgV), 對邊的操作需要做E次Decrease-Key, 耗時總共O(ElgV); 總體耗時因此為O((V+E)lgV), 因為E>=V-1總是成立, 因此可以寫作O(ElgV);
(3) 使用斐波那契堆實現最小優先隊列Q的話, Extract-Min時間仍是lgV, 但是Decrease-Key下降到O(1), 因此總體上時間是O(VlgV+E), 小于O(ElgV).
Kruskal算法
- 思路: 并邊, 不斷找整個圖中最短邊, 只要不構成環, 即相互不連通, 就并入, 也是貪心算法;.
MST-Kruskal(G, w)
A = ?
for each u ∈ G.V
Make-Set(u)
sort the edges of G.E as ascending order by weight w //到此初始化完成
for edge(u, v) of the sorted edge sequence
if Find-Set(u) != Find-Set(v):
A = A ∪ { (u,v) } //add edge to A;
Union(u, v) //combine two trees
return A //A is the tree;
- 時間復雜度分析: 排序消耗了O(ElgE), 在改進過的并查集中, Find-Set總共做了E次, 消耗O(E), 而Union總共做了V次, 消耗O(Vα(V)), 其中α(V)<=4, 可以看作O(lgV), 因此Union消耗了O(VlgV), 因此并邊循環總共消耗O(E+VlgV), 顯然dominator是排序操作, 因此總體來說 T = O(ElgE).
網路最短路徑問題
- 對象: 帶權重的有向圖G = (V, E); 注意和最小生成樹的主要區別在于這是有向圖!!!
共用方法
- 初始化
Initialize-Single-Source(G, s)
for each vertex v ∈ G.V:
v.d = ∞
v.parent = null
s.d = 0
- 松弛操作
Relax(u, v, w)
if v.d > u.d+w(u,v):
v.d = u.d+w(u,v)
v.parent = u
松弛定理: 對v點, 如果從s點到vi確實存在一條最短路徑, 那么只要(s, v1), (v1, v2), ... (vi-1, vi)都被依次執行了relax操作, 那么vi.d = δ(s, vi). 該性質的成立與其他穿插在該序列中間的松弛操作無關.
- 也就是說, 接下去的三個算法, 我們都要檢查松弛定理是否能運用, 只要能運用, 算法是正確的;
1. 一般情況下的單源最短路徑問題
- 條件寬松: 允許出現負權重的環路;
Bellman-Ford(G, w, s) {
Initialize-Single-Source(G, s)
for i = 1~|V|-1:
for each edge(u, v) ∈ G.E:
relax(u, v)
for each edge(u, v) ∈ G.E:
if v.d >u.d+w(u, v): //只要任何一條邊仍能繼續松弛, 那么說明有負權重的環路存在;
return FALSE
return TRUE
}
正確性:
(1) 對某個點vi來說, 從s如果有一條最短路徑, 那么在算法的第一個for循環, 到第i次循環, 該最短路徑上的邊(s, v1), (v1, v2)...(vi-1,vi)都將被松弛, 因此該算法能滿足松弛定理, 使得vi.d = δ(s, vi);
(2) 最短路徑上界定理: 對所有的最短路徑來說, 最多只能有V-1條邊, 否則將形成一個環. 因此Bellman-Ford算法只需要V-1輪全體relax就能cover到即使是最長的那條最短路徑.
- 時間復雜度分析: 主要時間消耗在V-1次輪的全體relax, 一共消耗了O(VE);
2. 有向無環圖中的單源最短路徑問題
- 條件收緊: 允許使用負權重的邊;
- 借助拓撲序
DAG-Shortest-Path(G, w, s) {
topologically sort the vertices of G
Initialize-Single-Source(G, s)
for each vertex u taken by the sorted order:
for each v of G.adj[u]:
Relax(u, v, w)
}
正確性: 因為是按照拓撲序對所有的邊進行松弛, 因此最短路徑上的邊(s, v1), (v1, v2)...(vi-1,vi)的邊, 都會被依次松弛, 盡管中間可能穿插其他的松弛, 最后必然能實現vi.d = δ(s, vi)
迪杰斯特拉算法
- 條件進一步收緊: 只允許非負權重的邊;
- 思路:
對每個節點賦值∞, 每次都并一個距離最小的結點, 同時更新該結點的adj結點, 不斷迭代;
Dijkstra(G, w, s) {
Initialize-Single-Source(G, s)
S = ?
Q = G.V //初始化完成
while Q != ?:
u = Extract-Min(Q)
S = S∪{u} //add u to S;
for each vertex v of G.adj[u]:
Relax(u, v, w)
}
正確性: Dij算法每次收入的點是當前Q中(尚未被染色的)滿足d最小的點u, u.d此時已經滿足被所有S中能到v的點給更新了. 那么為什么不擔心之后加入S的點可能還會再減小v.d呢? 這是因為之后再加S的點, 必須經過之前的S的點, 這些后加點的v.d只可能在已有能連到它的點的基礎上遞增, 因為Dij算法要求圖中沒有負權重邊.
- 時間復雜度分析:
(1) 使用二維數組W存儲weight, 一維數組V存儲結點: 初始化操作Θ(V);Extra-Min操作V次, 每次耗時Θ(V), 所以這個環節是Θ(V^2); 對邊的操作, 總共有E條邊, 每次耗時都是Θ(1), 因此這個環節Θ(E); 總體耗時因此是Θ(V^2+E); (注意到V-1<=E<=V^2, 因此可以寫成Θ(V^2)).
(2) 使用二叉堆構建最小優先隊列, Extract-Min耗時總共O(VlgV), 對邊的操作需要做E次Decrease-Key, 耗時總共O(ElgV); 總體耗時因此為O((V+E)lgV), 因為E>=V-1總是成立, 因此可以寫作O(ElgV);
(3) 使用斐波那契堆實現最小優先隊列Q的話, Extract-Min時間仍是lgV, 但是Decrease-Key下降到O(1), 因此總體上時間是O(VlgV+E), 小于O(ElgV).