最短路徑——Dijkstra算法和Floyd算法


Dijkstra算法概述

  Dijkstra算法是由荷蘭計算機科學家狄克斯特拉Dijkstra)於1959 年提出的,因此又叫狄克斯特拉算法。是從一個頂點到其余各頂點的最短路徑算法,解決的是有向圖(無向圖是一種特殊的有向圖,當然也可以)中最短路徑問題(單源最短路徑)

  其基本原理是:每次新擴展一個距離最短的點,更新與其相鄰的點的距離。當所有邊權都為正時,由於不會存在一個距離更短的沒擴展過的點,所以這個點的距離永遠不會再被改變,因而保證了算法的正確性。不過根據這個原理,用Dijkstra求最短路的圖不能有負權邊,因為擴展到負權邊的時候會產生更短的距離,有可能就破壞了已經更新的點距離不會改變的性質。

  舉例來說,如果圖中的頂點表示城市,而邊上的權重表示著城市間開車行經的距離。 Dijkstra算法可以用來找到兩個城市之間的最短路徑。

  Dijkstra算法的輸入包含了一個有權重的有向圖G,以及G中的一個來源頂點S。 我們以V表示G中所有頂點的集合。 每一個圖中的邊,都是兩個頂點所形成的有序元素對。(u,v)表示從頂點u到v有路徑相連。 我們以E為所有邊的集合,而邊的權重則由權重函數w: E → [0, ∞]定義。 因此,w(u,v)就是從頂點u到頂點v的非負花費值(cost)。 邊的花費可以想像成兩個頂點之間的距離。任兩點間路徑的花費值,就是該路徑上所有邊的花費值總和。 已知V中有頂點s及t,Dijkstra算法可以找到s到t的最低花費路徑(i.e. 最短路徑)。 這個算法也可以在一個圖中,找到從一個頂點s到任何其他頂點的最短路徑。

Image:Dijkstra算法圖.jpg

算法描述

  這個算法是通過為每個頂點v保留目前為止所找到的從s到v的最短路徑來工作的。初始時,源點s的路徑長度值被賦為0(d[s]=0), 同時把所有其他頂點的路徑長度設為無窮大,即表示我們不知道任何通向這些頂點的路徑(對於V中所有頂點v除s外d[v]= ∞)。當算法結束時,d[v]中儲存的便是從s到v的最短路徑,或者如果路徑不存在的話是無窮大。 Dijstra算法的基礎操作是邊的拓展:如果存在一條從u到v的邊,那么從s到u的最短路徑可以通過將邊(u,v)添加到尾部來拓展一條從s到v的路徑。這條路徑的長度是d[u]+w(u,v)。如果這個值比目前已知的d[v]的值要小,我們可以用新值來替代當前d[v]中的值。拓展邊的操作一直執行到所有的d[v]都代表從s到v最短路徑的花費。這個算法經過組織因而當d[u]達到它最終的值的時候沒條邊(u,v)都只被拓展一次。

  算法維護兩個頂點集S和Q。集合S保留了我們已知的所有d[v]的值已經是最短路徑的值頂點,而集合Q則保留其他所有頂點。集合S初始狀態為空,而后每一步都有一個頂點從Q移動到S。這個被選擇的頂點是Q中擁有最小的d[u]值的頂點。當一個頂點u從Q中轉移到了S中,算法對每條外接邊(u,v)進行拓展。

偽碼

  在下面的算法中,u:=Extract_Min(Q)表示在頂點集Q中搜索有最小的d[u]值的頂點u。這個頂點被從集合Q中刪除並返回給用戶:

 1  function Dijkstra(G, w, s)
 2     for each vertex v in V[G]                        // 初始化
 3           d[v] := infinity
 4           previous[v] := undefined
 5     d[s] := 0
 6     S := empty set
 7     Q := set of all vertices
 8     while Q is not an empty set                      // Dijstra算法主體
 9           u := Extract_Min(Q)
10           S := S union {u}
11           for each edge (u,v) outgoing from u
12                  if d[v] > d[u] + w(u,v)             // 拓展邊(u,v)
13                        d[v] := d[u] + w(u,v)
14                        previous[v] := u

如果我們只對在s和t之間尋找一條最短路徑的話,我們可以在第9行添加條件如果滿足u=t的話終止程序。

