轉自:https://www.cnblogs.com/xzxl/p/7232929.html
一、相關定義
最短路徑:求源點到某特定點的最短距離
特點:Bellman-Ford算法主要是針對有負權值的圖,來判斷該圖中是否有負權回路或者存在最短路徑的點
局限性:算法效率不高,不如SPFA算法
時間復雜度:O(mn)
具體與dijkstra算法的比較
Bellman-Ford算法為何需要循環n-1次來求解最短路徑?Dijkstra從源點開始,更新dis[],找到最小值,再更新dis[]……每次循環都可以確定一個點的最短路。Bellman-Ford算法同樣也是這樣,它的每次循環也可以確定一個點的最短路,只不過代價很大,因為 Bellman-Ford每次循環都是操作所有邊。既然代價這么大,相比Dijkstra 算法,Bellman-Ford算法還有啥用?因為后者可以檢測負權回路啊。Bellman-Ford 算法的時間復雜度為 O(nm),其中 n 為頂點數,m 為邊數。
負權回路
開始不懂,看了下面的圖和我的算法描述后就懂了:
在循環n-1次的基礎上再次遍歷各邊,對於所有邊,只要存在一條邊e(u, v)使得 dis[u] + w(u,v) < dis[v],則該圖存在負權回路。
【松弛操作】
如左圖所示,松弛計算之前,點B的值是8,但是點A的值加上邊上的權重2,得到5,比點B的值(8)小,所以,點B的值減小為5。這個過程的意義是,找到了一條通向B點更短的路線,且該路線是先經過點A,然后通過權重為2的邊,到達點B。
當然,如果出現右邊這種情況,則不會修改點B的值,因為3+4>6。
二、算法描述
關鍵詞:初始化 松弛操作
主要變量如下:
int n 表示有n個點,從1~n標號
int s,t s為源點,t為終點
int dis[N] dis[i]表示源點s到點i的最短路徑
int pre[N] 記錄路徑,pre[i]表示i的前驅結點
bool vis[N] vis[i]=true表示點i被標記
初始化
dis數組全部賦值為INF,pre數組全部賦值為-1(表示還不知道前驅),
dis[s] = 0 表示源點不要求最短路徑(或者最短路徑就是0)。
松弛操作
對於每一條邊e(u, v),如果dis[u] + w(u, v) < dis[v],則另dis[v] = dis[u]+w(u, v)。w(u, v)為邊e(u,v)的權值
注:上述循環執行至多n-1次。若上述操作沒有對Distant進行更新,說明最短路徑已經查找完畢或者部分點不可達,跳出循環;否則執行下次循環。
檢測負權回路
為了檢測圖中是否存在負環路,即權值之和小於0的環路。
檢測的方法很簡單,只需在求解最短路徑的 n-1 次循環基礎上,再進行第 n 次循環:對於每一條邊e(u, v),如果存在邊使得dis[u] + w(u, v) < dis[v],則圖中存在負環路,即是說改圖無法求出單源最短路徑。否則數組Distant[n]中記錄的就是源點s到各頂點的最短路徑長度。
小結
Bellman-Ford算法可以大致分為三個部分:
- 初始化所有點。每一個點保存一個值,表示從原點到達這個點的距離,將原點的值設為0,其它的點的值設為無窮大(表示不可達)。
- 進行循環,循環下標為從1到n-1(n等於圖中點的個數)。在循環內部,遍歷所有的邊,進行松弛計算。
- 遍歷途中所有的邊(edge(u,v)),判斷是否存在這樣情況: d(v) > d (u) + w(u,v),若存在,則返回false,表示圖中存在從源點可達的權為負的回路。
之所以需要第三步的原因,是因為,如果存在從源點可達的權為負的回路,則將因為無法收斂而導致不能求出最短路徑。
三、代碼實現
#include<iostream>
#include<stack>
using namespace std;
const int MAX = 10000; //假設權值最大不超過10000
struct Edge
{
int u;
int v;
int w;
};
Edge edge[10000]; //記錄所有邊
int dist[100]; //源點到頂點i的最短距離
int path[100]; //記錄最短路的路徑
int vertex_num; //頂點數
int edge_num; //邊數
int source; //源點
bool BellmanFord()
{
//初始化
for (int i = 0; i < vertex_num; i++)
dist[i] = (i == source) ? 0 : MAX;
//n-1次循環求最短路徑
for (int i = 1; i <= vertex_num - 1; i++)
{
for (int j = 0; j < edge_num; j++)
{
if (dist[edge[j].v] > dist[edge[j].u] + edge[j].w)
{
dist[edge[j].v] = dist[edge[j].u] + edge[j].w;
path[edge[j].v] = edge[j].u;
}
}
}
bool flag = true; //標記是否有負權回路
//第n次循環判斷負權回路
for (int i = 0; i < edge_num; i++)
{
if (dist[edge[i].v] > dist[edge[i].u] + edge[i].w)
{
flag = false;
break;
}
}
return flag;
}
void Print()
{
for (int i = 0; i < vertex_num; i++)
{
if (i != source)
{
int p = i;
stack<int> s;
cout << "頂點 " << source << " 到頂點 " << p << " 的最短路徑是: ";
while (source != p) //路徑順序是逆向的,所以先保存到棧
{
s.push(p);
p = path[p];
}
cout << source;
while (!s.empty()) //依次從棧中取出的才是正序路徑
{
cout << "--" << s.top();
s.pop();
}
cout << " 最短路徑長度是:" << dist[i] << endl;
}
}
}
int main()
{
cout << "請輸入圖的頂點數,邊數,源點:";
cin >> vertex_num >> edge_num >> source;
cout << "請輸入" << edge_num << "條邊的信息:\n";
for (int i = 0; i < edge_num; i++)
cin >> edge[i].u >> edge[i].v >> edge[i].w;
if (BellmanFord())
Print();
else
cout << "Sorry,it have negative circle!\n";
return 0;
}
四、實戰
https://acm.ecnu.edu.cn/problem/1817/
求出有 n (1<n<600) 個結點有向圖中,結點 1 到結點 n 的最短路徑。
輸入格式
第一行有 2 個整數 n,m (0<m≤n(n−1)2),接下來 m 行每行有三個整數 u,v,w 結點 u 到 v 有一條權為 w 的邊 (w<106)。
輸出格式
輸出結點 1 到結點 n 之間的最短路徑,如果 1 到 n 之間不存在路徑,輸出 −1。
樣例
input
3 3
1 2 10
2 3 15
1 3 30
output
25
AC代碼
#include<iostream>
#include<vector>
using namespace std;
/**
* @brief Bellman-Ford單源最短路徑算法
*
* @return int
*/
const int MAXN = 610;
const int MAXM = MAXN * MAXN;
const int INF = 1e6 + 10;
struct Edge{
int from, to;
int length;
Edge(int f, int t, int l): from(f), to(t), length(l) {}
};
vector<Edge> edges;
int dis[MAXN];
void Bellman_Ford(int start, int n, int m){
//初始化
for(int i = 1; i <= n; ++i){
dis[i] = INF;
}
dis[start] = 0;
//循環n-1次
for(int i = 1; i < n; ++i){
//對每一條邊進行松弛操作
for(int j = 0; j < m; ++j){
int from = edges[j].from;
int to = edges[j].to;
int d = edges[j].length;
if(dis[from] + d < dis[to]){
dis[to] = dis[from] + d;
}
}
}
//判斷是否存在負回路(再來一次遍歷)
/*for(int j = 0; j < m; ++j){
int from = edges[j].from;
int to = edges[j].to;
int d = edges[j].length;
if(dis[from] + d < dis[to]){
return 0;
}
}
return 1;*/
}
int main(){
int n, m;
int from, to, length;
scanf("%d%d", &n, &m);
for(int i = 0; i < m; ++i){
scanf("%d%d%d", &from, &to, &length);
edges.push_back(Edge(from, to, length));
}
Bellman_Ford(1, n, m);
if(dis[n] == INF){
printf("-1\n");
}
else{
printf("%d\n", dis[n]);
}
//system("pause");
return 0;
}
五、遇到的坑
之前筆者學習別的單源最短路徑代碼的時候,了解到可將INF
定義為INT_MAX
,即如下代碼
#include<climits>
const int INF = INT_MAX;
但是在提交題解時最后一個測試點通不過,查看測試數據input並進行測試,發現輸出為-2147483575
這個很離譜的結果。在將INF
的定義改為如下形式,則順利通過最后一個測試點。
const int INF = 1e6 + 10;
分析得出結論如下:
采用方案1,即INF = INT_MAX
,當遇到dis[from]為INF的情況是,dis[from] + d
超過INF
,即超過int型整數所能表示的最大整數,也就是溢出了,則此時dis[from] + d
的值變為等於或者接近INT_MIN
,即int型整數所能表示的最小整數。此時,dis[to] > INT_MIN
也就是 if
判斷條件必定滿足,進而dis[to]
就被賦值為一個接近或者等於INT_MIN
的值,bug就由此產生。可能會導致終點的dis始終為接近或等於INT_MIN
而得不到更新,也就是筆者測試時出現的-2147483575
這樣離譜的結果
用下面的列子說明一下:

