根據DSqiu的blog整理出來 :http://dsqiu.iteye.com/blog/1689163
PS:模板是自己寫的,如有錯誤歡迎指出~
本文內容框架:
§1 Dijkstra算法
§2 Bellman-Ford算法
§3 Floyd-Warshall算法
§4 Johnson算算法
§5 問題歸約
§0 小結
常用的最短路徑算法有:Dijkstra算法、Bellman-Ford算法、Floyd-Warshall算法、Johnson算法
最短路徑算法可以分為單源點最短路徑和全源最短路徑。
單源點最短路徑有Dijkstra算法和Bellman-Ford算法,其中Dijkstra算法主要解決所有邊的權為非負的單源點最短路徑,Bellman-Ford算法可以適用權值有負值的問題。
全源最短路徑主要有Floyd-Warshall算法和Johnson算法,其中Floyd算法可以檢測圖中的負環並可以解決不包括負環的圖中全源最短路徑問題,Johnson算法相比Floyd-Warshall算法,效率更高。
算法性能分析
在分別講解這四個算法之前先來理清下這個四個算法的復雜度:Dijkstra算法直接實現時間復雜度是O(n²),空間復雜度是O(n)(保存距離和路徑),二叉堆實現時間復雜度變成O((V+E)logV),Fibonacci Heap可以將復雜度降到O(E+VlogV);Bellman-Ford算法時間復雜度是O(V*E),SPFA是時間復雜度是O(kE);Floyd-Warshall算法時間復雜度是O(n³),空間復雜度是O(n²);Johnson算法時間復雜度是O( V * E * lgd(V) ),比Floyd-Warshall算法效率高。
最短路徑算法之Dijkstra算法
§1 Dijkstra算法
Dijkstra算法思想
Dijkstra算法思想為:設G=(V,E)是一個帶權有向圖(無向可以轉化為雙向有向),把圖中頂點集合V分成兩組,第一組為已求出最短路徑的頂點集合(用S表示,初始時S中只有一個源點,以后每求得一條最短路徑 , 就將 加入到集合S中,直到全部頂點都加入到S中,算法就結束了),第二組為其余未確定最短路徑的頂點集合(用U表示),按最短路徑長度的遞增次序依次把第二組的頂點加入S中。在加入的過程中,總保持從源點v到S中各頂點的最短路徑長度不大於從源點v到U中任何頂點的最短路徑長度。此外,每個頂點對應一個距離,S中的頂點的距離就是從v到此頂點的最短路徑長度,U中的頂點的距離,是從v到此頂點只包括S中的頂點為中間頂點的當前最短路徑長度。
Dijkstra算法具體步驟
(1)初始時,S只包含源點,即S={v},v的距離dist[v]為0。U包含除v外的其他頂點,U中頂點u距離dis[u]為邊上的權值(若v與u有邊) )或∞(若u不是v的出邊鄰接點即沒有邊<v,u>)。
(2)從U中選取一個距離v(dist[k])最小的頂點k,把k,加入S中(該選定的距離就是v到k的最短路徑長度)。
(3)以k為新考慮的中間點,修改U中各頂點的距離;若從源點v到頂點u(u∈ U)的距離(經過頂點k)比原來距離(不經過頂點k)短,則修改頂點u的距離值,修改后的距離值的頂點k的距離加上邊上的權(即如果dist[k]+w[k,u]<dist[u],那么把dist[u]更新成更短的距離dist[k]+w[k,u])。
(4)重復步驟(2)和(3)直到所有頂點都包含在S中(要循環n-1次)。
Dijkstra算法的鄰接表實現:
Cpp代碼
1 /* Dijkstra >> 優先隊列優化 */ 2 typedef pair<int,int> pii; 3 struct node{ 4 int first; 5 }nod[MAXN]; 6 struct edge{ 7 int next,to,v; 8 }e[MAXM]; 9 bool vis[MAXN]; 10 int dis[MAXN]; 11 void dijkstra (int st){ 12 memset(vis,0,sizeof vis); 13 for(int i=0;i<n;i++)dis[i]=INF; 14 dis[st]=0; 15 priority_queue < pii,vector<pii>,greater<pii> > q; //pair used to save(d[i],i) 16 q.push(make_pair(0,st)); 17 while(!q.empty()){ 18 pii x=q.top();q.pop(); 19 int u=x.second; 20 if(vis[u])continue; 21 vis[u]=1; 22 for(int i=nod[u].first;i!=-1;i=e[i].next)if(dis[e[i].to]>dis[u]+e[i].v){ 23 dis[e[i].to]=dis[u]+e[i].v; 24 q.push(make_pair(dis[e[i].to],e[i].to)); 25 } 26 } 27 }
最短路徑算法之Bellman-Ford算法
§2 Bellman-Ford算法
Bellman-Ford算法思想
Bellman-Ford算法能在更普遍的情況下(存在負權邊)解決單源點最短路徑問題。對於給定的帶權(有向或無向)圖 G=(V,E),其源點為s,加權函數 w是 邊集 E 的映射。對圖G運行Bellman-Ford算法的結果是一個布爾值,表明圖中是否存在着一個從源點s可達的負權回路。若不存在這樣的回路,算法將給出從源點s到 圖G的任意頂點v的最短路徑d[v]。
Bellman-Ford算法流程:
(1) 初始化:將除源點外的所有頂點的最短距離估計值 d[v] ←+∞, d[s] ←0;
(2) 迭代求解:反復對邊集E中的每條邊進行松弛操作,使得頂點集V中的每個頂點v的最短距離估計值逐步逼近其最短距離;(運行|v|-1次)
(3) 檢驗負權回路:判斷邊集E中的每一條邊的兩個端點是否收斂。如果存在未收斂的頂點,則算法返回false,表明問題無解;否則算法返回true,並且從源點可達的頂點v的最短距離保存在 d[v]中。
算法描述如下:
Bellman-Ford(G,w,s) :boolean //圖G ,邊集 函數 w ,s為源點
1 for each vertex v ∈ V(G) do //初始化 1階段
2 d[v] ←+∞
3 d[s] ←0; //1階段結束
4 for i=1 to |v|-1 do //2階段開始,雙重循環。
5 for each edge(u,v) ∈E(G) do //邊集數組要用到,窮舉每條邊。
6 If d[v]> d[u]+ w(u,v) then //松弛判斷
7 d[v]=d[u]+w(u,v) //松弛操作 2階段結束
8 for each edge(u,v) ∈E(G) do
9 If d[v]> d[u]+ w(u,v) then
10 Exit false
11 Exit true
下面給出描述性證明:
首先指出,圖的任意一條最短路徑既不能包含負權回路,也不會包含正權回路,因此它最多包含|v|-1條邊。
其次,從源點s可達的所有頂點如果 存在最短路徑,則這些最短路徑構成一個以s為根的最短路徑樹。Bellman-Ford算法的迭代松弛操作,實際上就是按頂點距離s的層次,逐層生成這棵最短路徑樹的過程。
在對每條邊進行1遍松弛的時候,生成了從s出發,層次至多為1的那些樹枝。也就是說,找到了與s至多有1條邊相聯的那些頂點的最短路徑;對每條邊進行第2遍松弛的時候,生成了第2層次的樹枝,就是說找到了經過2條邊相連的那些頂點的最短路徑……。因為最短路徑最多只包含|v|-1 條邊,所以,只需要循環|v|-1 次。
每實施一次松弛操作,最短路徑樹上就會有一層頂點達到其最短距離,此后這層頂點的最短距離值就會一直保持不變,不再受后續松弛操作的影響。(但是,每次還要判斷松弛,這里浪費了大量的時間,怎么優化?單純的優化是否可行?)
如果沒有負權回路,由於最短路徑樹的高度最多只能是|v|-1,所以最多經過|v|-1遍松弛操作后,所有從s可達的頂點必將求出最短距離。如果 d[v]仍保持 +∞,則表明從s到v不可達。
如果有負權回路,那么第 |v|-1 遍松弛操作仍然會成功,這時,負權回路上的頂點不會收斂。
1.Bellman-Ford算法實現:
Cpp代碼
1 /* Bellman-Ford (若有負權環路則返回false) */ 2 struct node{ 3 int first; 4 }nod[MAXN]; 5 struct edge{ 6 int next,to,v; 7 }e[MAXM]; 8 bool inq[MAXN]; //used to note whether u node is in queue. 9 int dis[MAXN]; 10 int Bellman_Ford (int st){ 11 memset(inq,0,sizeof inq); 12 for(int i=0;i<n;i++)dis[i]=INF; 13 dis[st]=0; 14 inq[st]=true; 15 for(int i=0;i<n;i++){ 16 for(int u=0;u<n;u++)if(inq[u]){ 17 inq[u]=false; 18 for(int ed=nod[u].first;ed!=-1;ed=e[ed].next)if(dis[e[ed].to]>dis[u]+e[ed].v){ 19 dis[e[ed].to]=dis[u]+e[ed].v; 20 inq[e[ed].to]=true; 21 } 22 } 23 } 24 for(int i=0;i<n;i++)if(inq[i])return false; 25 return true; 26 }
2.SPFA——Bellman-Ford算法優化
有隊列替代循環過程,對無負權環路的圖更快。
Cpp代碼
1 /* SPFA (無負權環路) */ 2 struct node{ 3 int first; 4 }nod[MAXN]; 5 struct edge{ 6 int next,to,v; 7 }e[MAXM]; 8 int dis[MAXN]; 9 void spfa (int st){ 10 queue <int> q; 11 for(int i=0;i<n;i++)dis[i]=INF; 12 dis[st]=0; 13 q.push(st); 14 while(!q.empty()){ 15 int u=q.top();q.pop(); 16 int i=nod[u].first; 17 for(;i!=-1;i=e[i].next)if(dis[e[i].to]>dis[u]+e[i].v){ 18 dis[e[i].to]=dis[u]+e[i].v; 19 q.push(e[i].to); 20 } 21 } 22 }
最短路徑算法之Floyd-Warshall算法
§3 Floyd-Warshall算法
Floyd-Warshall算法是解決任意兩點間的最短路徑的算法,可以處理有向圖或負權值的最短路徑問題,同時也被用於計算有向圖的傳遞閉包。算法的時間復雜度為O(n³),空間復雜度為O(n²)。
Floyd-Warshall算法的原理是動態規划。
設為從
到
的只以
集合中的節點為中間節點的最短路徑的長度。
- 若最短路徑經過點k,則
;
- 若最短路徑不經過點k,則
。
因此,。
在實際算法中,為了節約空間,可以直接在原來空間上進行迭代,這樣空間可降至二維。
Floyd-Warshall算法實現
Cpp代碼
1 /* Floyd part */ 2 for(int k =1 ; k <= n ; k ++ ){ 3 for(int i =1 ; i<= n ; i++){ 4 for(int j =1 ;j<=n;j++){ 5 dis[ i ][ j ]= min( dis[ i ][ j ],dis[ i ][ k ]+dist[ k ][ j ] ); 6 } 7 } 8 } 9 /* 如果dis[i][j]不存在用INF代替 */
最短路徑算法之Johnson算法
§4 Johnson算算法
Johson算法是目前最高效的在無負環可帶負權重的網絡中求所有點對最短路徑的算法. Johson算法是Bellman-Ford算法, Reweighting(重賦權重)和Dijkstra算法的大綜合. 對每個頂點運用Dijkstra算法的時間開銷決定了Johnson算法的時間開銷. 每次Dijkstra算法(d堆PFS實現)的時間開銷是O( E * lgd(V) ). 其中E為邊數, V為頂點數, d為采用d路堆實現優先隊列ADT. 所以, 此種情況下Johnson算法的時間復雜度是O( V * E * lgd(V) )。
Johnson算法具體步驟(翻譯自wikipedia):
1.初始化,把一個node q添加到圖G中,使node q 到圖G每一個點的權值為0。
2.使用Bellman-Ford算法,從源點為q,尋找每一個點 v從q到v的最短路徑h(v),如果存在負環的話,算法終止。
3.使用第2步驟中Bellman-Ford計算的最短路徑值對原來的圖進行reweight操作(重賦值):邊<u,v>的權值w(u,v),修改成w(u,v)+h(u)-h(v)。
4.最后,移去q,針對新圖(重賦值之后的圖)使用Dijkstra算法計算從每一個點s到其余另外點的最短距離。
Johnson算法實現:
Cpp代碼
1 /* Johnson O(V*E*logdV */ 2 struct node{ 3 int first; 4 }nod[MAXN]; 5 struct edge{ 6 int next,to,from,v; 7 }e[MAXM]; 8 int sz; //number of edges. 9 bool inq[MAXN]; //used to note whether u node is in queue. 10 int dis[MAXN],d[MAXN][MAXN]; 11 int johnson (){ 12 memset(inq,0,sizeof inq); 13 for(int i=1;i<=n;i++)dis[i]=INF; 14 dis[0]=0; 15 for(int i=1;i<=n;i++){ 16 e[sz].next=nod[0].first; 17 e[sz].from=0;e[sz].to=i;e[sz].v=0; 18 nod[0].first=sz++; 19 } 20 inq[0]=true; 21 for(int i=1;i<=n;i++){ 22 for(int u=1;u<=n;u++)if(inq[u]){ 23 inq[u]=false; 24 for(int ed=nod[u].first;ed!=-1;ed=e[ed].next)if(dis[e[ed].to]>dis[u]+e[ed].v){ 25 dis[e[ed].to]=dis[u]+e[ed].v; 26 inq[e[ed].to]=true; 27 } 28 } 29 } 30 for(int i=0;i<n;i++)if(inq[i])return false; 31 for(int i=0;i<sz;i++)e[i].v=e[i].v-dis[to]+dis[from]; 32 /*then run the dijkstra from every node.*/ 33 for(int i=1;i<=n;i++)dijkstra(i); 34 return true; 35 }
§5 問題歸約
對於兩個問題A和B,如果使用求解B的一個算法來開發一個求解A的算法,且最壞的情況下算法總時間不會超過最壞情況下求解B的算法運行時間的常量倍,則稱問題A可歸約(reduce)為問題B。
1.傳遞閉包問題可歸約為有非負權值的所有對最短路徑問題。
給定兩點u和v,有向圖中從u到v存在一條路徑,當且僅當網中從u到v的路徑長度非零。
2.在邊權沒有限制的網中,(單源點或所有對)最長路徑和最短路徑問題是等價的。
3.作業調度問題可歸約為差分約束問題。
4.有正常數的差分約束問題等價於無環網中的單源點最長路徑。
5.帶有截止期的作業調度問題可歸約為(允許帶有負權值的)最短路徑問題。
§6 最短路徑的擴展與應用
1.k短路
i.e:求從起點s到終點t的第k短的路,即k短路問題。
先用dijkstra從t反向尋找最短路。然后使用A*算法,把f(i)=g(i) + h(i)。h(i)就是i點到t的最短距離。當某點出隊次數達到k次的時候,結果為該點的當前路程+該點到t的最短距離。(我沒有判斷不連通的情況)
2.差分約束系統
i.e:給出一系列類似 xi-xj<=bij 的不等式,成為差分約束系統。
為了解決這個問題,可以將不等式變形,如 xi<=xj+bij,然后轉化為最短路問題。
設置一個源點,xi 看做 i 點到源點的最短路,則 bij 是路徑 i 到 j 的權值,一開始設置各點到源點的距離為一個常數C,(C其實可以任意,因為對於一個解系x,x+C一定構成另一個解系。)
對於構造好的無向圖,可以用Dijkstra或SPFA解決 O(∩_∩)O
3.DAG圖上的單源點最短路徑
只用把無向圖的Dijkstra改成有向圖就可以。
4.Flyod求最小環
最小環:所有環中帶權長度最小的(只允許繞一圈)。
可以進行m次Dijkstra,每次刪一條邊w(i,j),求dis(j,i)最短路,再更新最小環,時間復雜度O(E*E*logdV)。
也可以再Floyd過程中求出,時間復雜度O(n^3)。當k進行到L-1時,已經求得所有以0,1,……,L-1為中間節點的最短路徑,
設cir(L)為環上最大節點編號為L的環中的最小長度。
則cir(L)=min{w(i,L)+w(L,j)+dis(j,i)|i-->L & L-->j存在} (dis(i,j)為當前i到j的最短路)
整個Floyd走完,取cir(0,1,……,n-1)的最小值。
基本就是完整版了,里面有很多按自己的理解寫的,代碼有些還沒有經過題目檢驗,如有錯誤懇請斧正~