幾個最短路徑的演算法
一、floyd
1.介紹
floyd演算法只有五行程式碼,程式碼簡單,三個for迴圈就可以解決問題,所以它的時間複雜度為O(n^3),可以求多源最短路問題。
2.思想:
Floyd演算法的基本思想如下:從任意節點A到任意節點B的最短路徑不外乎2種可能,1是直接從A到B,2是從A經過若干個節點X到B。所以,我們假設Dis(AB)為節點A到節點B的最短路徑的距離,對於每一個節點X,我們檢查Dis(AX) + Dis(XB) < Dis(AB)是否成立,如果成立,證明從A到X再到B的路徑比A直接到B的路徑短,我們便設定Dis(AB) = Dis(AX) + Dis(XB),這樣一來,當我們遍歷完所有節點X,Dis(AB)中記錄的便是A到B的最短路徑的距離。
舉個例子:已知下圖,
如現在只允許經過1號頂點,求任意兩點之間的最短路程,只需判斷e[i][1]+e[1][j]是否比e[i][j]要小即可。e[i][j]表示的是從i號頂點到j號頂點之間的路程。e[i][1]+e[1][j]表示的是從i號頂點先到1號頂點,再從1號頂點到j號頂點的路程之和。其中i是1~n迴圈,j也是1~n迴圈,程式碼實現如下。
for(i=1; i<=n; i++)
{
for(j=1; j<=n; j++)
{
if ( e[i][j] > e[i][1]+e[1][j] )
e[i][j] = e[i][1]+e[1][j];
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
接下來繼續求在只允許經過1和2號兩個頂點的情況下任意兩點之間的最短路程。在只允許經過1號頂點時任意兩點的最短路程的結果下,再判斷如果經過2號頂點是否可以使得i號頂點到j號頂點之間的路程變得更短。即判斷e[i][2]+e[2][j]是否比e[i][j]要小,程式碼實現為如下。
//經過1號頂點
for(i=1; i<=n; i++)
for(j=1; j<=n; j++)
if (e[i][j] > e[i][1]+e[1][j])
e[i][j]=e[i][1]+e[1][j];
//經過2號頂點
for(i=1; i<=n; i++)
for(j=1; j<=n; j++)
if (e[i][j] > e[i][2]+e[2][j])
e[i][j]=e[i][2]+e[2][j];
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
最後允許通過所有頂點作為中轉,程式碼如下:
for(k=1; k<=n; k++)
for(i=1; i<=n; i++)
for(j=1; j<=n; j++)
if(e[i][j]>e[i][k]+e[k][j])
e[i][j]=e[i][k]+e[k][j];
- 1
- 2
- 3
- 4
- 5
- 6
這段程式碼的基本思想就是:最開始只允許經過1號頂點進行中轉,接下來只允許經過1和2號頂點進行中轉……允許經過1~n號所有頂點進行中轉,求任意兩點之間的最短路程。與上面相同
3.程式碼模板:
#include <stdio.h>
#define inf 0x3f3f3f3f
int map[1000][1000];
int main()
{
int k,i,j,n,m;
//讀入n和m,n表示頂點個數,m表示邊的條數
scanf("%d %d",&n,&m);
//初始化
for(i=1; i<=n; i++)
for(j=1; j<=n; j++)
if(i==j)
map[i][j]=0;
else
map[i][j]=inf;
int a,b,c;
//讀入邊
for(i=1; i<=m; i++)
{
scanf("%d %d %d",&a,&b,&c);
map[a][b]=c;//這是一個有向圖
}
//Floyd-Warshall演算法核心語句
for(k=1; k<=n; k++)
for(i=1; i<=n; i++)
for(j=1; j<=n; j++)
if(map[i][j]>map[i][k]+map[k][j] )
map[i][j]=map[i][k]+map[k][j];
//輸出最終的結果,最終二維陣列中存的即使兩點之間的最短距離
for(i=1; i<=n; i++)
{
for(j=1; j<=n; j++)
{
printf("%10d",map[i][j]);
}
printf("\n");
}
return 0;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
二、dijkstra
五.Dijkstra(迪傑斯特拉)
1.演算法介紹:
Dijkstra(迪傑斯特拉)演算法是典型的單源最短路徑演算法,Dijkstra 演算法,用於對有權圖進行搜尋,找出圖中兩點的最短距離,既不是DFS搜尋,也不是BFS搜尋。
把Dijkstra 演算法應用於無權圖,或者所有邊的權都相等的圖,Dijkstra 演算法等同於BFS搜尋。
2.演算法描述
1)演算法思想:設G=(V,E)是一個帶權有向圖,把圖中頂點集合V分成兩組,第一組為已求出最短路徑的頂點集合(用S表示,初始時S中只有一個源點,以後每求得一條最短路徑 , 就將加入到集合S中,直到全部頂點都加入到S中,演算法就結束了),第二組為其餘未確定最短路徑的頂點集合(用U表示),按最短路徑長度的遞增次序依次把第二組的頂點加入S中。在加入的過程中,總保持從源點v到S中各頂點的最短路徑長度不大於從源點v到U中任何頂點的最短路徑長度。此外,每個頂點對應一個距離,S中的頂點的距離就是從v到此頂點的最短路徑長度,U中的頂點的距離,是從v到此頂點只包括S中的頂點為中間頂點的當前最短路徑長度。
例子
重點需要理解這句拗口的”按最短路徑長度的遞增次序依次把第二組的頂點加入S中。在加入的過程中,總保持從源點v到S中各頂點的最短路徑長度不大於從源點v到U中任何頂點的最短路徑長度”
實際上,Dijkstra 演算法是一個排序過程,就上面的例子來說,是根據A到圖中其餘點的最短路徑長度進行排序,路徑越短越先被找到,路徑越長越靠後才能被找到,要找A到F的最短路徑,我們依次找到了
A –> C 的最短路徑 3
A –> C –> B 的最短路徑 5
A –> C –> D 的最短路徑 6
A –> C –> E 的最短路徑 7
A –> C –> D –> F 的最短路徑 9
Dijkstra 演算法執行的附加效果是得到了另一個資訊,A到C的路徑最短,其次是A到B, A到D, A到E, A到F
為什麼Dijkstra 演算法不適用於帶負權的圖?
就上個例子來說,當把一個點選入集合S時,就意味著已經找到了從A到這個點的最短路徑,比如第二步,把C點選入集合S,這時已經找到A到C的最短路徑了,但是如果圖中存在負權邊,就不能再這樣說了。舉個例子,假設有一個點Z,Z只與A和C有連線,從A到Z的權為50,從Z到C的權為-49,現在A到C的最短路徑顯然是A –> Z –> C
對帶負權的圖,應該用Floyd演算法
3.程式碼:
鄰接矩陣
#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std;
const int MAX=0x3f3f3f3f;
int map[110][110];
int dis[110];
int visit[110];
/*
關於三個陣列:map陣列存的為點邊的資訊,比如map[1][2]=3,表示1號點和2號點的距離為3
dis陣列存的為起始點與每個點的最短距離,比如dis[3]=5,表示起始點與3號點最短距離為5
visit陣列存的為0或者1,1表示已經走過這個點。
*/
int n,m;
int dijkstra()
{
int i,j,pos=1,min,sum=0;
memset(visit,0,sizeof(visit));//初始化為0,表示開始都沒走過
for(i=1; i<=n; i++)
{
dis[i]=map[1][i];
}
visit[1]=1;
dis[1]=0;
int T=n-1;
while(T--)
{
min=MAX;
for(j=1; j<=n; j++)
{
if(visit[j]==0&&min>dis[j])
{
min=dis[j];
pos=j;
}
}
visit[pos]=1;//表示這個點已經走過
for(j=1; j<=n; j++)
{
if(visit[j]==0&&dis[j]>min+map[pos][j])//更新dis的值
dis[j]=map[pos][j]+min;
}
}
return dis[n];
}
int main()
{
int i,j;
while(cin>>n>>m)//n表示n個點,m表示m條邊
{
memset(map,MAX,sizeof(map));
int a,b,c;
for(i=1; i<=m; i++)
{
cin>>a>>b>>c;
if(c<map[a][b])//防止有重邊
map[a][b]=map[b][a]=c;
}
int sum=dijkstra();
cout<<sum<<endl;
}
return 0;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
鄰接表實現
#include <stdio.h>
#include <string.h>
#include <string>
#include <vector>
#include <algorithm>
#define INF 0x3f3f3f3f
using namespace std;
struct node
{
int end;//終點
int power;//權值
} t;
int n;//n為邊數
vector<node>q[500001];//鄰接表儲存圖的資訊(相當於一個儲存著結構體的二維陣列)
int dis[500001];//距離陣列
bool vis[500001];//標記陣列
void Dijkstra(int start, int end)
{
memset(vis, false, sizeof(vis));
for(int i=0; i<=n; i++)
{
dis[i] = INF;
}
int len=q[start].size();
for(int i=0; i<len; i++)
{
if(q[start][i].power < dis[q[start][i].end] )
dis[q[start][i].end]=q[start][i].power; //從起點開始的dis陣列更新
}
vis[start]=true;//起點標記為1
for(int k=0; k<n-1; k++)
{
int pos, min=INF;
for(int i=1; i<=n; i++)
{
if( !vis[i] && dis[i]<min )
{
//當前節點未被訪問過且權值較小
min=dis[i];
pos=i;
}
}
vis[pos]=true;
//再次更新dis陣列
len=q[pos].size();
for(int j=0; j<len; j++)
{
if( !vis[q[pos][j].end] && dis[ q[pos][j].end ]>q[pos][j].power+dis[pos] )
dis[q[pos][j].end ] = q[pos][j].power + dis[pos];
}
}
printf("%d\n", dis[end] );
}
int main()
{
int m;
while(scanf("%d %d", &n, &m)&&n&&m)//輸入點和邊
{
for(int i=0; i<=n; i++)
q[i].clear();//將vector陣列清空
for(int i=0; i<m; i++)
{
int begin,end, power;
scanf("%d %d %d", &begin, &end, &power);//輸入
/*t作為node型臨時變數,為了方便壓入,以下程式碼為無向圖的輸入邊*/
t.end=end;
t.power=power;
q[begin].push_back(t); //輸入的資料依次存到q[begin][0]、q[begin][1]裡
t.end=begin;
t.power=power;
q[end].push_back(t);
}
//Dijkstra(1, n);
int start, end;//自己確定起始點和終止點
scanf("%d %d", &start, &end);//輸入起始點和終止點
Dijkstra(start, end);
}
return 0;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
三、Bellman-Ford(貝爾曼-福特)
Dijkstra演算法是處理單源最短路徑的有效演算法,但它侷限於邊的權值非負的情況,若圖中出現權值為負的邊,Dijkstra演算法就會失效,求出的最短路徑就可能是錯的。這時候,就需要使用其他的演算法來求解最短路徑,Bellman-Ford演算法就是其中最常用的一個。該演算法由美國數學家理查德•貝爾曼(Richard Bellman, 動態規劃的提出者)和小萊斯特•福特(Lester Ford)發明。Bellman-Ford演算法的流程如下:
給定圖G(V, E)(其中V、E分別為圖G的頂點集與邊集),源點s,
1.陣列Distant[i]記錄從源點s到頂點i的路徑長度,初始化陣列Distant[n]為, Distant[s]為0;
2.以下操作迴圈執行至多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進行更新,說明最短路徑已經查詢完畢,或者部分點不可達,跳出迴圈。否則執行下次迴圈;
3.為了檢測圖中是否存在負環路,即權值之和小於0的環路。對於每一條邊e(u, v),如果存在Distant[u] + w(u, v) < Distant[v]的邊,則圖中存在負環路,即是說改圖無法求出單源最短路徑。否則陣列Distant[n]中記錄的就是源點s到各頂點的最短路徑長度。
可知,Bellman-Ford演算法尋找單源最短路徑的時間複雜度為O(V*E).
首先介紹一下鬆弛計算。如下圖:
鬆弛計算之前,點B的值是8,但是點A的值加上邊上的權重2,得到5,比點B的值(8)小,所以,點B的值減小為5。這個過程的意義是,找到了一條通向B點更短的路線,且該路線是先經過點A,然後通過權重為2的邊,到達點B。
當然,如果出現一下情況:
則不會修改點B的值,因為3+4>6。
Bellman-Ford演算法可以大致分為三個部分
第一,初始化所有點。每一個點儲存一個值,表示從原點到達這個點的距離,將原點的值設為0,其它的點的值設為無窮大(表示不可達)。
第二,進行迴圈,迴圈下標為從1到n-1(n等於圖中點的個數)。在迴圈內部,遍歷所有的邊,進行鬆弛計算。
第三,遍歷途中所有的邊(edge(u,v)),判斷是否存在這樣情況:d(v) > d (u) + w(u,v),存在則返回false,表示途中存在從源點可達的權為負的迴路。
之所以需要第三部分,是因為,如果存在從源點可達的權為負的迴路。則應為無法收斂而導致不能求出最短路徑。
考慮如下的圖:
經過第一次遍歷後,點B的值變為5,點C的值變為8,這時,注意權重為-10的邊,這條邊的存在,導致點A的值變為-2。(8+ -10=-2)
第二次遍歷後,點B的值變為3,點C變為6,點A變為-4。正是因為有一條負邊在迴路中,導致每次遍歷後,各個點的值不斷變小。
在回過來看一下bellman-ford演算法的第三部分,遍歷所有邊,檢查是否存在d(v) > d (u) + w(u,v)。因為第二部分迴圈的次數是定長的,所以如果存在無法收斂的情況,則肯定能夠在第三部分中檢查出來。比如
此時,點A的值為-2,點B的值為5,邊AB的權重為5,5 > -2 + 5. 檢查出來這條邊沒有收斂。
所以,Bellman-Ford演算法可以解決圖中有權為負數的邊的單源最短路徑問。
程式碼:
int N, M;
typedef struct node
{
int u, v;
int cost;
} E;
node E[N];
int dis[N], pre[N];
bool Bellman()
{
int ok;
for(int i = 1; i <= N; ++i)
dis[i] = (i == 1 ? 0 : MAX);
for(int i = 1; i <= N - 1; ++i)
{
ok=1;
for(int j = 1; j <= M; ++j)
if(dis[E[j].v] > dis[E[j].u] + E[j].cost)
{
dis[E[j].v] = dis[E[j].u] + E[j].cost;
ok=0;
}
if(ok==1)
break;
}
bool flag = 1;
for(int i = 1; i <= M; ++i)
if(dis[E[i].v] > dis[E[i].u] + E[i].cost)
{
flag = 0;
break;
}
return flag;
}
int main()
{
cin>>N>>M;
for(int i = 1; i <= M; ++i)
cin>>E[i].u>>E[i].v>>E[i].cost;
if(Bellman())
cout<<dis[M];
else
cout<<"存在負";
return 0;
}
相關文章
- 最短路徑演算法演算法
- Djikstra最短路徑演算法演算法
- 最短路徑(Dijskra演算法)JS演算法
- 最短路徑(Floyd演算法)演算法
- 最短路徑之Floyd演算法演算法
- 最短路徑之Dijkstra演算法演算法
- [MATLAB]最短路徑Floyd演算法Matlab演算法
- 最短路徑演算法總結演算法
- 圖的最短路徑演算法彙總演算法
- 最短路徑問題 (dijkstra演算法)演算法
- Floyd演算法(計算最短路徑)演算法
- 單源最短路徑-Dijkstra演算法演算法
- 最短路徑——Dijkstra演算法和Floyd演算法演算法
- 多源最短路徑演算法:Floyd演算法演算法
- 單源最短路徑:最短路徑性質的證明
- 0016:單源最短路徑(dijkstra演算法)演算法
- 求最短路徑——DFS+Floyd演算法演算法
- 最短路徑--dijkstra演算法、弗洛伊德(Floyd)演算法(帶路徑輸出)演算法
- 迷宮的最短路徑
- 6.4.2最短路徑
- 圖 - 最短路徑
- 演算法與資料結構之-圖的最短路徑演算法資料結構
- [最短路徑問題]Dijkstra演算法(含還原具體路徑)演算法
- 路徑規劃演算法 - 求解最短路徑 - Dijkstra(迪傑斯特拉)演算法演算法
- 使用A*演算法解迷宮最短路徑問題演算法
- 最短路徑—Dijkstra(迪傑斯特拉)演算法演算法
- 最短路徑——dijkstra演算法程式碼(c語言)演算法C語言
- 最短路徑——floyd演算法程式碼(c語言)演算法C語言
- 求最短路徑-----迪傑斯特拉演算法演算法
- HDU - 3790 (雙標準限制最短路徑)最短路徑問題
- 圖的最短路徑(Dijkstra | Floyd)
- Python小白的數學建模課-16.最短路徑演算法Python演算法
- 最短路徑問題
- 單源最短路徑
- 最短路:求最長最短路
- 最短路 || 最長路 || 次短路
- python實現Dijkstra演算法之 最短路徑問題Python演算法
- 多源最短路徑,一文搞懂Floyd演算法演算法