[數據結構拾遺]圖的最短路徑算法

前言

本專題旨在快速瞭解常見的數據結構和算法。html

在須要使用到相應算法時,可以幫助你回憶出經常使用的實現方案而且知曉其優缺點和適用環境。並不涉及十分具體的實現細節描述。java

圖的最短路徑算法

最短路徑問題是圖論研究中的一個經典算法問題,旨在尋找圖(由結點和路徑組成的)中兩結點之間的最短路徑。git

算法具體的形式包括:github

  • 肯定起點的最短路徑問題:即已知起始結點,求最短路徑的問題。適合使用Dijkstra算法。
  • 肯定終點的最短路徑問題:與肯定起點的問題相反,該問題是已知終結結點,求最短路徑的問題。在無向圖中該問題與肯定起點的問題徹底等同,在有向圖中該問題等同於把全部路徑方向反轉的肯定起點的問題。
  • 肯定起點終點的最短路徑問題:即已知起點和終點,求兩結點之間的最短路徑。
  • 全局最短路徑問題:求圖中全部的最短路徑。適合使用Floyd-Warshall算法。

主要介紹如下幾種算法:算法

  • Dijkstra最短路算法(單源最短路)
  • Bellman–Ford算法(解決負權邊問題)
  • SPFA算法(Bellman-Ford算法改進版本)
  • Floyd最短路算法(全局/多源最短路)

經常使用算法

Dijkstra最短路算法(單源最短路)

圖片例子和史料來自:blog.51cto.com/ahalei/1387…編程

算法介紹:後端

迪科斯徹算法使用了廣度優先搜索解決賦權有向圖或者無向圖的單源最短路徑問題,算法最終獲得一個最短路徑樹。該算法經常使用於路由算法或者做爲其餘圖算法的一個子模塊。數組

指定一個起始點(源點)到其他各個頂點的最短路徑,也叫作「單源最短路徑」。例如求下圖中的1號頂點到二、三、四、五、6號頂點的最短路徑。安全

使用二維數組e來存儲頂點之間邊的關係,初始值以下。bash

咱們還須要用一個一維數組dis來存儲1號頂點到其他各個頂點的初始路程,以下。

將此時dis數組中的值稱爲最短路的「估計值」。

既然是求1號頂點到其他各個頂點的最短路程,那就先找一個離1號頂點最近的頂點。經過數組dis可知當前離1號頂點最近是2號頂點。當選擇了2號頂點後,dis[2]的值就已經從「估計值」變爲了「肯定值」,即1號頂點到2號頂點的最短路程就是當前dis[2]值。

既然選了2號頂點,接下來再來看2號頂點有哪些出邊呢。有2->3和2->4這兩條邊。先討論經過2->3這條邊可否讓1號頂點到3號頂點的路程變短。也就是說如今來比較dis[3]和dis[2]+e[2][3]的大小。其中dis[3]表示1號頂點到3號頂點的路程。dis[2]+e[2][3]中dis[2]表示1號頂點到2號頂點的路程,e[2][3]表示2->3這條邊。因此dis[2]+e[2][3]就表示從1號頂點先到2號頂點,再經過2->3這條邊,到達3號頂點的路程。

這個過程有個專業術語叫作「鬆弛」。鬆弛完畢以後dis數組爲:

接下來,繼續在剩下的三、四、5和6號頂點中, 選出離1號頂點最近的頂點4,變爲肯定值,以此類推。

最終dis數組以下,這即是1號頂點到其他各個頂點的最短路徑。

核心代碼:

//Dijkstra算法核心語句
    for(i=1;i<=n-1;i++)
    {
        //找到離1號頂點最近的頂點
        min=inf;
        for(j=1;j<=n;j++)
        {
            if(book[j]==0 && dis[j]<min)
            {
                min=dis[j];
                u=j;
            }
        }
        book[u]=1;
        for(v=1;v<=n;v++)
        {
            if(e[u][v]<inf)
            {
                if(dis[v]>dis[u]+e[u][v])
                    dis[v]=dis[u]+e[u][v];
            }
        }
    }
