圖算法(二)最短路

本文將介紹三種常見的最短路算法:Dijkstra解愤,F(xiàn)loyd镇饺,SPFA

Dijkstra

Dijkstra是有向圖上的單源最短路徑算法,本質(zhì)是一種貪心送讲。給定一個有向圖G(V奸笤,E)和起點s,基礎(chǔ)的Dijkstra算法會在O(|V|^2)的時間復(fù)雜度內(nèi)求出從s出發(fā)到所有點的最短路長度哼鬓。

Dijkstra算法要求圖中不能有負權(quán)邊监右,其算法描述如下:

  1. 建立一個空的優(yōu)先隊列Q;
  2. 把所有頂點根據(jù)與s的距離dis[i]插入優(yōu)先隊列异希,其中dis[s]=0健盒,與s不相連的頂點距離設(shè)為INF;
  3. 每次從Q中取出與s距離最近的頂點u,對點u的每條出邊e=(u,v)扣癣,更新dis[v] = min(dis[v]惰帽,dis[u] + l(u, v));
  4. 重復(fù)3操作搏色,直到所有頂點都被取出善茎,結(jié)果中dis[i]代表s到i的最短距離。

如果用數(shù)組維護dis[i]频轿,那么每次查找與s最近的點和更新操作代價都是O(|V|)垂涯,算法整體復(fù)雜度O(|V|^2),這個復(fù)雜度在稠密圖的情況下是很理想的航邢;如果用堆(優(yōu)先隊列)實現(xiàn)耕赘,那么每次查找最近點的代價變成O(lg|V|),在堆中的decrease-key操作是O(lg|V|)的膳殷,算法中最多有|V|次查找和|E|次更新操骡,算法整體復(fù)雜度O((|V| + |E|) lg|V|),在稀疏圖中是一種優(yōu)化赚窃。

正確性證明:
Dijkstra每次取出的點都有唯一的“前驅(qū)”册招,因此我們是可以恢復(fù)出一條從起點到終點的路徑的,我們只要證明這條路徑就是達到目標點的最短路徑勒极。采取歸納法證明:

  1. 除起點s外第一個被取出的點u找不出比s->u更短的路徑是掰。假如有一條更短的路:s->v...->u,由于不存在負權(quán)邊辱匿,那么l(s,v)<l(s,u)键痛,這與Dijkstra每次取dis最短的點矛盾;
  2. 假設(shè)已經(jīng)被取出的點都滿足條件匾七,Dijkstra選中的下一個與s最近的點是v絮短,其前驅(qū)是u(u可能與s重合),那么Dijkstra給出的路徑L1為:s->s1->...->sk->u->v昨忆,其中s1...sk丁频,u都是已經(jīng)被取出的點,其中dis[u]+l(u,v)是最小的邑贴。假設(shè)有一條從s到v的路徑L2長度小于L1限府,那么L2中v的前驅(qū)不能是u(如果是,與歸納假設(shè)矛盾)痢缎,如果不是u胁勺,則只能是一個還沒被取出的點t(如果不是,與剛才dis[u]+l(u,v)的最小性矛盾)独旷,那么此時dis[t]<dis[v]署穗,v不應(yīng)該被從隊列中選取寥裂,矛盾!
dijkstra.jpg

C++實現(xiàn):

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是有向圖上的多源最短路算法案疲,本質(zhì)是動態(tài)規(guī)劃封恰。給定有向圖G(V, E),F(xiàn)loyd算法可以在O(|V|^3)的時間復(fù)雜度內(nèi)算出圖上任意兩點之間的最短距離褐啡。算法描述如下:

  1. 初始化鄰接矩陣dis:i=j時诺舔,dis[i][j] = 0,i != j時备畦,若有從i到j(luò)的有向邊e低飒,dis[i][j]等于e的權(quán)值,若沒有懂盐,dis[i][j] = INF褥赊;
  2. 選取一個新的中間節(jié)點k,對于所有的(i, j)對莉恼,如果dis[i][j] < dis[i][k] + dis[k][j]拌喉,則更新dis[i][j]的值為dis[i][k] + dis[k][j];
  3. 重復(fù)操作2俐银,直到所有的點都成為過中間節(jié)點尿背,結(jié)果中dis[i][j]表示i到j(luò)的最短距離。

正確性證明:
Floyd算法中的中間節(jié)點k可以理解為:從起點i到終點j捶惜,只經(jīng)過1-k這些點可以得到的最短路是多少田藐。算法結(jié)束時dis[i][j]對應(yīng)的就是從頂點i到頂點j只經(jīng)過頂點1-|V|的最短路,就是所求的結(jié)果售躁。所以我們只要證明:前k個中間節(jié)點處理完后坞淮,dis[i][j]的值為從起點i只經(jīng)過1-k中的點到達終點j的最短長度茴晋。用歸納法描述:

  1. k=1時顯然(dis[i][j]要么是i和j的距離陪捷,要么是i到1的距離加上1到j(luò)的距離,且保證是兩者中較短的)
  2. 如果k-1得證诺擅,對于k的情況市袖,從i到j(luò)的最短路要么經(jīng)過k,要么不經(jīng)過k烁涌。經(jīng)過k時苍碟,dis[i][j]會更新為dis[i][k]+dis[k][j],否則dis[i][j]不變撮执。這兩種情況途徑點的標號都不會超過k微峰,而且由歸納假設(shè)可確保是最短的。