現在我們可以通過迭代來回溯出s到t的最短路徑:

1 S := empty sequence 
2 u := t
3 while defined u                                        
4       insert u to the beginning of S
5       u := previous[u]

現在序列S就是從s到t的最短路徑的頂點集.

時間復雜度

  我們可以用大O符號將Dijkstra算法的運行時間表示為邊數m和頂點數n的函數。

  Dijkstra算法最簡單的實現方法是用一個鏈表或者數組來存儲所有頂點的集合Q,所以搜索Q中最小元素的運算(Extract-Min(Q))只需要線性搜索Q中的所有元素。這樣的話算法的運行時間是O(n^2)。

  對於邊數少於n^2稀疏圖來說,我們可以用鄰接表來更有效的實現Dijkstra算法。同時需要將一個二叉堆或者斐波納契堆用作優先隊列來尋找最小的頂點(Extract-Min)。當用到二叉堆的時候,算法所需的時間為O((m+n)log n),斐波納契堆能稍微提高一些性能,讓算法運行時間達到O(m + n log n)。 相關問題和算法在Dijkstra算法的基礎上作一些改動,可以擴展其功能。例如,有時希望在求得最短路徑的基礎上再列出一些次短的路徑。為此,可先在原圖上計算出最短路徑,然后從圖中刪去該路徑中的某一條邊,在余下的子圖中重新計算最短路徑。對於原最短路徑中的每一條邊,均可求得一條刪去該邊后子圖的最短路徑,這些路徑經排序后即為原圖的一系列次短路徑。

  OSPF(open shortest path first, 開放最短路徑優先)算法是Dijkstra算法在網絡路由中的一個具體實現。

  與Dijkstra算法不同,Bellman-Ford算法可用於具有負花費邊的圖,只要圖中不存在總花費為負值且從源點 s 可達的環路(如果有這樣的環路,則最短路徑不存在,因為沿環路循環多次即可無限制的降低總花費)。

  與最短路徑問題有關的一個問題是旅行商問題(traveling salesman problem),它要求找出通過所有頂點恰好一次且最終回到源點的最短路徑。該問題是NP難的;換言之,與最短路徑問題不同,旅行商問題不太可能具有多項式時間算法。

  如果有已知信息可用來估計某一點到目標點的距離,則可改用A*算法,以減小最短路徑的搜索范圍。

 

Floyd算法

1.定義概覽

Floyd-Warshall算法(Floyd-Warshall algorithm)是解決任意兩點間的最短路徑的一種算法,可以正確處理有向圖或負權的最短路徑問題,同時也被用於計算有向圖的傳遞閉包。Floyd-Warshall算法的時間復雜度為O(N3),空間復雜度為O(N2)。

2.算法描述

1)算法思想原理:

      從任意節點i到任意節點j的最短路徑不外乎2種可能:

  (1)直接從i到j;

  (2)從i經過若干個節點k到j。

  所以,我們假設Dis(i,j)為節點i到節點j的最短路徑的距離,對於每一個節點k,我們檢查Dis(i,k) + Dis(k,j) < Dis(i,j)是否成立,如果成立,證明從i到k再到j的路徑比i直接到j的路徑短,我們便設置Dis(i,j) = Dis(i,k) + Dis(k,j),這樣一來,當我們遍歷完所有節點k,Dis(i,j)中記錄的便是i到j的最短路徑的距離。

  依次掃描每一點(k),並以該點作為中介點,計算出通過k點的其他任意兩點(i,j)的最短距離,這就是floyd算法的精髓!同時也解釋了為什么k點這個中介點要放在最外層循環的原因.

2).算法描述:

a.從任意一條單邊路徑開始。所有兩點之間的距離是邊的權,如果兩點之間沒有邊相連,則權為無窮大。   

b.對於每一對頂點 i 和 j,看看是否存在一個頂點 k 使得從 i 到 k 再到 j 比己知的路徑更短。如果是更新它。

 

