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
