本文將介紹三種常見的最短路算法:Dijkstra,Floyd,SPFA
Dijkstra
Dijkstra是有向圖上的單源最短路徑算法,本質是一種貪心。給定一個有向圖G(V,E)和起點s,基礎的Dijkstra算法會在O(|V|^2)的時間復雜度內求出從s出發到所有點的最短路長度。
Dijkstra算法要求圖中不能有負權邊,其算法描述如下:
- 建立一個空的優先隊列Q;
- 把所有頂點根據與s的距離dis[i]插入優先隊列,其中dis[s]=0,與s不相連的頂點距離設為INF;
- 每次從Q中取出與s距離最近的頂點u,對點u的每條出邊e=(u,v),更新dis[v] = min(dis[v],dis[u] + l(u, v));
- 重復3操作,直到所有頂點都被取出,結果中dis[i]代表s到i的最短距離。
如果用數組維護dis[i],那么每次查找與s最近的點和更新操作代價都是O(|V|),算法整體復雜度O(|V|^2),這個復雜度在稠密圖的情況下是很理想的;如果用堆(優先隊列)實現,那么每次查找最近點的代價變成O(lg|V|),在堆中的decrease-key操作是O(lg|V|)的,算法中最多有|V|次查找和|E|次更新,算法整體復雜度O((|V| + |E|) lg|V|),在稀疏圖中是一種優化。
正確性證明:
Dijkstra每次取出的點都有唯一的“前驅”,因此我們是可以恢復出一條從起點到終點的路徑的,我們只要證明這條路徑就是達到目標點的最短路徑。采取歸納法證明:
- 除起點s外第一個被取出的點u找不出比s->u更短的路徑。假如有一條更短的路:s->v...->u,由于不存在負權邊,那么l(s,v)<l(s,u),這與Dijkstra每次取dis最短的點矛盾;
- 假設已經被取出的點都滿足條件,Dijkstra選中的下一個與s最近的點是v,其前驅是u(u可能與s重合),那么Dijkstra給出的路徑L1為:s->s1->...->sk->u->v,其中s1...sk,u都是已經被取出的點,其中dis[u]+l(u,v)是最小的。假設有一條從s到v的路徑L2長度小于L1,那么L2中v的前驅不能是u(如果是,與歸納假設矛盾),如果不是u,則只能是一個還沒被取出的點t(如果不是,與剛才dis[u]+l(u,v)的最小性矛盾),那么此時dis[t]<dis[v],v不應該被從隊列中選取,矛盾!
C++實現:
void dijkstra(int s) { // s: starting vertix
std::priority_queue<Node, std::vector<Node>, std::greater<Node> > q;
for(int i = 0; i < n; i++)
dis[i] = INF;
dis[s] = 0;
q.push(std::make_pair(0, s));
for(int i = 0; i < n; i++) {
while(!q.empty() && q.top().first > d[q.top().second])
q.pop();
// the graph may be unconnected
if(q.empty()) break;
int hd = q.top().second;
q.pop();
for(Edge *p = e[hd]; p; p = p->next)
if(dis[hd] + p->len < dis[p->dst])
q.push(std::make_pair(dis[p->dst] = p->len + dis[hd], p->dst));
}
}
Floyd
Floyd是有向圖上的多源最短路算法,本質是動態規劃。給定有向圖G(V, E),Floyd算法可以在O(|V|^3)的時間復雜度內算出圖上任意兩點之間的最短距離。算法描述如下:
- 初始化鄰接矩陣dis:i=j時,dis[i][j] = 0,i != j時,若有從i到j的有向邊e,dis[i][j]等于e的權值,若沒有,dis[i][j] = INF;
- 選取一個新的中間節點k,對于所有的(i, j)對,如果dis[i][j] < dis[i][k] + dis[k][j],則更新dis[i][j]的值為dis[i][k] + dis[k][j];
- 重復操作2,直到所有的點都成為過中間節點,結果中dis[i][j]表示i到j的最短距離。
正確性證明:
Floyd算法中的中間節點k可以理解為:從起點i到終點j,只經過1-k這些點可以得到的最短路是多少。算法結束時dis[i][j]對應的就是從頂點i到頂點j只經過頂點1-|V|的最短路,就是所求的結果。所以我們只要證明:前k個中間節點處理完后,dis[i][j]的值為從起點i只經過1-k中的點到達終點j的最短長度。用歸納法描述:
- k=1時顯然(dis[i][j]要么是i和j的距離,要么是i到1的距離加上1到j的距離,且保證是兩者中較短的)
- 如果k-1得證,對于k的情況,從i到j的最短路要么經過k,要么不經過k。經過k時,dis[i][j]會更新為dis[i][k]+dis[k][j],否則dis[i][j]不變。這兩種情況途徑點的標號都不會超過k,而且由歸納假設可確保是最短的。
C++實現:
void floyd() {
// suppose dis matrix is intialized
for(int k = 1; k <= n; k++)
for(int i = 1; i <= n; i++)
for(int j = 1; j <= n; j++)
dis[i][j] = std::min(dis[i][j], dis[i][k] + dis[j][k]);
}
因為只是一個三重循環,Floyd算法一定會終止,那么它是否能處理有負邊的情況呢?答案是可以的,負邊對我們的歸納并沒有影響。
SPFA
SPFA(Shortest Path Faster Algorithm)是有向圖上的單源最短路算法,它最大的亮點是可以處理負邊,而且在大部分情況下運行效率很高。其算法描述如下:
- 初始化dis數組,其中dis[s]為0,其他值為INF,初始化一個隊列,其中只有s一個元素;
- 從隊列中取出第一個元素hd,對hd的每一條出邊e(hd, i),更新dis[i] = min(dis[i], dis[hd] + weight[hd][i]),如果dis[i]被更新(更新有時被稱為松弛操作)了且i不在隊列中,那么把i加入隊列末尾;
- 重復2操作直到隊列為空
正確性證明:
SPFA的正確性是三種算法中最不顯然的。首先證明算法是會終止的,向隊列中加入頂點i要求dis[i]被更新為更小的值,而沒有負環時,dis[i]是有下界的,即每個點只會被放入隊列有限次,每一次循環都會取出一個頂點,所以沒有負環時,循環一定會終止。而在有負環的時候,SPFA會陷入死循環。
接著證明SPFA所得的dis[i]就是從起點s到終點i的最短路,先證明一個引理:每次檢查隊列是否為空時,所有能引起松弛操作的點都在隊列中。
We want to show that if
dis[w] > dis[u] + weight[u][w]
at the time the condition is checked,u
is in the queue. We do so by induction on the number of iterations of the loop that have already occurred. First we note that this certainly holds before the loop is entered: ifu!=v
, then relaxation is not possible; relaxation is possible fromu=v
, and this is added to the queue immediately before the while loop is entered. Now, consider what happens inside the loop. A vertexu
is popped, and is used to relax all its neighbors, if possible. Therefore, immediately after that iteration of the loop,u
is not capable of causing any more relaxations (and does not have to be in the queue anymore). However, the relaxation byu
might cause some other vertices to become capable of causing relaxation. If there exists somex
such thatdis[x] > dis[w] + weight[w][x]
before the current loop iteration, thenw
is already in the queue. If this condition becomes true during the current loop iteration, then eitherdis[x]
increased, which is impossible, ordis[w]
decreased, implying thatw
was relaxed. But afterw
is relaxed, it is added to the queue if it is not already present.
SPFA算法結束時隊列為空,代表沒有松弛操作可以做了。而我們給出一個dis數組,它是最短路問題解的充要條件就是不可以再執行松弛操作。所以SPFA給出的解一定正確的。
時間復雜度:
段凡丁在提出SPFA的論文中指出SPFA的時間復雜度是O(|E|)的,但原文的證明很不嚴謹,關鍵在于他的一個結論:平均每個點進入隊列的次數是一個常數。我暫時也沒有找到很好的證明,不過一般認為SPFA的平均時間復雜度就是O(|E|)的。值得一說的是,SPFA在效率上并沒有Dijkstra穩定,原因就在于頂點可能多次被加入隊列,如果很多點都存在“多跳路徑短于少跳路徑”的話,SPFA就會變得很慢。
C++實現:
void spfa(int s) {
std::queue<int> q;
for(int i = 0; i < n; i++)
dis[i] = INF;
dis[s] = 0;
q.push(s); vis[s] = true;
while(!q.empty()) {
int hd = q.front();
q.pop(); vis[hd] = false;
for(Edge *p = e[hd]; p; p = p->next)
if(dis[hd] + p->len < dis[p->dst]) {
dis[p->dst] = dis[hd] + p->len;
if(!vis[p->dst])
q.push(p->dst), vis[p->dst] = true;
}
}
}
附
本文圖片來自 Lecture Slides for Alogorithm Design by Jon Kleinberg and éva Tardos.