input
3 3
2 3 15
1 2 10
1 3 30
1 | 2 | 3 | |
---|---|---|---|
Init | 0 | inf | inf |
round1 edge1 | 0 | inf | -inf |
round1 edge2 | 0 | 10 | -inf |
round1 edge3 | 0 | 10 | -inf |
round2 edge1 | 0 | 10 | -inf |
round2 edge2 | 0 | 10 | -inf |
round2 edge3 | 0 | 10 | -inf |
知道了哪里出現問題,修改就很容易了,只需要在if條件判斷即可
if(dis[from] != INF && dis[from] + d < dis[v])
而定義為INF = 1e6 + 10
,則不會出現這樣的上述bug,原因是即便是dis[from]=INF
,只要dis[from] + d
沒有超過int型變量所能表示的最大整數即可。但是此值需要根據題目描述來確定。
六、改進
《挑戰程序設計競賽》中關於Bellman Ford算法的代碼如下:
若在n-1次的循環中,某一次遍歷所有的邊后,並沒有更新dis,則算法運行結束
#include<iostream>
#include<vector>
#include<string.h>
#include<climits>
using namespace std;
/**
* @brief Bellman-Ford單源最短路徑算法
*
* @return int
*/
const int MAXN = 610;
const int MAXM = MAXN * MAXN;
const int INF = INT_MAX;
struct Edge{
int from, to;
int length;
Edge(int f, int t, int l): from(f), to(t), length(l) {}
};
vector<Edge> edges;
int dis[MAXN];
void Bellman_Ford(int start, int n, int m){
//初始化
for(int i = 1; i <= n; ++i){
dis[i] = INF;
}
dis[start] = 0;
while(true){
//update標記此趟遍歷是否有更新
bool update = false;
for(int i = 0; i < m; ++i){
Edge e = edges[i];
if(dis[e.from] != INF && dis[e.from] + e.length < dis[e.to]){
dis[e.to] = dis[e.from] + e.length;
update = true;
}
}
if(update == false){ //若此趟沒有更新則算法結束
break;
}
}
}
bool find_negative_loop(int n, int m){
memset(dis, 0, sizeof(dis));
for(int i = 0; i < n; ++i){
for(int j = 0; j < m; ++j){
Edge e = edges[j];
if(dis[e.from] + e.length < dis[e.to]){
dis[e.to] = dis[e.from] + e.length;
if(i == n - 1){
return false;
}
}
}
}
return true;
}
int main(){
int n, m;
int from, to, length;
scanf("%d%d", &n, &m);
for(int i = 0; i < m; ++i){
scanf("%d%d%d", &from, &to, &length);
edges.push_back(Edge(from, to, length));
}
Bellman_Ford(1, n, m);
if(dis[n] == INF){
printf("-1\n");
}
else{
printf("%d\n", dis[n]);
}
system("pause");
return 0;
}