複製代碼

關於複雜度:

  • M:邊的數量
  • N:節點數量

經過上面的代碼咱們能夠看出,咱們實現的Dijkstra最短路算法的時間複雜度是O(N^2)。其中每次找到離1號頂點最近的頂點的時間複雜度是O(N)

優化:

  • 這裏咱們能夠用「堆」(之後再說)來優化,使得這一部分的時間複雜度下降到O(logN)

  • 另外對於邊數M少於N^2的稀疏圖來講(咱們把M遠小於N^2的圖稱爲稀疏圖,而M相對較大的圖稱爲稠密圖),咱們能夠用鄰接表來代替鄰接矩陣,使得整個時間複雜度優化到O((M+N)logN)

  • 請注意!在最壞的狀況下M就是N^2,這樣的話MlogN要比N^2還要大。可是大多數狀況下並不會有那麼多邊,所以(M+N)logN要比N^2小不少。

Dijkstra思想總結:

dijkstra算法本質上算是貪心的思想,每次在剩餘節點中找到離起點最近的節點放到隊列中,並用來更新剩下的節點的距離,再將它標記上表示已經找到到它的最短路徑,之後不用更新它了。這樣作的緣由是到一個節點的最短路徑必然會通過比它離起點更近的節點,而若是一個節點的當前距離值比任何剩餘節點都小,那麼當前的距離值必定是最小的。(剩餘節點的距離值只能用當前剩餘節點來更新,由於求出了最短路的節點以前已經更新過了)

dijkstra就是這樣不斷從剩餘節點中拿出一個能夠肯定最短路徑的節點最終求得從起點到每一個節點的最短距離。

用鄰接表代替鄰接矩陣存儲

參考:blog.51cto.com/ahalei/1391…

總結以下:

能夠發現使用鄰接表來存儲圖的時間空間複雜度是O(M),遍歷每一條邊的時間複雜度是也是O(M)。若是一個圖是稀疏圖的話,M要遠小於N^2。所以稀疏圖選用鄰接表來存儲要比鄰接矩陣來存儲要好不少。

Bellman–Ford算法(解決負權邊問題)

思想:

bellman-ford算法進行n-1次更新(一次更新是指用全部節點進行一次鬆弛操做)來找到到全部節點的單源最短路。

bellman-ford算法和dijkstra其實有點類似,該算法可以保證每更新一次都能肯定一個節點的最短路,但與dijkstra不一樣的是,並不知道是那個節點的最短路被肯定了,只是知道比上次多肯定一個,這樣進行n-1次更新後全部節點的最短路都肯定了(源點的距離原本就是肯定的)。

如今來講明爲何每次更新都能多找到一個能肯定最短路的節點:

1.將全部節點分爲兩類:已知最短距離的節點和剩餘節點。

2.這兩類節點知足這樣的性質:已知最短距離的節點的最短距離值都比剩餘節點的最短路值小。(這一點也和dijkstra同樣)

3.有了上面兩點說明,易知到剩餘節點的路徑必定會通過已知節點

4.而從已知節點連到剩餘節點的全部邊中的最小的那個邊,這條邊所更新後的剩餘節點就必定是肯定的最短距離,從而就多找到了一個能肯定最短距離的節點,不用知道它究竟是哪一個節點。
複製代碼

bellman-ford的一個優點是能夠用來判斷是否存在負環,在不存在負環的狀況下,進行了n-1次全部邊的更新操做後每一個節點的最短距離都肯定了,再用全部邊去更新一次不會改變結果。而若是存在負環,最後再更新一次會改變結果。緣由是以前是假定了起點的最短距離是肯定的而且是最短的,而又負環的狀況下這個假設再也不成立。

