Dijkstra算法:
解決的問題:
帶權重的有向圖上單源最短路徑問題。且權重都為非負值。如果采用的實現方法合適,Dijkstra運行時間要低於Bellman-Ford算法。
思路:
如果存在一條從i到j的最短路徑(Vi.....Vk,Vj),Vk是Vj前面的一頂點。那么(Vi...Vk)也必定是從i到k的最短路徑。為了求出最短路徑,Dijkstra就提出了以最短路徑長度遞增,逐次生成最短路徑的算法。譬如對於源頂點V0,首先選擇其直接相鄰的頂點中長度最短的頂點Vi,那么當前已知可得從V0到達Vj頂點的最短距離dist[j]=min{dist[j],dist[i]+matrix[i][j]},應用了貪心的思想。根據這種思路,直接給出Dijkstra算法的偽代碼,他可用於計算正權圖的單源最短路徑,同時適用於無向圖和有向圖。
清除所有點的標號
設d[0]=0,其他d[i]=INF
循環n次
{
在所有點的標號中,選出d值最小的結點x
給結點x標記
對於從x出發的所有邊(x,y),更新d[y]=min{d[y],d[x]+w(x,y)}
}
除了求出最短路的長度外,使用Dijkstra算法也能很方便地打印出結點0到所有節點的最短路本身.
代碼實現:
void dijkstra(int start)//從start點開始 { int i,j,k; memset(vis,0,sizeof(vis));//標記是否訪問過 for(i=1; i<=n; i++)//n為總點數 { if(i==start) dis[i]=0; else dis[i]=INF; } for(i=1; i<=n; i++) { int r; int min=INF; for(j=1; j<=n; j++) if(!vis[j]&&dis[j]<min) { min=dis[j]; r=j; } vis[r]=1; for(k=1; k<=n; k++)//對所有從r出發的邊進行松弛 if(dis[k]<(dis[r]+g[r][k])) dis[k]=dis[k]; else dis[k]=dis[r]+g[r][k]; } return; }
Floyd算法:
負權重的邊可以存在,但不能存在權重為負值的環路
算法考慮的是一條最短路徑上的中間結點。
算法核心思想: 三圈for循環
for (int k = 0; k < graph.getNumVex(); k++) { for (int v = 0; v < graph.getNumVex(); v++) { for (int w = 0; w < graph.getNumVex(); w++) { if (d[v][w] > d[v][k] + d[k][w]) { d[v][w] = d[v][k] + d[k][w]; p[v][w] = p[v][k];// p[v][w]是v--w最短路徑上 v的下一頂點 } } } }
第一層 k是作為中間頂點
第二層 v是作為起始頂點
第三層 w是作為終點頂點
內層核心代碼:
以v為起點,w為終點,再以k作為v和w之間的中間點,去判斷d[v][ w]和d[v][k] + d[k][w]的大小關系,如果d[v][w] > d[v][k] + d[k][w],說明找到從v→w的更短路徑了,此時更改d[v][w]的值為d[v][k] + d[k][w]。
p[v][w]的值也要相應改成p[v][k]的值,因為 p[v][k]的值是v→k最短路徑上v的后繼頂點,而v→w這段最短路徑是連接在v→k這段路徑后面的,所以令所當然p[v][w]也要指向p[v][k]。
注意:最外層的k循環,前面的n此循環的結果跟后面n+1次循環的錯做過程是息息相關,
三次循環完成后,各個頂點之間的最短路徑權重會存儲在d矩陣中:d[i][j]表示i→j的最短路徑權重。
鄰接矩陣算法實現:
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; } } }
Bellman-Ford算法
解決的問題:
一般情況下的單源最短路徑問題,這里權重可以為負值。
Bellman-ford算法返回一個布爾值,一表明是否存在一個從源結點可以到達的權重為負的環路。如果存在這樣一個環路,算法將告訴我們不存在解決方案,如果沒有這種環路的存在算法將給出最短路徑和他們的權重。
Bellman-Ford算法的流程如下:
給定圖G(V, E)(其中V、E分別為圖G的頂點集與邊集),源點s,數組Distant[i]記錄從源點s到頂點i的路徑長度,初始化數組Distant[n]為, Distant[s]為0;
以下操作循環執行至多n-1次,n為頂點數:
對於每一條邊e(u, v),如果Distant[u] + w(u, v) < Distant[v],則另Distant[v] = Distant[u]+w(u, v)。w(u, v)為邊e(u,v)的權值;
若上述操作沒有對Distant進行更新,說明最短路徑已經查找完畢,或者部分點不可達,跳出循環。否則執行下次循環;
為了檢測圖中是否存在負環路,即權值之和小於0的環路。對於每一條邊e(u, v),如果存在Distant[u] + w(u, v) < Distant[v]的邊,則圖中存在負環路,即是說改圖無法求出 單源最短路徑。否則數組Distant[n]中記錄的就是源點s到各頂點的最短路徑長度。
可知,Bellman-Ford算法尋找單源最短路徑的時間復雜度為O(V*E).
Bellman-Ford算法可以大致分為三個部分
第一,初始化所有點。每一個點保存一個值,表示從原點到達這個點的距離,將原點的值設為0,其它的點的值設為無窮大(表示不可達)。
第二,進行循環,循環下標為從1到n-1(n等於圖中點的個數)。在循環內部,遍歷所有的邊,進行松弛計算。
第三,遍歷途中所有的邊(edge(u,v)),判斷是否存在這樣情況:
d(v) > d (u) + w(u,v)
則返回false,表示途中存在從源點可達的權為負的回路。
之所以需要第三部分的原因,是因為,如果存在從源點可達的權為負的回路。則 應為無法收斂而導致不能求出最短路徑。
#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) { if(i==original) dis[i]=0; else dis[i]=MAX; } for(int i = 1; i <= nodenum - 1; ++i)//循環n-1次 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; printf("%d ",dis[edge[j].v]); pre[edge[j].v] = edge[j].u; } printf("%d ",dis[edge[j].v]); } 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; }
spfa算法:
算法流程
算法大致流程是用一個隊列來進行維護。初始時將源加入隊列。每次從隊列中取出一個元素,並對所有與他相鄰的點進行松弛,若某個相鄰的點松弛成功,則將其入隊。直到隊列為空時算法結束。
這個算法,簡單的說就是隊列優化的bellman-ford,利用了每個點不會更新次數太多的特點發明的此算法
SPFA——Shortest Path Faster Algorithm,它可以在O(kE)的時間復雜度內求出源點到其他所有點的最短路徑,可以處理負邊。SPFA的實現甚至比Dijkstra或者Bellman_Ford還要簡單:
設Dist代表S到I點的當前最短距離,Fa代表S到I的當前最短路徑中I點之前的一個點的編號。開始時Dist全部為+∞,只有Dist[S]=0,Fa全部為0。
維護一個隊列,里面存放所有需要進行迭代的點。初始時隊列中只有一個點S。用一個布爾數組記錄每個點是否處在隊列中。
每次迭代,取出隊頭的點v,依次枚舉從v出發的邊v->u,設邊的長度為len,判斷Dist[v]+len是否小於 Dist[u],若小於則改進Dist[u],將Fa[u]記為v,並且由於S到u的最短距離變小了,有可能u可以改進其它的點,所以若u不在隊列中,就將它放入隊尾。這樣一直迭代下去直到隊列變空,也就是S到所有的最短距離都確定下來,結束算法。若一個點入隊次數超過n,則有負權環。
SPFA 在形式上和寬度優先搜索非常類似,不同的是寬度優先搜索中一個點出了隊列就不可能重新進入隊列,但是SPFA中一個點可能在出隊列之后再次被放入隊列,也就是一個點改進過其它的點之后,過了一段時間可能本身被改進,於是再次用來改進其它的點,這樣反復迭代下去。設一個點用來作為迭代點對其它點進行改進的平均次數為k,有辦法證明對於通常的情況,k在2左右。
代碼模板:
SPFA void Spfa() { for (int i(0); i<num_town; ++i)//初始化 { dis[i] = MAX; visited[i] = false; } queue<int> Q; dis[start] = 0; visited[start] = true; Q.push(start); while (!Q.empty()){ int temp = Q.front(); Q.pop(); for (int i(0); i<num_town; ++i) { if (dis[temp] + road[temp][i] < dis[i])//存在負權的話,就需要創建一個COUNT數組,當某點的入隊次數超過V(頂點數)返回。 { dis[i] = dis[temp] + road[temp][i]; if (!visited[i]) { Q.push(i); visited[i] = true; } } } visited[temp] = false; } }
四種算法總結完了,都是東拼西湊的,自己學的也不好,還是靜下心來好好學吧。也許有一天,你發覺日子特別的艱難,那可能是這次的收獲將特別的巨大。這幾天總是在抱怨生活,患得患失,卻忘了自己為什么留下來暑期集訓,因為你什么都沒有,所以你必須努力!噶嗚!~加油
————Anonymous.PJQ