3). Floyd算法是一個經典的動態規划算法。用通俗的語言來描述的話,首先我們的目標是尋找從點i到點j的最短路徑。從動態規划的角度看問題,我們需要為這個目標重新做一個詮釋(這個詮釋正是動態規划最富創造力的精華所在),floyd算法加入了這個概念  Ak(i,j):表示從i到j中途不經過索引比k大的點的最短路徑. 這個限制的重要之處在於,它將最短路徑的概念做了限制,使得該限制有機會滿足迭代關系,這個迭代關系就在於研究:假設Ak(i,j)已知,是否可以借此推導出Ak-1(i,j)。

    假設我現在要得到Ak(i,j),而此時Ak(i,j)已知,那么我可以分兩種情況來看待問題:1. Ak(i,j)沿途經過點k;2. Ak(i,j)不經過點k。如果經過點k,那么很顯然,Ak(i,j) = Ak-1(i,k) + Ak-1(k,j),為什么是Ak-1呢?因為對(i,k)和(k,j),由於k本身就是源點(或者說終點),加上我們求的是Ak(i,j),所以滿足不經過比k大的點的條件限制,且已經不會經過點k,故得出了Ak-1這個值。那么遇到第二種情況,Ak(i,j)不經過點k時,由於沒有經過點k,所以根據概念,可以得出Ak(i,j)=Ak-1(i,j)。現在,我們確信有且只有這兩種情況--不是經過點k,就是不經過點k,沒有第三種情況了,條件很完整,那么是選擇哪一個呢?很簡單,求的是最短路徑,當然是哪個最短,求取哪個,故得出式子:

    Ak(i,j) = min( Ak-1(i,j), Ak-1(i,k) + Ak-1(k,j) )

    現在已經得出了Ak(i,j) = Ak-1(i,k) + Ak-1(k,j)這個遞歸式,但顯然該遞歸還沒有一個出口,也就是說,必須定義一個初始狀態,事實上,這個初始狀態取決於索引k是從0開始還是從1開始,上面的代碼是C寫的,是以0為開始索引,但一般描述算法似乎習慣用1做開始索引,如果是以1為開始索引,那么初始狀態值應設置為A0了,A0(i,j)的含義不難理解,即從i到j的邊的距離。也就是說,A0(i,j) = cost(i,j) 。由於存在i到j不存在邊的情況,也就是說,在這種情況下,cost(i,j)無限大,故A0(i,j) = oo(當i到j無邊時)

    到這里,已經列出了求取Ak(i,j)的整個算法了,但是,最終的目標是求dist(i,j),即i到j的最短路徑,如何把Ak(i,j)轉換為dist(i,j)?這個其實很簡單,當k=n(n表示索引的個數)的時候,即是說,An(i,j)=dist(i,j)。那是因為當k已經最大時,已經不存在索引比k大的點了,那這時候的An(i,j)其實就已經是i到j的最短路徑了。

    從floyd算法中不難看出,要設計一個好的動態規划算法,首先需要研究是否能把目標進行重新詮釋(這一步是最關鍵最富創造力的一步),轉化為一個可以被分解的子目標,如果可以轉化,就要想辦法尋找數學等式使目標收斂為子目標,如果這一步可以實現了,還需要研究該遞歸收斂式的出口,即初始狀態是否明確(這一步往往已經簡單了)。

如果需要保存最短路徑,需要借助path數組.

typedef struct          
{        
    char vertex[VertexNum];                                //頂點表         
    int edges[VertexNum][VertexNum];                       //鄰接矩陣,可看做邊表         
    int n,e;                                               //圖中當前的頂點數和邊數         
}MGraph; 

void Floyd(MGraph g)
{
   int A[MAXV][MAXV];
   int path[MAXV][MAXV];
   int i,j,k,n=g.n;
   // 初始化數據   
for(i=0;i<n;i++)   for(j=0;j<n;j++)   {    A[i][j]=g.edges[i][j];    path[i][j]=-1;   }
   //   
for(k=0;k<n;k++)   {
  
for(i=0;i<n;i++)   for(j=0;j<n;j++)   if(A[i][j]>(A[i][k]+A[k][j]))   {   A[i][j]=A[i][k]+A[k][j];   path[i][j]=k;   }  } }

 

【參考】

1.最短路徑—Dijkstra算法和Floyd算法

2. MBA智庫:

http://wiki.mbalib.com/wiki/Dijkstra%E7%AE%97%E6%B3%95

3. floyd算法

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM