每一對頂點之間的最短路徑【Floyd】
演算法總結:
①Dijkstra演算法用的是貪心策略,每次都找當前最短路徑的下一個最短距離點。所以不適合帶有負權的情況。至於時間效率通過各種優化可以到達不同的程度。但是樸素的Dijkstra演算法永遠是最穩定的。
②Bellman-Ford演算法是Dijkstra的一種變式,它摒棄了貪心的策略,但是每次都需要鬆弛所有的路徑,所以也適合負權的情況。但是時間效率較低。有資料顯示,Bellman-Ford演算法也可以應用貪心策略,這屬於高階技巧,這裡不予考慮。
③Floyd演算法用的是動態規劃的思想,由於是不定源點,所以它需要對所有的情況進行鬆弛操作,最終獲得所有的最短路徑長度。由於需要遍歷,所以它的時間效率比較低。但是遍歷策略好的話,還是能很快得到想要的結果。
④SPFA演算法是用FIFO佇列優化的Bellman-Ford演算法,所以支援負權的情況,由於已經將需要考慮的節點放入佇列中,所以避免了很多不必要的鬆弛,時間效率也相對較高。由於佇列中無法進行修改,所以時間效率相對不穩定。
⑤所有的佇列優化其實都能轉化為hash優化,這裡不再考慮更進一步的優化。
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
一、Dijkstra演算法:單源到所有節點的最短路演算法。
演算法要求:可以是無向圖或有向圖,所有邊權均為正,最短路存在。如果圖是五環圖,則最長路也一定存在,但是如果是有環圖,則最長路不一定存在。
演算法思想:每次比較所有的從源點可以到達的路的長度,然後選出最短的一條放入源點集合,然後更新最短路徑長度。這樣,當所有的點都在源點集合中時,得到的長度就是最短路長度。
(1)使用鄰接矩陣的稠密圖普通Dijkstra演算法
演算法時間複雜度:O(n^2)
演算法程式碼:
#include <iostream> #include <cstdio> #include <cstring> using namespace std; #define INF (1<<31)-1 //int最大值 #define MAXN 100 //最大節點數 int dist[MAXN],c[MAXN][MAXN]; //dist[i]存放從源點到i節點的最短距離,c陣列用來表示圖 int n,line; //n為節點數,line為邊數 //迪傑斯特拉演算法主演算法模組 void Dijkstra(int v) //v代表源點 { inti,j,temp,u; //temp用於暫時存放最短路徑長度 boolvis[MAXN]; //判定重複標記陣列 for(i=0; i<n; i++) //dist陣列與vis陣列的初始化 { dist[i]=c[v][i]; vis[i]=0; } dist[v]=0; //源點到源點的距離設為0 vis[v]=1; //源點預設為已經訪問 for(i=0; i<n; i++) //總共應該遍歷n次 { temp=INF; //初始化temp為最大值 u=v; //初始化最短節點為當前節點 for(j=0; j<n; j++)if((!vis[j])&&dist[j]<temp) { u=j; //找出v距離最短的節點u temp=dist[j]; //更新最短距離 } if(temp==INF)return; //不存在其它節點了,返回 vis[u]=1; //標記該最短節點為已經訪問 for(j=0; j<n; j++)if((!vis[j])&&dist[u]+c[u][v]<dist[j]) //鬆弛操作 dist[j]=dist[u]+c[u][v]; //更新最短距離 } } int main() { int p,q,len,i,j,origin; while(scanf(“%d%d”,&n,&line)) { if(!n&&!line) break; //如果圖為空白圖,則結束 for(i=0; i<n; i++) for(j=0; j<n; j++) c[i][j]=INF; //初始化節點間距離,預設為節點間全部不連通 while(line--) //輸入這line條邊的內容 { scanf(“%d%d%d”,&p,&q,&len); //p為邊的起點,q為邊的終點,len為邊的長度 //下面是無向圖的輸入方式 if(len<c[p][q]) { c[p][q]=len; c[q][p]=len; } /*下面是有向圖的輸入方式 if(len<c[p][q]) c[p][q]=lem; 以上是有向圖的輸入方式*/ } for(i=0; i<n; i++) dist[i]=INF; //初始化最短距離陣列為最大長度 scanf(“%d”,&origin); Dijkstra(origin); //最短路演算法呼叫 printf(“%d\n”,dist[n]); } }
(2)使用鄰接表的稀疏圖的普通Dijkstra演算法
演算法時間複雜度:O(m*log(n))
演算法程式碼:(程式碼以無向圖為例,有向圖部分以註釋形式寫在程式碼中)
###標頭檔案以及巨集定義部分省略###
int first[MAXN]; //first[u]儲存節點u的第一條邊的編號
int u[MAXN],v[MAXN],w[MAXN];
//u[i]表示第i條邊的一端端點,v[i]表示第i條邊的另一端端點,w[i]表示第i條邊的長度
int next[2*MAXN]; //next[i]表示第i條邊的下一條邊的編號【無向圖】
//int next[MAXN]; next[i]表示第i條邊的下一條邊的編號。【有向圖】
#########省略部分#########
scanf(“%d%d”,&n,&m);
for(i=0; i<n; i++) first[i]=-1; //初始化連結串列表頭
for(i=0; i<2*line; i++) //輸入2*line條邊,每次將邊首插法插入連結串列表頭(避免遍歷連結串列)。
//如果是有向圖,那麼只有line條邊,即i<line。
{
scanf(“%d%d%d”,&u[i],&v[i],&w[i]);
next[i]=first[u[i]];
first[u[i]]=i;
//以下只有無向圖才有
i++;
u[i]=v[i-1];
v[i]=u[i-1];
next[i]=first[u[i]];
first[u[i]]=i;
//以上只有無向圖才有
}
//對應的Dijkstra演算法
void Dijkstra(int x) //v代表源點
{
inti,j,temp,minu; //temp用於暫時存放最短路徑長度
boolvis[MAXN]; //判定重複標記陣列
for(i=first[x]; i!=-1; i=next[i]) //dist陣列與vis陣列的初始化
{
dist[v[i]]=w[i];
vis[v[i]]=0;
}
dist[x]=0; //源點到源點的距離設為0
vis[x]=1; //源點預設為已經訪問
for(i=0; i<n; i++) //總共應該遍歷n次
{
temp=INF; //初始化temp為最大值
minu=x; //初始化最短節點為當前節點
for(j=first[x]; j!=-1; j=next[j])
{
if((!vis[v[j]])&&dist[v[j]]<temp)
{
minu=j; //找出v距離最短的節點uu
temp=dist[v[j]]; //更新最短距離
}
}
if(temp==INF)return; //不存在其它節點了,返回
vis[minu]=1; //標記該最短節點為已經訪問
for(j=first[x]; j!=-1; j=next[j])if((!vis[v[j]])&&dist[minu]+w[j]<dist[v[j]]) //鬆弛操作
dist[v[j]]=dist[minu]+w[j]; //更新最短距離
}
}
(3)使用優先佇列的Dijkstra優化演算法
演算法時間複雜度:O(n*lgn+ m)。一般情況下最快。
演算法程式碼:
#include <queue> //需要優先佇列
#include <utility> //需要pair型別
######其他標頭檔案已經巨集定義省略######
int first[MAXN]; //first[u]儲存節點u的第一條邊的編號
int u[MAXN],v[MAXN],w[MAXN];
//u[i]表示第i條邊的一端端點,v[i]表示第i條邊的另一端端點,w[i]表示第i條邊的長度
int next[2*MAXN]; //next[i]表示第i條邊的下一條邊的編號【無向圖】
//int next[MAXN]; next[i]表示第i條邊的下一條邊的編號。【有向圖】
#########省略部分#########
scanf(“%d%d”,&n,&m);
for(i=0;i<n;i++) first[i]=-1; //初始化連結串列表頭
for(i=0;i<2*line;i++) //輸入2*line條邊,每次將邊首插法插入連結串列表頭(避免遍歷連結串列)。
//如果是有向圖,那麼只有line條邊,即i<line。
{
scanf(“%d%d%d”,&u[i],&v[i],&w[i]);
next[i]=first[u[i]];
first[u[i]]=i;
//以下只有無向圖才有
i++;
u[i]=v[i-1];
v[i]=u[i-1];
next[i]=first[u[i]];
first[u[i]]=i;
//以上只有無向圖才有
}
typedef pair<int,int> pii; //定義雙物件型別
priority_queue<pii,vector<pii>,greater<pii>> pq; //宣告最小出隊的優先佇列
//對應的Dijkstra演算法
int Dijkstra(int x)
{
inti,j;
pq.push(make_pair(d[x],x);
while(!pq.empty())
{
piiminu=pq.top();pq.pop();
intx=minu.second;
if(minu.first!=dist[x])continue;
for(inti=first[x];i!=-1;i=next[i]) if(d[v[i]]>dist[x]+w[i])
{
dist[v[i]]=dist[x]+w[i];
pq.push(make_pair(d[v[i]],v[i]));
}
}
}
二、Bellman-Ford演算法:單源到所有節點的最短最長路演算法。
演算法要求:可以使無向圖或者有向圖。邊權可以存在負值。當然最短路不一定存在,當最短路存在時,可以求出最短路長度。如果圖為有環圖,則最短路不一定存在,最長路也不一定存在。如有負權則輸出錯誤提示。也適合求解約束拆分系統的不等式問題。
演算法思想:如果最短路存在,一定存在一個不含環的最短路。在邊權可正可負的圖中,環有零環、正環、負環3種。如果包含零環或正環,去掉以後路徑不會變長;如果包含負環,則最短路不存在。可以通過n-1輪鬆弛操作得到。
(1)樸素的BF演算法。
演算法時間複雜度:O(mn)
演算法程式碼:
const int N =205;
const int M =20005;
const int MAXN = 1000000000
int dist[N];
//自定義邊的結構體
struct edge{int u,v,w;}e[M]; //u,v分別是邊的兩端點,w為邊長度
//初始化dist陣列
void init(int vs,int s)
{
inti;
for(i=0;i<vs;i++)
dist[i]=MAXN;
dist[s]=0;
return;
}
//鬆弛操作,成功返回true,失敗返回false
bool relax(int u,int v,int w)
{
if(dist[v]>dist[u]+w)
{
dist[v]=dist[u]+w;
returntrue;
}
return false;
}
//BF主演算法模組,返回false表示演算法失敗,圖中存在負環。
bool bellmanFord(int es,int vs,int s) //es表示邊數,vs表示點數,s表示起點
{
inti,j;
init(vs,s);
boolflag;
for(i=0;i<vs-1;i++) //應進行vs-1次鬆弛操作
{
flag=false;
for(j=0;j<es;j++)
if(relax(e[j].u,e[j].v,e[j].w))
flag=true;
return flag;
}
}
int main()
{
intn,m,i;
while(scanf(“%d%d”,&n,&m)!=EOF)
{
for(i=0;i<m;i++)
{
scanf(“%d%d%d”,&e[i].u,&e[i].v,&e[i].w);
e[m+i].u=e[i].v;
e[m+i].v=e[i].u;
e[m+i].w=e[i].w;
}
if(bellmanFord(m<<1,n,1))
printf(“%d\n”,dist[n]);
else printf(“No\n”);
}
return 0;
}
(2)使用FIFO佇列的優化BF演算法(使用鄰接表)
演算法時間複雜度:O(mn)
演算法程式碼:
#include <queue>
#define INF (1<<31)-1
######其他標頭檔案以及巨集定義省略######
int first[MAXN]; //first[u]儲存節點u的第一條邊的編號
int u[MAXN],v[MAXN],w[MAXN];
//u[i]表示第i條邊的一端端點,v[i]表示第i條邊的另一端端點,w[i]表示第i條邊的長度
int next[2*MAXN]; //next[i]表示第i條邊的下一條邊的編號【無向圖】
//int next[MAXN]; next[i]表示第i條邊的下一條邊的編號。【有向圖】
#########省略部分#########
queue<int> q;
bool inq[MAXN];
bool bellmanFord(int x)
{
int i,j;
bool ans;
for(i=0; i<n; i++)d[i]=!i?0:INF;
memset(inq,0,sizeof(inq)); //在佇列中的標誌
q.push(x);
ans=false;
while(!q.empty())
{
int x=q.front();
q.pop();
inq[x]=false;
for(i=first[x]; i!=-1; i=next[i]) if(dist[v[e]]>dist[x]+w[e])
{
dist[v[e]]=dist[x]+w[e];
ans=true;
if(!inq[v[e]])
{
inq[v[e]]=true;
q.push(v[e]);
}
}
}
return ans;
}
三、Floyd演算法:任意兩點之間的最短路
演算法思想:動態規劃。
時間複雜度:O(n^3)
演算法程式碼:
#define MAXN 100
#define INF (1<<31)-1
int n,m,p,q;
int f[MAXN+10][MAXN+10];
void Floyd()
{
inti,j,k;
for(k=0;k<n;k++)
{
for(i=0;i<n;i++)
{
for(j=0;j<n;j++)
{
if(f[i][k]+f[k][j]<f[i][j])
f[i][j]=f[i][k]+f[k][j];
}
}
}
if(f[0][n-1]==INF) printf(“0\n”);
else printf(“%d\n”,f[0][n-1]);
}
int main()
{
inta,b,c,i;
while(~scanf(“%d%d”,&n,&m))
{
if(!n&&!m) break;
for(p=0;p<n;p++)
for(q=0;q<n;q++)
f[p][q]=INF;
for(i=0;i<m;i++)
{
scanf(“%d%d%d”,&a,&b,&c);
f[a][b]=c;
f[b][a]=c;
}
floyd();
}
return 0;
}
四、SPFA演算法:單源點最短路的高效實用演算法
演算法要求:無特殊要求
演算法思想:用FIFO佇列優化的BF演算法。
演算法時間複雜度:O(k|E|),k為常數,一般k<=2
演算法程式碼:
#include <queue>
#######省略部分######
#define INF (1<<31)-1
#define N 1010
int dist[N],n,m;
int edge[N][N];
bool vis[N];
void spfa(int s)
{
inti,u;
memset(vis,false,sizeof(vis));
for(i=0;i<n;i++)dist[i]=INF;
queue<int>q;
q.push(s);
vis[s]=true;
dist[s]=0;
while(!q.empty())
{
u=q.front();
q.pop();
vis[u]=false;
for(i=0;i<n;i++)
{
if(dist[i]>dist[u]+edge[u][i])
dist[i]=dist[u]+edge[u][i];
if(!vis[i])
{
vis[i]=true;
q.push(i);
}
}
}
}
int main()
{
inti,j,a,b,c,origin;
while(scanf(“%d%d”,&n,&m)!=EOF&&(n||m))
{
for(i=0;i<N;i++)
for(j=0;j<i;j++)
{
edge[i][j]=INF;
edge[j][i]=INF;
}
},
for(i=0;i<m;i++)
{
scanf(“%d%d%d”,&a,&b,&c);
if(edge[a][b]>c)
{
edge[a][b]=c;
edge[b][a]=c;
}
scanf(“%d”,&origin);
spfa(origin);
printf(“%d\n”,dist[n]);
}
return 0;
}