Bellman-Ford 算法描述:

  • 建立源頂點 v 到圖中全部頂點的距離的集合 distSet,爲圖中的全部頂點指定一個距離值,初始均爲 Infinite,源頂點距離爲 0;
  • 計算最短路徑,執行 V - 1 次遍歷;
    • 對於圖中的每條邊:若是起點 u 的距離 d 加上邊的權值 w 小於終點 v 的距離 d,則更新終點 v 的距離值 d;
  • 檢測圖中是否有負權邊造成了環,遍歷圖中的全部邊,計算 u 至 v 的距離,若是對於 v 存在更小的距離,則說明存在環;

僞代碼:

BELLMAN-FORD(G, w, s)
  INITIALIZE-SINGLE-SOURCE(G, s)
  for i  1 to |V[G]| - 1
       do for each edge (u, v)  E[G]
            do RELAX(u, v, w)
  for each edge (u, v)  E[G]
       do if d[v] > d[u] + w(u, v)
            then return FALSE
  return TRUE
複製代碼

SPFA(Bellman-Ford算法改進版本)

SPFA算法是1994年西安交通大學段凡丁提出

spfa能夠當作是bellman-ford的隊列優化版本,正如在前面講到的,bellman每一輪用全部邊來進行鬆弛操做能夠多肯定一個點的最短路徑,可是用每次都把全部邊拿來鬆弛太浪費了,不難發現,只有那些已經肯定了最短路徑的點所連出去的邊纔是有效的,由於新肯定的點必定要先經過已知(最短路徑的)節點。

因此咱們只須要把已知節點連出去的邊用來鬆弛就好了,可是問題是咱們並不知道哪些點是已知節點,不過咱們能夠放寬一下條件,找哪些多是已知節點的點,也就是以前鬆弛後更新的點,已知節點必然在這些點中。 因此spfa的作法就是把每次更新了的點放到隊列中記錄下來。

僞代碼:

ProcedureSPFA;
Begin
    initialize-single-source(G,s);
    initialize-queue(Q);
    enqueue(Q,s);
    while not empty(Q) do begin
        u:=dequeue(Q);
        for each v∈adj[u] do begin
            tmp:=d[v];
            relax(u,v);
            if(tmp<>d[v])and(not v in Q)then enqueue(Q,v);
        end;
    end;
End; 
複製代碼

如何看待 SPFA 算法已死這種說法?

來自:www.zhihu.com/question/29…

在非負邊權的圖中,隨手卡 SPFA 已經是業界常識。在負邊權的圖中,不把 SPFA 卡到最慢就設定時限是很是不負責任的行爲,而卡到最慢就意味着 SPFA 和傳統 Bellman Ford 算法的時間效率相似,然後者的實現難度遠低於前者。

Floyd最短路算法(全局/多源最短路)

圖片例子和史料來自:www.cnblogs.com/ahalei/p/36…

此算法由Robert W. Floyd(羅伯特·弗洛伊德)於1962年發表在「Communications of the ACM」上。同年Stephen Warshall(史蒂芬·沃舍爾)也獨立發表了這個算法。Robert W.Floyd這個牛人是朵奇葩,他本來在芝加哥大學讀的文學,可是由於當時美國經濟不太景氣,找工做比較困難,無奈之下到西屋電氣公司當了一名計算機操做員,在IBM650機房值夜班,並由此開始了他的計算機生涯。此外他還和J.W.J. Williams(威廉姆斯)於1964年共同發明了著名的堆排序算法HEAPSORT。

算法介紹:

上圖中有4個城市8條公路,公路上的數字表示這條公路的長短。請注意這些公路是單向的。咱們如今須要求任意兩個城市之間的最短路程,也就是求任意兩個點之間的最短路徑。這個問題這也被稱爲「多源最短路徑」問題。

如今須要一個數據結構來存儲圖的信息,咱們仍然能夠用一個4*4的矩陣(二維數組e)來存儲。

核心代碼:

for(k=1;k<=n;k++)
    for(i=1;i<=n;i++)
        for(j=1;j<=n;j++)
            if(e[i][j]>e[i][k]+e[k][j])
                 e[i][j]=e[i][k]+e[k][j];
