文章出自:http://dsqiu.iteye.com/blog/1689163
最短路徑算法——Dijkstra,Bellman-Ford,Floyd-Warshall,Johnson,無一倖免
本文內容框架:
§1 Dijkstra算法
§2 Bellman-Ford算法
§3 Floyd-Warshall算法
§4 Johnson算算法
§5 問題歸約
§6 小結
常用的最短路徑算法有: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算法實現
╔
直接實現
最簡單的實現方法就是,在每次循環中,再用一個循環找距離最短的點,然後用任意的方法更新與其相鄰的邊,時間複雜度顯然爲O(n²)
對於空間複雜度:如果只要求出距離,只要n的附加空間保存距離就可以了(距離小於當前距離的是已訪問的節點,對於距離相等的情況可以比較編號或是特殊處理一下)。如果要求出路徑則需要另外V的空間保存前一個節點,總共需要2n的空間。
╝②
╔
- /*********************************
- * 最短路徑---Dijkstra算法實現
- * HDU:2544
- * BLOG:www.cnblogs.com/newwy
- * AUTHOR:Wang Yong
- **********************************/
- #include <iostream>
- #define MAX 100
- #define INF 1000000000
- using namespace std;
- int dijkstra (int mat[][MAX],int n, int s,int f)
- {
- int dis[MAX];
- int mark[MAX];//記錄被選中的結點
- int i,j,k = 0;
- for(i = 0 ; i < n ; i++)//初始化所有結點,每個結點都沒有被選中
- mark[i] = 0;
- for(i = 0 ; i < n ; i++)//將每個結點到start結點weight記錄爲當前distance
- {
- dis[i] = mat[s][i];
- //path[i] = s;
- }
- mark[s] = 1;//start結點被選中
- //path[s] = 0;
- dis[s] = 0;//將start結點的的距離設置爲0
- int min ;//設置最短的距離。
- for(i = 1 ; i < n; i++)
- {
- min = INF;
- for(j = 0 ; j < n;j++)
- {
- if(mark[j] == 0 && dis[j] < min)//未被選中的結點中,距離最短的被選中
- {
- min = dis[j] ;
- k = j;
- }
- }
- mark[k] = 1;//標記爲被選中
- for(j = 0 ; j < n ; j++)
- {
- if( mark[j] == 0 && (dis[j] > (dis[k] + mat[k][j])))//修改剩餘結點的最短距離
- {
- dis[j] = dis[k] + mat[k][j];
- }
- }
- }
- return dis[f];
- }
- int mat[MAX][MAX];
- int main()
- {
- int n,m;
- while(scanf("%d %d",&n,&m))
- {
- int a,b,dis;
- if(n == 0 || m == 0)
- break;
- int i,j;
- for(i = 0 ; i < n;i++)
- for(j = 0 ; j < n; j++)
- mat[i][j] = INF;
- for(i = 0 ; i < m ;i++)
- {
- scanf("%d %d %d",&a,&b,&dis);
- --a,--b;
- if(dis < mat[a][b] || dis < mat[b][a])
- mat[a][b] = mat[b][a] = dis;
- }
- int ans = dijkstra(mat,n,0,n-1);
- printf("%d\n",ans);
- }
- }
╝⑤
╔
二叉堆實現
使用二叉堆(Binary Heap)來保存沒有擴展過的點的距離並維護其最小值,並在訪問每條邊的時候更新,可以把時間複雜度變成O((V+E)logV)。
當邊數遠小於點數的平方時,這種算法相對來說有很好的效果。但是當E=O(V2)時(有時候表現爲不限制邊的條數),用二叉堆的優化反倒會更慢。因爲此時的複雜度是O(V+V*2logV),小於不用堆的實現的O(n²)的複雜度。
另外此時要用鄰接表保存邊,使得擴展邊的總複雜度爲O(E),否則複雜度不會減小。
空間複雜度:這種算法需要一個二叉堆,及其反向指針,另外還要保存距離,所以所用空間爲3V。如果保存路徑則爲4V。
具體思路:先將所有的點插入堆,並將值賦爲極大值(maxint/maxlongint),將原點賦值爲0,通過鬆弛技術(relax)進行更新以及設定爲擴展。
╝②
╔
- int GraphDijk(struct Graph *g, int root, int *parent, int *distance)
- {
- // 將除根結點之外的點都放入堆中,設置所有鍵爲INFINITY
- // 遍歷根結點發出的邊,將其最短路徑設爲相應權值,並維持堆性質
- // RemoveTop,此結點已經取最短路徑,如果爲INFINITY,則終止算法
- // 否則,將其狀態設爲已標記,並設爲根結點
- // loop back
- parent[root] = root;
- int reflection[g->V];
- int heap_real[g->V - 1];
- for (int i=0,j=0; i < g->V; i++) {
- if (i == root) {
- distance[i] = 0;
- } else {
- distance[i] = INFINITY;
- heap_real[j++] = i;
- reflection[i] = j;
- }
- }
- struct Edge *e;
- struct list_t *iter;
- int *heap = heap_real - 1;
- int base = 0; /* euqal to distance[root] */
- int size = g->V - 1;
- int length;
- do {
- iter = list_next(&(g->vertices + root)->link);
- for (; iter; iter = list_next(iter)) {
- e = list_entry(iter, struct Edge, link);
- length = base + e->weight;
- if (length < distance[e->to]) {
- HeapDecreaseKey(heap, size,
- distance, reflection,
- reflection[e->to], length);
- parent[e->to] = root;
- }
- }
- root = HeapRemoveTop(heap, size, distance, reflection);
- base = distance[root];
- if (distance[root] == INFINITY) {
- /* remain nodes in heap is not accessible */
- return g->V - (size + 1); /* 返回強連通分支結點數 */
- }
- } while (size);
- /* successfull end algorightm */
- return g->V;
- }
╝④
再獻上一個實現
╔
- /*很裸很水的最短路,練習二叉堆優化的Dijstra~
- 之前二叉堆優化的Prim敲了好幾遍前後花了不下八個小時調試還是沒有調試成功,
- 但是還好,熟悉了優先隊列的操作。
- 十幾天後的今天重新想起這個,終於調出來了堆優化的Dijstra。理解之後還是蠻簡單的。
- 一些寫法並不是最優的,例如heap的實現中可以減少交換元素等。但是有這個自己寫的AC
- 過的Dijkstra在,以後寫二叉堆優化的Prim/Dijkstra和其它優先隊列的題目就可以拿它對照着Debug了。
- 2011-07-24 23:00
- */
- #include <stdio.h>
- #define MAXN 1200
- #define MAXM 1200000
- #define INF 19930317
- struct node
- {
- int d, v, p;
- }heap[MAXN];
- int pos[MAXN], hl;
- int e[MAXM], cost[MAXM], next[MAXM], g[MAXN], size;
- int m, n, s, t;
- void insert(int u, int v, int w)
- {
- e[++size] = v;
- next[size] = g[u];
- cost[size] = w;
- g[u] = size;
- }
- void swap(int a, int b)
- {
- heap[0] = heap[a];
- heap[a] = heap[b];
- heap[b] = heap[0];
- pos[heap[a].v] = a;
- pos[heap[b].v] = b;
- }
- void heapfy()
- {
- int i = 2;
- while (i <= hl)
- {
- if ((i < hl) && (heap[i + 1].d < heap[i].d))
- i++;
- if (heap[i].d < heap[i >> 1].d)
- {
- swap(i, i >> 1);
- i <<= 1;
- }
- else
- break;
- }
- }
- void decrease(int i)
- {
- while ((i != 1) && (heap[i].d < heap[i >> 1].d))
- {
- swap(i, i >> 1);
- i >>= 1;
- }
- }
- void relax(int u ,int v, int w)
- {
- if (w + heap[pos[u]].d < heap[pos[v]].d)
- {
- heap[pos[v]].p = u;
- heap[pos[v]].d = w + heap[pos[u]].d;
- decrease(pos[v]);
- }
- }
- void delete_min()
- {
- swap(1, hl);
- hl--;
- heapfy();
- }
- void init()
- {
- int u ,v ,w, i;
- scanf("%d%d", &m, &n);
- for (i = 1; i <= m; i++)
- {
- scanf("%d%d%d", &u, &v, &w);
- insert(u, v, w);
- insert(v, u, w);
- }
- s = 1;
- t = n;
- }
- int dijkstra()
- {
- int u, p, i;
- for (i = 1; i <= n; i++)
- {
- heap[i].v = pos[i] = i;
- heap[i].d = INF;
- }
- heap[s].p = s;
- heap[s].d = 0;
- swap(1, s);
- hl = n;
- while (hl)
- {
- u = heap[1].v;
- delete_min();
- p = g[u];
- while (p)
- {
- if (pos[e[p]] <= hl)
- relax(u, e[p], cost[p]);
- p = next[p];
- }
- }
- }
- int main()
- {
- init();
- dijkstra();
- printf("%d\n", heap[pos[t]].d);
- return 0;
- }
╝③
╔
菲波那契堆實現
用類似的方法,使用Fibonacci Heap可以將複雜度降到O(E+VlogV),但實現比較麻煩。因此這裏暫不列舉。
╝②
最短路徑算法之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 遍鬆弛操作仍然會成功,這時,負權迴路上的頂點不會收斂。
╝⑥
Bellman-Ford算法實現
╔
- #include<iostream>
- #include<cstdio>
- using namespace std;
- #define MAX 0x3f3f3f3f
- #define N 1010
- int nodenum, edgenum, original; //點,邊,起點
- typedef struct Edge //邊
- {
- int u, v;
- int cost;
- }Edge;
- Edge edge[N];
- int dis[N], pre[N];
- bool Bellman_Ford()
- {
- for(int i = 1; i <= nodenum; ++i) //初始化
- dis[i] = (i == original ? 0 : MAX);
- for(int i = 1; i <= nodenum - 1; ++i)
- for(int j = 1; j <= edgenum; ++j)
- if(dis[edge[j].v] > dis[edge[j].u] + edge[j].cost) //鬆弛(順序一定不能反~)
- {
- dis[edge[j].v] = dis[edge[j].u] + edge[j].cost;
- pre[edge[j].v] = edge[j].u;
- }
- bool flag = 1; //判斷是否含有負權迴路
- for(int i = 1; i <= edgenum; ++i)
- if(dis[edge[i].v] > dis[edge[i].u] + edge[i].cost)
- {
- flag = 0;
- break;
- }
- return flag;
- }
- void print_path(int root) //打印最短路的路徑(反向)
- {
- while(root != pre[root]) //前驅
- {
- printf("%d-->", root);
- root = pre[root];
- }
- if(root == pre[root])
- printf("%d\n", root);
- }
- int main()
- {
- scanf("%d%d%d", &nodenum, &edgenum, &original);
- pre[original] = original;
- for(int i = 1; i <= edgenum; ++i)
- {
- scanf("%d%d%d", &edge[i].u, &edge[i].v, &edge[i].cost);
- }
- if(Bellman_Ford())
- for(int i = 1; i <= nodenum; ++i) //每個點最短路
- {
- printf("%d\n", dis[i]);
- printf("Path:");
- print_path(i);
- }
- else
- printf("have negative circle\n");
- return 0;
- }
╝⑦
Bellman-Ford算法優化——SPFA算法
循環的提前跳出:在實際操作中,貝爾曼-福特算法經常會在未達到V-1次前就出解,V-1其實是最大值。於是可以在循環中設置判定,在某次循環不再進行鬆弛時,直接退出循環,進行負權環判定。
具體做法是用一個隊列保存待鬆弛的點,然後對於每個出隊的點依次遍歷每個與他有邊相鄰的點(用鄰接表效率較高),如果該點可以鬆弛並且隊列中沒有該點則將它加入隊列中(只有進行鬆弛操作的點纔會對它的鄰接點有影響,也就是說其鄰接點才需要鬆弛操作),如此迭代直到隊列爲空。
SPFA算法實現
╔
- #include <iostream>
- #include <queue>
- using namespace std;
- const long MAXN=10000;
- const long lmax=0x7FFFFFFF;
- typedef struct
- {
- long v;
- long next;
- long cost;
- }Edge;
- Edge e[MAXN];
- long p[MAXN];
- long Dis[MAXN];
- bool vist[MAXN];
- queue<long> q;
- long m,n;//點,邊
- void init()
- {
- long i;
- long eid=0;
- memset(vist,0,sizeof(vist));
- memset(p,-1,sizeof(p));
- fill(Dis,Dis+MAXN,lmax);
- while (!q.empty())
- {
- q.pop();
- }
- for (i=0;i<n;++i)
- {
- long from,to,cost;
- scanf("%ld %ld %ld",&from,&to,&cost);
- e[eid].next=p[from];
- e[eid].v=to;
- e[eid].cost=cost;
- p[from]=eid++;
- //以下適用於無向圖
- swap(from,to);
- e[eid].next=p[from];
- e[eid].v=to;
- e[eid].cost=cost;
- p[from]=eid++;
- }
- }
- void print(long End)
- {
- //若爲lmax 則不可達
- printf("%ld\n",Dis[End]);
- }
- void SPF()
- {
- init();
- long Start,End;
- scanf("%ld %ld",&Start,&End);
- Dis[Start]=0;
- vist[Start]=true;
- q.push(Start);
- while (!q.empty())
- {
- long t=q.front();
- q.pop();
- vist[t]=false;
- long j;
- for (j=p[t];j!=-1;j=e[j].next)
- {
- long w=e[j].cost;
- if (w+Dis[t]<Dis[e[j].v])
- {
- Dis[e[j].v]=w+Dis[t];
- if (!vist[e[j].v])
- {
- vist[e[j].v]=true;
- q.push(e[j].v);
- }
- }
- }
- }
- print(End);
- }
- int main()
- {
- while (scanf("%ld %ld",&m,&n)!=EOF)
- {
- SPF();
- }
- return 0;
- }
╝⑧
最短路徑算法之Floyd-Warshall算法
§3 Floyd-Warshall算法
╔
Floyd-Warshall算法是解決任意兩點間的最短路徑的算法,可以處理有向圖或負權值的最短路徑問題,同時也被用於計算有向圖的傳遞閉包。算法的時間複雜度爲O(n³),空間複雜度爲O(n²)。
Floyd-Warshall算法的原理是動態規劃。
設爲從到的只以集合中的節點爲中間節點的最短路徑的長度。
- 若最短路徑經過點k,則;
- 若最短路徑不經過點k,則。
因此,。
在實際算法中,爲了節約空間,可以直接在原來空間上進行迭代,這樣空間可降至二維。
╝⑨
Floyd-Warshall算法實現
╔
- for(int k =1 ; k <= n ; k ++ ){
- for(int i =1 ; i<= n ; i++){
- for(int j =1 ;j<=n;j++){
- dist[ i ][ j ]= min( dist[ i ][ j ],dist[ i ][ k ]+dist[ k ][ j ] );
- }
- }
- }
如果dist[i][k]或者dist[k][j]不存在,程序中用∞代替。
真正的Floyd算法是一種基於DP(Dynamic Programming)的最短路徑算法。
設圖G中n 個頂點的編號爲1到n。令c [i, j, k]表示從i 到j 的最短路徑的長度,其中k 表示該路徑中的最大頂點,也就是說c[i,j,k]這條最短路徑所通過的中間頂點最大不超過k。因此,如果G中包含邊<i, j>,則c[i, j, 0] =邊<i, j> 的長度;若i= j ,則c[i,j,0]=0;如果G中不包含邊<i, j>,則c (i, j, 0)= +∞。c[i, j, n] 則是從i 到j 的最短路徑的長度。
對於任意的k>0,通過分析可以得到:中間頂點不超過k 的i 到j 的最短路徑有兩種可能:該路徑含或不含中間頂點k。若不含,則該路徑長度應爲c[i, j, k-1],否則長度爲 c[i, k, k-1] +c [k, j, k-1]。c[i, j, k]可取兩者中的最小值。
狀態轉移方程:c[i, j, k]=min{c[i, j, k-1], c [i, k, k-1]+c [k, j, k-1]},k>0。
這樣,問題便具有了最優子結構性質,可以用動態規劃方法來求解。
- #include <iostream>
- 2 using namespace std;
- 3
- 4 const int INF = 100000;
- 5 int n=10,map[11][11],dist[11][11][11];
- 6 void init(){
- 7 int i,j;
- 8 for(i=1;i<=n;i++)
- 9 for(j=1;j<=n;j++)
- 10 map[i][j]=(i==j)?0:INF;
- 11 map[1][2]=2,map[1][4]=20,map[2][5]=1;
- 12 map[3][1]=3,map[4][3]=8,map[4][6]=6;
- 13 map[4][7]=4,map[5][3]=7,map[5][8]=3;
- 14 map[6][3]=1,map[7][8]=1,map[8][6]=2;
- 15 map[8][10]=2,map[9][7]=2,map[10][9]=1;
- 16 }
- 17 void floyd_dp(){
- 18 int i,j,k;
- 19 for(i=1;i<=n;i++)
- 20 for(j=1;j<=n;j++)
- 21 dist[i][j][0]=map[i][j];
- 22 for(k=1;k<=n;k++)
- 23 for(i=1;i<=n;i++)
- 24 for(j=1;j<=n;j++){
- 25 dist[i][j][k]=dist[i][j][k-1];
- 26 if(dist[i][k][k-1]+dist[k][j][k-1]<dist[i][j][k])
- 27 dist[i][j][k]=dist[i][k][k-1]+dist[k][j][k-1];
- 28 }
- 29 }
- 30 int main(){
- 31 int k,u,v;
- 32 init();
- 33 floyd_dp();
- 34 while(cin>>u>>v,u||v){
- 35 for(k=0;k<=n;k++){
- 36 if(dist[u][v][k]==INF) cout<<"+∞"<<endl;
- 37 else cout<<dist[u][v][k]<<endl;
- 38 }
- 39 }
- 40 return 0;
- 41 }
╝⑨
最短路徑算法之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算法實現:
╔
- #include "stdafx.h"
- #include<iostream>
- #define Infinity 65535
- #define MAX 100
- using namespace std;
- //邊尾節點結構體
- struct edgeNode
- {
- int no;//節點序號
- int weight; //此邊權值
- edgeNode *next; //下一條鄰接邊
- };
- //節點信息
- struct vexNode
- {
- char info; //節點序號
- edgeNode *link; //與此節點爲首節點的邊的尾節點鏈表
- };
- //優先隊列元素結構體
- struct PriQue
- {
- int no; //節點元素序號
- int weight; //源點到此節點的權值
- };
- //節點數組
- vexNode adjlist[MAX];
- //添加一個序號爲0節點到其他各節點的最小權值
- int d[MAX];
- //源點到各節點的最小權值
- int lowcost[MAX];
- //各節點對間的最小權值
- int mincost[MAX][MAX];
- //優先隊列
- PriQue queue[2*MAX];
- //建立圖的鄰接表
- void createGraph(vexNode *adjlist,int n,int e)
- {
- int i;
- cout<<"請輸入這些節點的信息:"<<endl;
- for(i=1;i<=n;i++)
- {
- cout<<"節點"<<i<<"的名稱:";
- cin>>adjlist[i].info;
- adjlist[i].link = NULL;
- }
- cout<<"請輸入這些邊的信息:"<<endl;
- int v1,v2;
- edgeNode *p1;
- int weight1;
- for(i=1;i<=e;i++)
- {
- cout<<"邊"<<i<<"的首尾節點:";
- cin>>v1>>v2;
- cout<<"請輸入此邊的權值:";
- cin>>weight1;
- p1 = (edgeNode*)malloc(sizeof(edgeNode));
- p1->no = v2;
- p1->weight = weight1;
- p1->next = adjlist[v1].link;
- adjlist[v1].link = p1;
- }
- //添加節點0,到每一個節點的距離都是0
- adjlist[0].info ='0';
- adjlist[0].link = NULL;
- for(i=n;i>=1;i--)
- {
- d[i] = 0;
- p1 = (edgeNode*)malloc(sizeof(edgeNode));
- p1->no = i;
- p1->weight = 0;
- p1->next = adjlist[0].link;
- adjlist[0].link = p1;
- }
- }
- //bellman_ford算法求節點0到其他各節點的最短距離
- bool bellman_ford(vexNode *adjlist,int *d,int n)
- {
- int i,j;
- d[0] = 0;
- edgeNode *p1;
- for(j=1;j<=n;j++)
- {
- for(i=0;i<=n;i++)
- {
- p1= adjlist[i].link;
- while(p1 != NULL)
- {
- if(d[p1->no]>d[i]+p1->weight)
- d[p1->no] = d[i] + p1->weight;
- p1 = p1->next;
- }
- }
- }
- for(i=0;i<=n;i++)
- {
- p1= adjlist[i].link;
- while(p1 != NULL)
- {
- if(d[p1->no]>d[i]+p1->weight)
- return false;
- p1 = p1->next;
- }
- }
- return true;
- }
- //johnson算法中,需要對每一條邊重新賦權值產生非負的權
- void G_w_to_G1_w1(int *d,const int n)
- {
- int i;
- edgeNode *p1;
- for(i=0;i<=n;i++)
- {
- p1= adjlist[i].link;
- while(p1 != NULL)
- {
- p1->weight = p1->weight + d[i] - d[p1->no];
- p1 = p1->next;
- }
- }
- }
- //保持優先隊列的優先性,以指定源點到每一點的最少距離爲關鍵字
- void keep_heap(PriQue *queue,int &num,int i)
- {
- int smallest = i;
- int left = 2*i,right = 2*i+1;
- if(left<=num&&queue[left].weight<queue[i].weight)
- smallest = left;
- if(right<=num&&queue[right].weight<queue[smallest].weight)
- smallest = right;
- if(smallest != i)
- {
- PriQue q = queue[smallest];
- queue[smallest] = queue[i];
- queue[i] = q;
- keep_heap(queue,num,smallest);
- }
- }
- //插入一個元素到優先隊列中,並保持隊列優先性
- void insert_heap(PriQue *queue,int &num,int no,int wei)
- {
- num += 1;
- queue[num].no = no;
- queue[num].weight = wei;
- int i = num;
- while(i>1&&queue[i].weight<queue[i/2].weight)
- {
- PriQue q1;
- q1 = queue[i/2];
- queue[i/2] = queue[i];
- queue[i] = q1;
- i = i/2;
- }
- }
- //取出隊列首元素
- PriQue heap_extract_min(PriQue *queue,int &num)
- {
- if(num<1)
- return queue[0];
- PriQue que = queue[1];
- queue[1] = queue[num];
- num = num -1;
- keep_heap(queue,num,1);
- return que;
- }
- //dijkstra算法求節點i到其他每一個節點的最短距離
- void dijkstra(vexNode *adjlist,PriQue * queue,int i,const int n,int &num)
- {
- int v = i;
- //lowcost[v] = 0;
- int j;
- for(j=1;j<n;j++)
- {
- edgeNode *p1 = adjlist[v].link;
- while(p1 != NULL)
- {
- if(lowcost[p1->no] > lowcost[v] + p1->weight)
- {
- lowcost[p1->no] = lowcost[v] + p1->weight;
- insert_heap(queue,num,p1->no,lowcost[p1->no]);
- }
- p1 = p1->next;
- }
- v = heap_extract_min(queue,num).no;
- if(v==0)
- {
- cout<<"隊列中沒有節點!"<<endl;
- return;
- }
- }
- }
- int _tmain(int argc, _TCHAR* argv[])
- {
- int cases;
- cout<<"請輸入案例的個數:";
- cin>>cases;
- //用隊列0元素作爲哨兵,如果隊列中沒有元素,則返回隊列0元素
- queue[0].no = 0;
- queue[0].weight = 0;
- while(cases--)
- {
- int n,e;
- cout<<"請輸入節點數:";
- cin>>n;
- cout<<"請輸入邊數:";
- cin>>e;
- //隊列中的元素,初始爲0
- int num = 0;
- int i,j;
- //創建鄰接表
- createGraph(adjlist,n,e);
- cout<<endl;
- memset(d,Infinity,sizeof(d));
- //bellman_ford算法求節點0到其他各節點的最短距離
- bool flag = bellman_ford(adjlist,d,n);
- if(!flag)
- {
- cout<<"此圖存在負迴路,不正確!"<<endl;
- continue;
- }
- //johnson算法中,需要對每一條邊重新賦權值產生非負的權
- G_w_to_G1_w1(d,n);
- //運用dijkstra算法求得每一對節點間的最短距離
- for(i=1;i<=n;i++)
- {
- for(j=1;j<=n;j++)
- lowcost[j] = Infinity;
- lowcost[i] =0;
- dijkstra(adjlist,queue,i,n,num);
- //重新把原值賦值回來,因爲在函數G_w_to_G1_w1()中改變過
- for(j=1;j<=n;j++)
- mincost[i][j] = lowcost[j] + d[j] - d[i];
- }
- cout<<"下面輸出每一對頂點之間的最短距離:"<<endl;
- for(i=1;i<=n;i++)
- for(j=1;j<=n;j++)
- {
- cout<<"頂點("<<i<<":"<<adjlist[i].info<<")到頂點("<<j<<":"<<adjlist[j].info<<")的最短距離爲:"<<mincost[i][j]<<endl;
- }
- }
- system("pause");
- return 0;
- }
╝⑩+1
§5 問題歸約
╔
對於兩個問題A和B,如果使用求解B的一個算法來開發一個求解A的算法,且最壞的情況下算法總時間不會超過最壞情況下求解B的算法運行時間的常量倍,則稱問題A可歸約(reduce)爲問題B。
1.傳遞閉包問題可歸約爲有非負權值的所有對最短路徑問題。
給定兩點u和v,有向圖中從u到v存在一條路徑,當且僅當網中從u到v的路徑長度非零。
2.在邊權沒有限制的網中,(單源點或所有對)最長路徑和最短路徑問題是等價的。
3.作業調度問題可歸約爲差分約束問題。
4.有正常數的差分約束問題等價於無環網中的單源點最長路徑。
5.帶有截止期的作業調度問題可歸約爲(允許帶有負權值的)最短路徑問題。
╝⑩+2
§6 最短路徑的擴展與應用
1.k短路
2.差分約束系統
3.DAG圖上的單源點最短路徑
4.Flyod求最小環
§6 小結
這篇文章把最短路徑的四個算法——Dijkstra,Bellman-Ford,Floyd-Warshall,Johnson從原理到步驟,再從流程到實現都將了,有了一定的認識和理解。如果你有任何建議或者批評和補充,請留言指出,不勝感激,更多參考請移步互聯網。
參考:
①永遠的綠巖:http://2728green-rock.blog.163.com/blog/static/43636790200901211848284/
②NOCOW:http://www.nocow.cn/index.php/Dijkstra%E7%AE%97%E6%B3%95
③oa414:http://www.cnblogs.com/oa414/archive/2011/07/25/2115858.html
④NOCOW:http://www.nocow.cn/index.php/Dijkstra_%E4%BA%8C%E5%8F%89%E5%A0%86%E5%AE%9E%E7%8E%B0_C
⑤IT_元帥:http://www.cnblogs.com/newwy/archive/2010/11/20/1882569.html
⑥infinity:http://www.cppblog.com/infinity/archive/2008/11/11/66621.html
⑦niushuai666:http://blog.csdn.net/niushuai666/article/details/6791765
⑧Lost:http://www.cnblogs.com/zhuangli/archive/2008/07/26/1251869.html
⑨極限定律:http://www.cppblog.com/mythit/archive/2009/04/21/80579.html
⑩wikipedia:http://en.wikipedia.org/wiki/Johnson's_algorithm
⑩+1pleasetojava:http://pleasetojava.iteye.com/blog/1270377
⑩+2Robert Sedgewick:Algorithm in C