圖算法(二)最短路

本文將介紹三種常見的最短路算法:Dijkstra,Floyd,SPFA

Dijkstra

Dijkstra是有向圖上的單源最短路徑算法,本質是一種貪心。給定一個有向圖G(V,E)和起點s,基礎的Dijkstra算法會在O(|V|^2)的時間復雜度內求出從s出發到所有點的最短路長度。

Dijkstra算法要求圖中不能有負權邊,其算法描述如下:

  1. 建立一個空的優先隊列Q;
  2. 把所有頂點根據與s的距離dis[i]插入優先隊列,其中dis[s]=0,與s不相連的頂點距離設為INF;
  3. 每次從Q中取出與s距離最近的頂點u,對點u的每條出邊e=(u,v),更新dis[v] = min(dis[v],dis[u] + l(u, v));
  4. 重復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每次取出的點都有唯一的“前驅”,因此我們是可以恢復出一條從起點到終點的路徑的,我們只要證明這條路徑就是達到目標點的最短路徑。采取歸納法證明:

  1. 除起點s外第一個被取出的點u找不出比s->u更短的路徑。假如有一條更短的路:s->v...->u,由于不存在負權邊,那么l(s,v)<l(s,u),這與Dijkstra每次取dis最短的點矛盾;
  2. 假設已經被取出的點都滿足條件,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不應該被從隊列中選取,矛盾!
dijkstra.jpg

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)的時間復雜度內算出圖上任意兩點之間的最短距離。算法描述如下:

  1. 初始化鄰接矩陣dis:i=j時,dis[i][j] = 0,i != j時,若有從i到j的有向邊e,dis[i][j]等于e的權值,若沒有,dis[i][j] = INF;
  2. 選取一個新的中間節點k,對于所有的(i, j)對,如果dis[i][j] < dis[i][k] + dis[k][j],則更新dis[i][j]的值為dis[i][k] + dis[k][j];
  3. 重復操作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的最短長度。用歸納法描述:

  1. k=1時顯然(dis[i][j]要么是i和j的距離,要么是i到1的距離加上1到j的距離,且保證是兩者中較短的)
  2. 如果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)是有向圖上的單源最短路算法,它最大的亮點是可以處理負邊,而且在大部分情況下運行效率很高。其算法描述如下:

  1. 初始化dis數組,其中dis[s]為0,其他值為INF,初始化一個隊列,其中只有s一個元素;
  2. 從隊列中取出第一個元素hd,對hd的每一條出邊e(hd, i),更新dis[i] = min(dis[i], dis[hd] + weight[hd][i]),如果dis[i]被更新(更新有時被稱為松弛操作)了且i不在隊列中,那么把i加入隊列末尾;
  3. 重復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: if u!=v, then relaxation is not possible; relaxation is possible from u=v, and this is added to the queue immediately before the while loop is entered. Now, consider what happens inside the loop. A vertex u 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 by u might cause some other vertices to become capable of causing relaxation. If there exists some x such that dis[x] > dis[w] + weight[w][x] before the current loop iteration, then w is already in the queue. If this condition becomes true during the current loop iteration, then either dis[x] increased, which is impossible, or dis[w] decreased, implying that w was relaxed. But after w 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.

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,117評論 6 537
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,860評論 3 423
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,128評論 0 381
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,291評論 1 315
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,025評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,421評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,477評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,642評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,177評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,970評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,157評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,717評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,410評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,821評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,053評論 1 289
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,896評論 3 395
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,157評論 2 375

推薦閱讀更多精彩內容