一、最短路徑問題介紹
1、問題解釋: 從圖中的某個頂點出發到達另外一個頂點的所經過的邊的權重和最小的一條路徑,稱爲最短路徑
2、解決問題的算法:
- 迪傑斯特拉算法(Dijkstra算法)
- 弗洛伊德算法(Floyd算法)
- SPFA算法
- bellman-ford算法
二、Dijkstra算法
1、算法特點:Dijkstra(迪傑斯特拉)算法是典型的單源最短路徑算法,用於計算一個節點到其他所有節點的最短路徑。主要特點是以起始點爲中心向外層層擴展,直到擴展到終點爲止。Dijkstra算法是很有代表性的最短路徑算法,在很多專業課程中都作爲基本內容有詳細的介紹,如數據結構,圖論,運籌學等等。注意該算法要求圖中不存在負權邊。
2、問題描述:在無向圖 G=(V,E) 中,假設每條邊 E[i] 的長度爲 w[i],找到由頂點 V0 到其餘各點的最短路徑。(單源最短路徑)
3、算法思想:設G=(V,E)是一個帶權有向圖,把圖中頂點集合V分成兩組,第一組爲已求出最短路徑的頂點集合(用S表示,初始時S中只有一個源點,以後每求得一條最短路徑 , 就將加入到集合S中,直到全部頂點都加入到S中,算法就結束了),第二組爲其餘未確定最短路徑的頂點集合(用U表示),按最短路徑長度的遞增次序依次把第二組的頂點加入S中。在加入的過程中,總保持從源點v到S中各頂點的最短路徑長度不大於從源點v到U中任何頂點的最短路徑長度。此外,每個頂點對應一個距離,S中的頂點的距離就是從v到此頂點的最短路徑長度,U中的頂點的距離,是從v到此頂點只包括S中的頂點爲中間頂點的當前最短路徑長度。
4、算法步驟:
a.初始時,S只包含源點,即S={v},v的距離爲0。U包含除v外的其他頂點,即:U={其餘頂點},若v與U中頂點u有邊,則<u,v>正常有權值,若u不是v的出邊鄰接點,則<u,v>權值爲∞。
b.從U中選取一個距離v最小的頂點k,把k,加入S中(該選定的距離就是v到k的最短路徑長度)。
c.以k爲新考慮的中間點,修改U中各頂點的距離;若從源點v到頂點u的距離(經過頂點k)比原來距離(不經過頂點k)短,則修改頂點u的距離值,修改後的距離值的頂點k的距離加上邊上的權。
d.重複步驟b和c直到所有頂點都包含在S中。
5、執行動畫過程如下圖:
5、算法代碼實現:
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int MAXN = 0x3f3f3f3f;
const int p = 1005;
int a[p][p];//存放點與點的距離
int dist[p];//源點到各個點的最短距離
int pre[p];//計算過的點
int s[p];//判斷是否已經加入pre,是否已訪問
int n,m;
void Dijkstra(int v)
{
int i,j;
for(i = 0;i < n;++i)
{
if(i != v)
{
dist[i] = a[v][i];
}
s[i] = false;
}
s[v] = true;
dist[v] = 0;
pre[0]=v;
//對各個數組進行初始化
for(i = 0;i < n;++i)
{
int minset = MAXN;
int u = v;
for(j = 0; j < n;++j)
{
if(!s[j] && dist[j] < minset)
//找到剩餘節點中最小的節點
{
u = j;
minset = dist[u];
}
}
s[u] = true;
//將節點標記爲以訪問,相當於加入s數組中
for(j = 0;j < n;++j)
{
if(!s[j] && a[u][j] < MAXN)
//a[u][j] < MAXN指更新u的可達點
{
if(dist[u] + a[u][j] < dist[j])
{
dist[j] = dist[u] + a[u][j];
pre[j] = u;
//儲存得出最短路徑的前一個節點,用於路徑還原
}
}
}
}
}
int main()
{
int t;
cin>>t;
while(~scanf("%d %d",&n,&m))
{
memset(a,MAXN,sizeof(a));
memset(dist,MAXN,sizeof(dist));
int i,j;
for(i = 0;i < m;++i)
{
int x,y,d;
scanf("%d %d %d",&x,&y,&d);
a[x-1][y-1]=d;
a[y-1][x-1]=d;
}
//int v;
//scanf("%d",&v);
Dijkstra(0);
for(int i = 0;i < n;++i)
{
printf("%d ",dist[i]);
}
printf("\n");
/*for(int i = 0;i < n;++i)
{
printf("%d ",s[i]);
}
printf("\n");
for(int i = 0;i < n;++i)
{
printf("%d ",pre[i]);
}
printf("\n");*/
}
return 0;
}
6、算法實例
先給出一個無向圖:
用Dijkstra算法找出以A爲起點的單源最短路徑步驟如下:
三、Floyd算法
1、定義:Floyd-Warshall算法(Floyd-Warshall algorithm)是解決任意兩點間的最短路徑的一種算法,可以正確處理有向圖或負權的最短路徑問題,同時也被用於計算有向圖的傳遞閉包。Floyd-Warshall算法的時間複雜度爲O(N3),空間複雜度爲O(N2)。
2、算法思想原理:Floyd算法是一個經典的動態規劃算法。用通俗的語言來描述的話,首先我們的目標是尋找從點i到點j的最短路徑。從動態規劃的角度看問題,我們需要爲這個目標重新做一個詮釋(這個詮釋正是動態規劃最富創造力的精華所在)。
從任意節點i到任意節點j的最短路徑不外乎2種可能,1是直接從i到j,2是從i經過若干個節點k到j。所以,我們假設Dis(i,j)爲節點u到節點v的最短路徑的距離,對於每一個節點k,我們檢查Dis(i,k) + Dis(k,j) < Dis(i,j)是否成立,如果成立,證明從i到k再到j的路徑比i直接到j的路徑短,我們便設置Dis(i,j) = Dis(i,k) + Dis(k,j),這樣一來,當我們遍歷完所有節點k,Dis(i,j)中記錄的便是i到j的最短路徑的距離。
3、算法描述:
a.從任意一條單邊路徑開始。所有兩點之間的距離是邊的權,如果兩點之間沒有邊相連,則權爲無窮大。
b.對於每一對頂點 u 和 v,看看是否存在一個頂點 w 使得從 u 到 w 再到 v 比己知的路徑更短。如果是更新它。
3).Floyd算法過程矩陣的計算----十字交叉法
方法:兩條線,從左上角開始計算一直到右下角 如下所示
給出矩陣,其中矩陣A是鄰接矩陣,而矩陣Path記錄u,v兩點之間最短路徑所必須經過的點。
相應計算方法如下:
最後A3即爲所求結果
4、算法代碼實現:
(1)用Floyd求無向圖的最小環
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int MAXN = 100005;
const int p = 105;
int dist[p][p];
int mp[p][p];
int main()
{
int n,m;
while(~scanf("%d %d",&n,&m))
{
int i,j,k;
for(i = 0;i < n;++i)
{
for(j = 0;j < n;++j)
{
dist[i][j] = MAXN;
mp[i][j] = MAXN;
}
}
for(i = 0;i < m;++i)
{
int a,b,c;
scanf("%d %d %d",&a,&b,&c);
if(c < mp[a-1][b-1])
dist[a-1][b-1]=dist[b-1][a-1]=mp[b-1][a-1]=mp[a-1][b-1]=c;
}
int ans = MAXN;
for(k = 0;k < n;++k)
{
for(i = 0;i < n;++i)
{
for(j = i+1;j < n;++j)
//這裏用i+1,防止i,j是不同的兩個點
{
if(ans > dist[i][j] + mp[i][k] + mp[k][j])
{
ans = dist[i][j] + mp[i][k] + mp[k][j];
}
}
}
for(i = 0;i < n;++i)
{
for(j = 0;j < n;++j)
{
if(dist[i][j] > dist[i][k] + dist[k][j])
{
dist[i][j] = dist[i][k] + dist[k][j];
}
}
}
}
if(ans == MAXN)
printf("It's impossible.\n");
else
printf("%d\n",ans);
}
return 0;
}
(2)Floyd的模板:
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int MAXN = 0x3f3f3f3f;
const int p = 1005;
int dist[p][p];
int path[p][p];
int mp[p][p];
int n,m;
int Floyd()
{
int i,j,k;
for(i = 0;i < n;++i)
{
for(j = 0;j < n;++j)
{
dist[i][i]=0;
mp[i][i]=0;
}
}
int ans = MAXN;
for(k = 0;k < n;++k){
for(i = 0;i < n;++i)
{
for(j = 0;j < n;++j)
{
if(dist[i][k] + dist[k][j] < dist[i][j])
{
dist[i][j] = dist[i][k] + dist[k][j];
path[i][j] = k;
}
}
}
}
return ans;
}
int main()
{
int t;
cin>>t;
while(t--)
{
int i,j;
memset(path,-1,sizeof(path));
memset(dist,MAXN,sizeof(dist));
memset(mp,MAXN,sizeof(mp));
scanf("%d %d",&n,&m);
for(int i = 0;i < m;++i)
{
int x,y,d;
scanf("%d %d %d",&x,&y,&d);
dist[x][y]=d;
dist[y][x]=d;
mp[x][y]=d;
mp[y][x]=d;
}
printf("%d\n",Floyd());
for(i = 0;i < n;++i)
{
for(j = 0;j < n;++j)
{
printf("%d ",dist[i][j]);
}
cout<<endl;
}
for(i = 0;i < n;++i)
{
for(j = 0;j < n;++j)
{
printf("%d ",path[i][j]);
}
cout<<endl;
}
}
return 0;
}
四、SPFA算法
1、SPFA(Shortest Path Faster Algorithm)算法是求單源最短路徑的一種算法,它是Bellman-ford的隊列優化,它是一種十分高效的最短路算法。很多時候,給定的圖存在負權邊,這時類似Dijkstra等算法便沒有了用武之地,而Bellman-Ford算法的複雜度又過高,SPFA算法便派上用場了。SPFA的複雜度大約是O(kE),k是每個點的平均進隊次數(一般的,k是一個常數,在稀疏圖中小於2)。
但是,SPFA算法穩定性較差,在稠密圖中SPFA算法時間複雜度會退化。
2、實現方法:建立一個隊列,初始時隊列裏只有起始點,在建立一個表格記錄起始點到所有點的最短路徑(該表格的初始值要賦爲極大值,該點到他本身的路徑賦爲0)。然後執行鬆弛操作,用隊列裏有的點去刷新起始點到所有點的最短路,如果刷新成功且被刷新點不在隊列中則把該點加入到隊列最後。重複執行直到隊列爲空。
此外,SPFA算法還可以判斷圖中是否有負權環,即一個點入隊次數超過N。
3、算法圖解:
給定一個有向圖,求A~E的最短路。
源點A首先入隊,並且AB鬆弛
擴展與A相連的邊,B,C 入隊並鬆弛。
B,C分別開始擴展,D入隊並鬆弛
D出隊,E入隊並鬆弛。
E出隊,此時隊列爲空,源點到所有點的最短路已被找到,A->E的最短路即爲8
以上就是SPFA算法的過程。
4、算法模板:
#include "bits/stdc++.h"
using namespace std;
const int maxN = 200010 ;
struct Edge
{
int to , next , w ;
} e[ maxN ];
int n,m,cnt,p[ maxN ],Dis[ maxN ];
int In[maxN ];
bool visited[ maxN ];
void Add_Edge ( const int x , const int y , const int z )
{
e[ ++cnt ] . to = y ;
e[ cnt ] . next = p[ x ];
e[ cnt ] . w = z ;
p[ x ] = cnt ;
return ;
}
bool Spfa(const int S)
{
int i,t,temp;
queue<int> Q;
memset ( visited , 0 , sizeof ( visited ) ) ;
memset ( Dis , 0x3f , sizeof ( Dis ) ) ;
memset ( In , 0 , sizeof ( In ) ) ;
Q.push ( S ) ;
visited [ S ] = true ;
Dis [ S ] = 0 ;
while( !Q.empty ( ) )
{
t = Q.front ( ) ;Q.pop ( ) ;visited [ t ] = false ;
for( i=p[t] ; i ; i = e[ i ].next )
{
temp = e[ i ].to ;
if( Dis[ temp ] > Dis[ t ] + e[ i ].w )
{
Dis[ temp ] =Dis[ t ] + e[ i ].w ;
if( !visited[ temp ] )
{
Q.push(temp);
visited[temp]=true;
if(++In[temp]>n)return false;
}
}
}
}
return true;
}
int main ( )
{
int S , T ;
scanf ( "%d%d%d%d" , &n , &m , &S , &T ) ;
for(int i=1 ; i<=m ; ++i )
{
int x , y , _ ;
scanf ( "%d%d%d" , &x , &y , &_ ) ;
Add_Edge ( x , y , _ ) ;
}
if ( !Spfa ( S ) ) printf ( "FAIL!\n" ) ;
else printf ( "%d\n" , Dis[ T ] ) ;
return 0;
}
五、bellman-ford算法
1、單源最短路徑。給定一個圖,和一個源頂點src,找到從src到其它所有所有頂點的最短路徑,圖中可能含有負權值的邊。Dijksra的算法是一個貪婪算法,時間複雜度是O(VLogV)(使用最小堆)。但是迪傑斯特拉算法在有負權值邊的圖中不適用,Bellman-Ford適合這樣的圖。在網絡路由中,該算法會被用作距離向量路由算法。Bellman-Ford也比迪傑斯特拉算法更簡單和同時也適用於分佈式系統。但Bellman-Ford的時間複雜度是O(VE),這要比迪傑斯特拉算法慢。(V爲頂點的個數,E爲邊的個數)
2、算法描述
輸入:圖 和 源頂點src
輸出:從src到所有頂點的最短距離。如果有負權迴路(不是負權值的邊),則不計算該最短距離,沒有意義,因爲可以穿越負權迴路任意次,則最終爲負無窮。
3、算法步驟
1.初始化:將除源點外的所有頂點的最短距離估計值 dist[v] ← +∞, dist[s] ←0;
2.迭代求解:反覆對邊集E中的每條邊進行鬆弛操作,使得頂點集V中的每個頂點v的最短距離估計值逐步逼近其最短距離;(運行|v|-1次)
3.檢驗負權迴路:判斷邊集E中的每一條邊的兩個端點是否收斂。如果存在未收斂的頂點,則算法返回false,表明問題無解;否則算法返回true,並且從源點可達的頂點v的最短距離保存在 dist[v]中。
關於該算法的證明也比較簡單,採用反證法,具體參考:http://courses.csail.mit.edu/6.006/spring11/lectures/lec15.pdf
該算法是利用動態規劃的思想。該算法以自底向上的方式計算最短路徑。
它首先計算最多一條邊時的最短路徑(對於所有頂點)。然後,計算最多兩條邊時的最短路徑。外層循環需要執行|V|-1次。
4、例子
一下面的有向圖爲例:給定源頂點是0,初始化源頂點距離所有的頂點都是是無窮大的,除了源頂點本身。因爲有5個頂點,因此所有的邊需要處理4次。
按照以下的順序處理所有的邊:(B,E), (D,B), (B,D), (A,B), (A,C), (D,C), (B,C), (E,D).
第一次迭代得到如下的結果(第一行爲初始化情況,最後一行爲最終結果):
當 (B,E), (D,B), (B,D) 和 (A,B) 處理完後,得到的是第二行的結果。
當 (A,C) 處理完後,得到的是第三行的結果。
當 (D,C), (B,C) 和 (E,D) 處理完後,得到第四行的結果。
第一次迭代保證給所有最短路徑最多隻有1條邊。當所有的邊被第二次處理後,得到如下的結果(最後一行爲最終結果):
第二次迭代保證給所有最短路徑最多隻有2條邊。我們還需要2次迭代(即所謂的鬆弛操作),就可以得到最終結果。
5、代碼
/*-------------------------------------
* Bellman-Ford算法(單源最短路徑)
*
------------------------------------*/
#include <iostream>
using namespace std;
//表示一條邊
struct Edge{
// 起點
int src;
// 終點
int dest;
// 權重
int weight;
};
//帶權值的有向圖
struct Graph{
// 頂點的數量
int V;
// 邊的數量
int E;
// 用邊的集合 表示一個圖
Edge* edge;
};
// 創建圖
Graph* CreateGraph(int v,int e){
Graph* graph = (Graph*)malloc(sizeof(Graph));
graph->E = e;
graph->V = v;
graph->edge = (Edge*)malloc(e*sizeof(Edge));
return graph;
}
// 打印結果
void Print(int dist[],int n){
cout<<"單源最短路徑:"<<endl;
for(int i = 0;i < n;++i){
if(dist[i] == INT_MAX){
cout<<"與節點"<<i<<"距離->無窮大"<<endl;
}//if
else{
cout<<"與節點"<<i<<"距離->"<<dist[i]<<endl;
}
}//for
}
// 單源最短路徑
bool BellmanFord(Graph* graph,int src){
int v = graph->V;
int e = graph->E;
// 存儲距離
int dist[v];
// 初始化
for(int i = 0;i < v;++i){
dist[i] = INT_MAX;
}//for
dist[src] = 0;
// v-1次操作
Edge edge;
int a,b,weight;
for(int i = 1;i < v;++i){
// 對e條邊進行鬆弛
for(int j = 0;j < e;++j){
edge = graph->edge[j];
a = edge.src;
b = edge.dest;
weight = edge.weight;
if(dist[a] != INT_MAX && dist[a]+weight < dist[b]){
dist[b] = dist[a]+weight;
}//if
}//for
}//for
// 檢測負權迴路
bool isBack = false;
for(int i = 0;i < e;++i){
edge = graph->edge[i];
a = edge.src;
b = edge.dest;
weight = edge.weight;
if(dist[a] != INT_MAX && dist[a]+weight < dist[b]){
isBack = true;
break;
}//if
}//for
// 打印結果
Print(dist,v);
return isBack;
}
int main(){
int v = 7;
int e = 9;
Graph* graph = CreateGraph(v,e);
graph->edge[0].src = 0;
graph->edge[0].dest = 1;
graph->edge[0].weight = -1;
graph->edge[1].src = 0;
graph->edge[1].dest = 2;
graph->edge[1].weight = 4;
graph->edge[2].src = 1;
graph->edge[2].dest = 2;
graph->edge[2].weight = 3;
graph->edge[3].src = 1;
graph->edge[3].dest = 3;
graph->edge[3].weight = 2;
graph->edge[4].src = 1;
graph->edge[4].dest = 4;
graph->edge[4].weight = 2;
graph->edge[5].src = 3;
graph->edge[5].dest = 2;
graph->edge[5].weight = 5;
graph->edge[6].src = 3;
graph->edge[6].dest = 1;
graph->edge[6].weight = 1;
graph->edge[7].src = 4;
graph->edge[7].dest = 3;
graph->edge[7].weight = -3;
graph->edge[8].src = 5;
graph->edge[8].dest = 6;
graph->edge[8].weight = 2;
bool result = BellmanFord(graph,0);
if(result){
cout<<"圖中存在迴路"<<endl;
}//if
else{
cout<<"圖中不存在迴路"<<endl;
}//else
return 0;
}
6、適用範圍:
- 單源最短路徑(從源點到其他所有點v);
- 有向圖&無向圖;
- 邊權可正可負
- 差分約束系統
六、四大算法對比
bellman-ford可以用於邊權爲負的圖中,圖裏有負環也可以,如果有負環,算法會檢測出負環。
時間複雜度O(VE);
dijkstra只能用於邊權都爲正的圖中。
時間複雜度O(n2);
spfa是個bellman-ford的優化算法,本質是bellman-ford,所以適用性和bellman-ford一樣。(用隊列和鄰接表優化)。
時間複雜度O(KE);
floyd可以用於有負權的圖中,即使有負環,算法也可以檢測出來,可以求任意點的最短路徑,有向圖和無向圖的最小環和最大環。
時間複雜度O(n3);
任何題目中都要注意的有四點事項:圖是有向圖還是無向圖、是否有負權邊,是否有重邊,頂點到自身的可達性。
參考:https://blog.csdn.net/qq_35644234/article/details/60870719
https://www.cnblogs.com/biyeymyhjob/archive/2012/07/31/2615833.html