核心思想:鬆弛操作
對於邊(u,v),用dist(u)和(u,v)的和嘗試更新dist(v):
dist(v) = min(dist(v) , dist(u)+l(u,v)
注:dist(i)為源點(起點)到i點的距離,l(u,v)為u->v的邊權。
Bellman-Ford的基本操作是進行多次迭代,每一輪迭代對圖上所有邊進行鬆弛操作,直到再一次迭代中沒有點的dist發生變化即可停止迭代。為什麼呢?不妨假設已經沒有dist發生變化了,再進行一輪迭代的話,很顯然,之後的迭代沒有產生任何作用,dist陣列依舊沒有改變,反倒增大了時間複雜度,這不是多此一舉麼。
圖解:
初始:(S為源點)
初始設定為inf無窮大,表示還沒有最短路
S | A | B | C | D | E |
0 | inf | inf | inf | inf | inf |
第一輪迭代:
對S點連出的邊(s->e,s->a)
S | A | B | C | D | E |
0 | 7(0+7) | inf | inf | inf | 5(0+5) |
對A連出的邊(a->c)
S | A | B | C | D | E |
0 | 7 | inf | 9(7+2) | inf | 5 |
對B連出的邊(b->a)
S | A | B | C | D | E |
0 | 7 | inf | 9 | inf | 5 |
dist(B)還沒有找到最短路,更新其他點的最短路徑無意義,故對B點的出邊不進行鬆弛
對C連出的邊(c->b)
S | A | B | C | D | E |
0 | 7 | 7(9+(-2)) | 9 | inf | 5 |
對D連出的邊(d->c,d->a)
S | A | B | C | D | E |
0 | 7 | 7 | 9 | inf | 5 |
dist(D)還沒有找到最短路,更新其他點的最短路徑無意義,故對D點的出邊不進行鬆弛
對E連出的邊(e->d)
S | A | B | C | D | E |
0 | 7 | 7 | 9 | 6(5+1) | 5 |
已經對所有的邊進行了鬆弛操作,第一輪迭代結束
第二輪迭代
對S點連出的邊(s->e,s->a)
S | A | B | C | D | E |
0 | 7 | 7 | 9 | 6 | 5 |
無需更新
對A連出的邊(a->c)
S | A | B | C | D | E |
0 | 7 | 7 | 9 | 6 | 5 |
無需更新
對B連出的邊(b->a)
S | A | B | C | D | E |
0 | 7 | 7 | 9 | 6 | 5 |
無需更新
對C連出的邊(c->b)
S | A | B | C | D | E |
0 | 7 | 7 | 9 | 6 | 5 |
無需更新
對D連出的邊(d->c,d->a)
S | A | B | C | D | E |
0 | 2(6+(-4)) | 7 | 5(6+(-1)) | 6 | 5 |
對E連出的邊(e->d)
S | A | B | C | D | E |
0 | 2 | 7 | 5 | 6 | 5 |
已經對所有的邊進行了鬆弛操作,第二輪迭代結束
第三輪迭代
與第一第二輪同理(此處直接給出迭代結束的結果)
S | A | B | C | D | E |
0 | 2 | 2 | 4 | 6 | 5 |
第四輪迭代
無任何更新,迭代結束,更新完成
演算法分析:
如果最短路存在,一定存在一個不含環的最短路。(理由:對零環和正環,去掉後路徑不會邊長;對負環,若最短路徑中存在負環,那一定不是最短路,負環可以無限繞下去,路徑可以是負無窮)
最短路不含環,那麼一條最短路徑最多經過n-1個點(不含起點),所以最多需要n-1輪鬆弛操作。
複雜度分析:
最多進行n-1次迭代,每次迭代列舉遍歷所有邊,嘗試通過邊進行鬆弛操作,故複雜度為
O(N-1)*O(M)即O(NM),(注:N為點數,M為邊數)
虛擬碼
for (int i = 0; i <= n; i++)
dist[i] = inf;//初始化為無窮大
dist[s] = 0;//s為起點,自己到自己的最短路為0
for (int k = 1; k <= n - 1; k++)//迭代n-1輪
{
for (int i = 1; i <= m; i++)//列舉每一條邊
{
int x = u[i], y = v[i];
if (dist[x] < inf)
dist[y] = min(dist[y], dist[x] + w[i]);//鬆弛
}
}
檢查有無負環
將dist陣列初始化為0,迭代n-1次後進行第n次迭代,如果第n次迭代有進行鬆弛操作,則一定存在負環,因為不存在負環最多隻能進行n-1次鬆弛操作
程式碼實現:
void bellman_ford(int s, int end) // s為起點,end為終點
{
memset(dis, 127, sizeof(dis));
dis[s] = 0; //起點最短路為0
pre[s] = -1;
for (int i = 1; i <= n - 1; i++)
{
bool ok = false;
for (int j = 1; j <= m; j++)
{
int x = edge[j].u, y = edge[j].v, w = edge[j].w;
if (dis[x] < (1 << 30) && dis[x] + w < dis[y])
{
dis[y] = dis[x] + w;
pre[y] = x; // y的上一個點為x,如不需列印路徑無需pre陣列
ok = true;
}
}
if (ok == false)
{
break; //未進行鬆弛操作,提前退出迴圈,減小時間複雜度
}
}
if (dis[end] < (1 << 30))
cout << dis[end] << "\n";
else
cout << "-1\n";
// Print_Path(end); //列印路徑
}
模板題
題目連結:
題目描述:
給你一張簡單有向圖,邊權都為非負整數。以及一些詢問,詢問兩個點之間的距離。
圖用以下形式給出:
第一行輸入三個整數 n,m,k表示圖的頂點數、邊數和詢問次數,頂點編號從 1 到 n。
接下來 m 行,每行三個整數 x,y,z表示 x 到 y 有一條有向邊,邊權為 z。
接下來 k 行,每行兩個整數 x,y 詢問從 x 到 y 的最短路長度,如果無法到達,輸出 −1。
輸入格式:
第一行三個整數 n,m,k 表示圖的頂點數、邊數和詢問次數。
接下來 m 行,每行有三個整數,代表一條邊。
接下來 k 行,每行有兩個整數,代表一次詢問。
輸出格式:
輸出共 k 行,每行一個數表示一次詢問的答案。
資料規模:
對於所有資料,保證 2≤n≤5000,0≤m≤10000,1≤k≤5,1≤x,y≤n,x≠y,1≤z≤10000。
樣例輸入:
3 3 2
1 2 3
2 3 2
3 2 1
1 3
3 1
樣例輸出:
5
-1
直接給程式碼了
參考文獻:
《演算法競賽,入門經典(第二版)》
2022 Namomo Spring Camp Div2 Day8 直播課
ending
有什麼錯誤之處歡迎指正!不勝感激!