複製代碼

這段代碼的基本思想就是:

最開始只容許通過1號頂點進行中轉,接下來只容許通過1和2號頂點進行中轉……容許通過1~n號全部頂點進行中轉,求任意兩點之間的最短路程。一旦發現比以前矩陣內存儲的距離短,就用它覆蓋原來保存的距離。

用一句話歸納就是:從i號頂點到j號頂點只通過前k號點的最短路程。

另外須要注意的是:Floyd-Warshall算法不能解決帶有「負權迴路」(或者叫「負權環」)的圖,由於帶有「負權迴路」的圖沒有最短路。例以下面這個圖就不存在1號頂點到3號頂點的最短路徑。由於1->2->3->1->2->3->…->1->2->3這樣路徑中,每繞一次1->-2>3這樣的環,最短路就會減小1,永遠找不到最短路。其實若是一個圖中帶有「負權迴路」那麼這個圖則沒有最短路。

代碼實現:

#include <stdio.h>
int main()
{
    int e[10][10],k,i,j,n,m,t1,t2,t3;
    int inf=99999999; //用inf(infinity的縮寫)存儲一個咱們認爲的正無窮值
    //讀入n和m,n表示頂點個數,m表示邊的條數
    scanf("%d %d",&n,&m);
    
    //初始化
    for(i=1;i<=n;i++)
        for(j=1;j<=n;j++)
            if(i==j) e[i][j]=0;  
              else e[i][j]=inf;

    //讀入邊
    for(i=1;i<=m;i++)
    {
        scanf("%d %d %d",&t1,&t2,&t3);
        e[t1][t2]=t3;
    }
    
    //Floyd-Warshall算法核心語句
    for(k=1;k<=n;k++)
        for(i=1;i<=n;i++)
            for(j=1;j<=n;j++)
                if(e[i][j]>e[i][k]+e[k][j] ) 
                    e[i][j]=e[i][k]+e[k][j];
    
    //輸出最終的結果
    for(i=1;i<=n;i++)
    {
     for(j=1;j<=n;j++)
        {
            printf("%10d",e[i][j]);
        }
        printf("\n");
    }
    
    return 0;
}
複製代碼

總結

關於BellmanFord和SPFA再說兩句

來自:www.zhihu.com/question/27…

SPFA只是BellmanFord的一種優化,其複雜度是O(kE),SPFA的提出者認爲k很小,能夠看做是常數,但事實上這一說法十分不嚴謹(原論文的「證實」居然是靠編程驗證,甚至沒有說明編程驗證使用的數據是如何生成的),如其餘答案所說的,在一些數據中,這個k可能會很大。而Dijkstra算法在使用斐波那契堆優化的狀況下複雜度是O(E+VlogV)。SPFA,或者說BellmanFord及其各類優化(姜碧野的國家集訓隊論文就提到了一種棧的優化)的優點更主要體如今可以處理負權和判斷負環吧(BellmanFord能夠找到負環,但SPFA只能判斷負環是否存在)。

補充算法

還有一些最短路算法的優化或者引伸方法,感興趣能夠谷歌一下:

  • Johnson算法
  • Bi-Direction BFS算法
  • ...

參考

關注我

我目前是一名後端開發工程師。技術領域主要關注後端開發,數據安全,爬蟲,5G物聯網等方向。

微信:yangzd1102

Github:@qqxx6661

我的博客:

原創博客主要內容

  • Java知識點複習全手冊
  • Leetcode算法題解析
  • 劍指offer算法題解析
  • SpringCloud菜鳥入門實戰系列
  • SpringBoot菜鳥入門實戰系列
  • Python爬蟲相關技術文章
  • 後端開發相關技術文章

我的公衆號:Rude3Knife

我的公衆號:Rude3Knife

若是文章對你有幫助,不妨收藏起來並轉發給您的朋友們~

相關文章
相關標籤/搜索