最短路徑問題算法(Shortest Path Problems' Algorithms)

最短路徑問題算法

作者:Bluemapleman([email protected])

麻煩不吝star和fork本博文對應的github上的技術博客項目吧!謝謝你們的支持!

知識無價,寫作辛苦,歡迎轉載,但請註明出處,謝謝!



前言:最短路徑問題可以根據要解決的具體問題類型,分爲單源單目標最短路徑(single pair),單源最短路徑(single source),單目標最短路徑(single destination),和多源多目標最短路徑問題(all pairs)。我們從單源最短路徑問題入手,單目標最短路徑問題等價於反向的單源最短路徑問題,單源單目標最短路徑問題則是一個順便解決的問題,而多源多目標最短路徑問題的解決方案也是基於單源最短路徑問題的。

本博文談論最短路徑問題中,除非特別說明,否則默認都是有向圖(Digraph):G=(V,E),V表示頂點集合,E表示邊集合。

單源最短路徑問題

設定

  • 有向圖
  • 單一起始點
  • 每條邊都是加權有向邊(Weighted directed edges),連接頂點a和頂點b之間的邊的權重用w(a,b)表示
  • 每個頂點都有一個key,名爲d,表示從源點s到達該頂點的最短距離.(s.d=0);並且每個點有一個屬性prev來記錄最短路徑上的前繼頂點(s.prev=s)。

引入

  • 最短路徑與最短子路徑

容易觀察到的一個最短路徑相關的性質是:若有一條從任意a點到b點的最短路徑,則該路徑上任意兩點之間的路徑也是這兩點之間的最短路徑。(容易通過反證法證明)

  • 最短路徑存在性質

顯然,如果圖中沒有負值迴路(negative cycles,即所有邊的權值和爲負數的迴路),並且如果從點s到點t之間有至少一條路徑可達,則一定存在一個從s到t的最短路徑,並且該路徑一定是簡單(simple,即沒有重複走過的頂點)的。

如果有負值迴路,會造成的情況是:某最短路徑可以通過不斷走負值迴路降低路徑總長度,所以導致最短路徑可以無限短,也就沒有了真正意義上的最短路徑。

  • 鬆弛操作(RELAX)

鬆弛操作接受點a,點b,以及從點a指向b的邊的權重w三個參數,並進行如下操作:

RELAX(a,b,w)
IF b.d>a.d+w:
    b.d=a.d+w;
    b.prev=a.prev;

該操作的含義就是,若目標點的key當前大於【某邊的源點的key+邊的權重的和】,則將該目標點的key設置爲這個和,即表示點b可以通過將點a設爲前綴頂點,並走邊w(a,b),以實現更短的到達路徑。

Bellman-Ford算法

Bellman-Ford算法是解決單元最短路徑的一個算法,其做法是:重複V-1次,對圖的所有的E條邊進行鬆弛操作。並且,Bellman-Ford算法中允許邊的權重爲負數,即w(a,b)<0

  • 僞代碼
BELLMAN-FORD(G,w,s)
1 INITIALIZE-SINGLE-SOURCE(G,s) // 初始化步驟:設置所有頂點的key
2 for i=1 to |G.V| - 1          // 重複V-1次:對所有的邊進行鬆弛操作
3   for each edge (u,v) in G.E
4     RELAX (u,v,w)
5   for each edge(u,v) in G:E   // 5-7行檢查是否有負值迴路,有則不存在最短路徑
6     if v.d > u.d + w(u,v)
7       return FALSE
8   return TRUE

時間複雜度:O(VE)

一種直觀的改進思路是:在每輪循環中用一個標記變量記錄本輪是否有有鬆弛操作生效,若沒有,說明已經到達最終情況,可以退出鬆弛的循環。(類似布爾排序的改進。)

而如果存在負值迴路,在算法跑完後,我們也可以通過前繼結點的回溯發現一個環。

DAG的單源最短路徑算法

DAG(Directed acyclic graph)即有向無環圖,相比Bellman-Fold算法還必須注意負值迴路的問題,DAG則通過限定無環避免了這個問題。而我們針對這類常見的圖,就可以用拓撲排序的方法在O(V+E)的時間複雜度內解決圖內的單源最短路徑問題:

DAG-SHORTEST-PATHS(G,w,s)
1 topologically sort the vertices of G // 拓撲排序,O(V+E)
2 INITIALIZE-SINGLE-SOURCE(G,s)  // 初始化 O(V)
3 for each vertex u, taken in topologically sorted order // 3-5行對按照拓撲排序的順序,對每個頂點的邊逐一進行鬆弛操作,O(V)
4   for each vertex v in G.Adj(u)
5     RELAX(u,v,w)

Dijkstra算法(迪傑斯特拉算法)

在Bellman-Fold和DAG單元最短路徑算法中,都允許權重爲負數的邊存在,而Dijkstra最短路徑算法則不允許權重爲負數的邊存在。

DIJKSTRA(G,w,s)
1 INITIALIZE-SINGLE-SOURCE(G,s)
2 S = null set
3 Q = G.V   // 建立優先隊列, O(VlgV)時間複雜度,O(V)空間複雜度
4 while Q != null set ;
5   u = EXTRACT-MIN(Q)  // 優先隊列的提取key最小的頂點的操作
6   S = S union {u}
7   for each vertex v in G.Adj[u]
8     RELAX(u,v,w)      // DecreaseKey操作

Dijkstra算法用到了優先隊列來實現,先將所有頂點通通入隊,然後按照key從小到大的順序出隊並進行鬆弛操作,而先出隊的頂點的鬆弛操作可能影響尚未出隊的頂點的key值大小,因此我們用DecreaseKey操作保證尚未出隊的頂點在隊列中的正確相對順序。

Dijkstra的時間複雜度主要取決於我們如何實現優先隊列,甚至我們可以不用優先隊列,而只用一個數組來存頂點的key值,並通過遍歷數組來找key最小的頂點。以下幾種實現方式分別的複雜度:

  • 數組存key代替優先隊列: O(V^2+E)
  • 最小二叉堆實現的優先隊列: O(VlgV+ElgV)
  • Fibonacci堆實現的優先隊列:O(VlgV+E) (Fibonacci的DecreaseKey操作的時間複雜度是O(1))

空間複雜度則都是O(V)。

(從利用了優先隊列和複雜度來看,Dijkstra和MST的Prim算法很像。)

多源多目標最短路徑問題

設定

  • 有向圖
  • 每條邊都是加權有向邊(Weighted directed edges)
  • 每個頂點都有一個key,名爲d,表示從源點s到達該頂點的最短距離.(s.d=0);並且每個點有一個屬性prev來記錄最短路徑上的前繼頂點(s.prev=s)。

稀疏圖——Johnson算法

  • 適合解決稀疏圖(E<<V^2)
  • 允許存在負數權重邊

Johnson算法的整體思路是:先運行Bellman-Fold算法一遍,然後分別以各個頂點爲源點,運行Dijkstra算法N遍。

但是首先注意,我們說過,Dijkstra算法是不允許存在負數邊的,因此我們需要做一個reweighting操作,以重新構建整個圖的邊的權重,但不能影響最終結果。

reweighting的做法是這樣的:

假設我們有一個“高度”函數(height function):

  h: V -> R

我們可以定義reweighting:

  w'(u,v)=w(u,v)+h(u)-h(v)

假設P是這樣一條路徑:v0->v1->v2->...->vk

則reweighting前的路徑權重和爲:w(P)=w(v0,v1)+w(v1,v2)+...+w(vk-1,vk)

而reweighting後的路徑權重和爲:w'(P)=w(P)+h(v0)-h(vk)

我們希望儘量找到這樣的一個h函數,使得所有的reweighting過的邊的權重都爲非負數。

  • Johnson算法的具體步驟

Step 1: 添加一個新結點s,並添加從s到所有圖G中的頂點的邊,這些邊的權重都初始化爲0.這個新圖,我們稱之爲G’.

Step 2: 運行一次Bellman-Ford算法;如果發現了負值迴路,則退出;否則,令高度函數h(v)=δ(s,t\delta(s,t,即從s到v的最短路徑長,並定義w’(u,v)=w(u,v)+h(u)-h(v). (通過Bellman-Fold的算法可知,w(u,v)+h(u)>=h(v),所以w’(u,v)>=0)

Step 3: 基於w’,對每個V中的頂點運行一次Dijkstra算法。

Step 4: 輸出所有s到所有t的最短路徑δ(s,t)=δ^(s,t)h(s)+h(t)\delta(s,t)=\hat{\delta}(s,t)-h(s)+h(t)

時間複雜度:O(VE+VE+V2lgV)=O(VE+V2lgV)O(VE+VE+V^2lgV)=O(VE+V^2lgV) (基於Fibonacci Heap的優先隊列實現)

空間複雜度:O(V2+V+V)=O(V2)O(V^2+V+V)=O(V^2)

稠密圖——矩陣乘法/Floyd-Warshall算法

如果我們的圖是個稠密圖(dense),即E=Θ(V2)E=\Theta(V^2)

矩陣乘法(Matrix Multiplication)算法

若圖的邊的權重以矩陣的形式給出:

n*n矩陣:W=(wijw_{ij}), n=|V|,

wij=w_{ij}=

  • 0, 若i=j
  • w(i,j),若(i,j)\inE
  • 無窮大,otherwise

定義另一個矩陣L(m)=(lij(m))L^{(m)}=(l_{ij}^{(m)}):

l_{ij}^{(m)}=用小於m個邊實現的從頂點i到頂點j的最短路徑的長度。

而我們的最終目標就是計算L(n)L^{(n)},而我們初始狀態下擁有的是L(1)=WL^{(1)}=W,即權重矩陣。

  • 時間複雜度爲O(n4)(Slowmethod)O(n^4)的方法(Slow method):

EXTEND-S-P操作具體如下,它的含義就是基於當前的矩陣L(i1)L^{(i-1)},爲每個最短路徑多延伸一條邊,得到L(i)L^{(i)}

  • 更好的O(n3lgn)O(n^3lgn)方法

“重複平方”:利用平方更快地得到L(n)L^{(n)}

Floyd-Warshall算法

初始狀態下,我們有權重矩陣W,V={1,2,3,…,n}(給所有頂點編號),權重矩陣的元素wijw_{ij}爲從點i到點j的邊的權重,同時也可以看作是從點i到點j的、要求一步以內就能到達的最短路徑

定義dij(k)=d_{ij}^{(k)}=從i到j的最短路徑,要求路徑上所有途徑點的編號都不大於k。

定義D(k)=(dij(k))D^{(k)}=(d_{ij}^{(k)}),而我們最終想要的就是D(n)D^{(n)}.

  • 算法描述

時間複雜度:Θ(n3)\Theta(n^3) (每個D(i1)D(i)D^{(i-1)}到D^{(i)}的運算花費O(n2)O(n^2))

空間複雜度:Θ(n3)\Theta(n^3)(如果存儲所有的D(i)D^{(i)}),Θ(n2)\Theta(n^2)(如果只存儲當前需要用到的D(i)D^{(i)})

比較多源多目標最短路徑算法

算法 時間複雜度
矩陣相乘算法 O(n3lgn)O(n^3lgn)
Floyd-Warshall算法 O(n3n^3)
Johnson算法 O(nm+n2lgnn^2lgn)

另外,矩陣相乘算法和Floyd-Warshall算法也需要做負值迴路檢測,以確保存在解。檢測的方法時:只要在算法進行過程中發現任何lij(m)l_{ij}^{(m)}或者dij(k)d_{ij}^{(k)}爲負值,就說明有負值迴路。

單源單目標最短路徑算法

我們重新回頭看單源單目標最短路徑算法問題:我們當然可以用單源最短路徑算法像Bellman-Fold,DAG最短路徑或者Dijkstra來解決,但是當然也沒必要這樣”高射炮打蚊子“,其實有專門針對這種問題的算法A*搜索,這裏就不細講了。

參考文獻

[1] Introduction to Algorithms: Third Edition, Thomas et al.

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章