C++實現(xiàn):

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]);
}

因為只是一個三重循環(huán)抒钱,F(xiàn)loyd算法一定會終止蜓肆,那么它是否能處理有負邊的情況呢颜凯?答案是可以的,負邊對我們的歸納并沒有影響仗扬。

SPFA

SPFA(Shortest Path Faster Algorithm)是有向圖上的單源最短路算法症概,它最大的亮點是可以處理負邊,而且在大部分情況下運行效率很高早芭。其算法描述如下:

  1. 初始化dis數(shù)組彼城,其中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. 重復(fù)2操作直到隊列為空

正確性證明:
SPFA的正確性是三種算法中最不顯然的。首先證明算法是會終止的照棋,向隊列中加入頂點i要求dis[i]被更新為更小的值资溃,而沒有負環(huán)時,dis[i]是有下界的烈炭,即每個點只會被放入隊列有限次溶锭,每一次循環(huán)都會取出一個頂點,所以沒有負環(huán)時符隙,循環(huán)一定會終止趴捅。而在有負環(huán)的時候,SPFA會陷入死循環(huán)霹疫。
接著證明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算法結(jié)束時隊列為空猎拨,代表沒有松弛操作可以做了。而我們給出一個dis數(shù)組屠阻,它是最短路問題解的充要條件就是不可以再執(zhí)行松弛操作红省。所以SPFA給出的解一定正確的。

時間復(fù)雜度:
段凡丁在提出SPFA的論文中指出SPFA的時間復(fù)雜度是O(|E|)的国觉,但原文的證明很不嚴謹吧恃,關(guān)鍵在于他的一個結(jié)論:平均每個點進入隊列的次數(shù)是一個常數(shù)。我暫時也沒有找到很好的證明麻诀,不過一般認為SPFA的平均時間復(fù)雜度就是O(|E|)的痕寓。值得一說的是缸逃,SPFA在效率上并沒有Dijkstra穩(wěn)定,原因就在于頂點可能多次被加入隊列厂抽,如果很多點都存在“多跳路徑短于少跳路徑”的話需频,SPFA就會變得很慢。

C++實現(xiàn):

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.

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末筷凤,一起剝皮案震驚了整個濱河市昭殉,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌藐守,老刑警劉巖挪丢,帶你破解...
    沈念sama閱讀 218,941評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異卢厂,居然都是意外死亡乾蓬,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,397評論 3 395
  • 文/潘曉璐 我一進店門慎恒,熙熙樓的掌柜王于貴愁眉苦臉地迎上來任内,“玉大人,你說我怎么就攤上這事融柬∷类拢” “怎么了?”我有些...
    開封第一講書人閱讀 165,345評論 0 356
  • 文/不壞的土叔 我叫張陵粒氧,是天一觀的道長越除。 經(jīng)常有香客問我,道長外盯,這世上最難降的妖魔是什么摘盆? 我笑而不...
    開封第一講書人閱讀 58,851評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮饱苟,結(jié)果婚禮上孩擂,老公的妹妹穿的比我還像新娘。我一直安慰自己掷空,他們只是感情好肋殴,可當我...
    茶點故事閱讀 67,868評論 6 392
  • 文/花漫 我一把揭開白布囤锉。 她就那樣靜靜地躺著坦弟,像睡著了一般。 火紅的嫁衣襯著肌膚如雪官地。 梳的紋絲不亂的頭發(fā)上酿傍,一...
    開封第一講書人閱讀 51,688評論 1 305
  • 那天,我揣著相機與錄音驱入,去河邊找鬼赤炒。 笑死氯析,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的莺褒。 我是一名探鬼主播掩缓,決...
    沈念sama閱讀 40,414評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼遵岩!你這毒婦竟也來了你辣?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,319評論 0 276
  • 序言:老撾萬榮一對情侶失蹤尘执,失蹤者是張志新(化名)和其女友劉穎舍哄,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體誊锭,經(jīng)...
    沈念sama閱讀 45,775評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡表悬,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,945評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了丧靡。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片蟆沫。...
    茶點故事閱讀 40,096評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖温治,靈堂內(nèi)的尸體忽然破棺而出饥追,到底是詐尸還是另有隱情,我是刑警寧澤罐盔,帶...
    沈念sama閱讀 35,789評論 5 346
  • 正文 年R本政府宣布但绕,位于F島的核電站,受9級特大地震影響惶看,放射性物質(zhì)發(fā)生泄漏捏顺。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,437評論 3 331
  • 文/蒙蒙 一纬黎、第九天 我趴在偏房一處隱蔽的房頂上張望幅骄。 院中可真熱鬧,春花似錦本今、人聲如沸拆座。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,993評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽挪凑。三九已至,卻和暖如春逛艰,著一層夾襖步出監(jiān)牢的瞬間艾扮,已是汗流浹背址否。 一陣腳步聲響...
    開封第一講書人閱讀 33,107評論 1 271
  • 我被黑心中介騙來泰國打工雄嚣, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人肄渗。 一個月前我還...
    沈念sama閱讀 48,308評論 3 372
  • 正文 我出身青樓,卻偏偏與公主長得像咬最,于是被迫代替她去往敵國和親翎嫡。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 45,037評論 2 355

推薦閱讀更多精彩內(nèi)容