853. 有邊數限制的最短路
Bellman-Ford演算法
演算法思路:
for n次
for 所有邊 a,b,w 表示存在一條a->b 的邊,權重是 w
dist[b]=min(dist[b],dist[a]+w)
結束!
概念:瞭解即可
對於所有的邊都滿足 dist[b]<=dist[a]+w ,這個叫三角不等式,更新操作叫鬆弛操作。
Bellman_Ford適用於負權邊的,
有負權迴路的話,最短路是不一定存在的。為什麼是不一定,可以看圖1和圖2.
圖1描述了一直走下去一直小,那麼,最短路徑就是負無窮,不存在最短路徑。
圖2描述了雖然有負權迴路,但由於此負權迴路不在1~n的路線中,不影響最短路,所以,也還存在最短路徑。
Bellman_Ford是可以找負環的,但時間複雜度比較高,一般不用它來找負環。
Bellman_Ford能幹的事,SPFA都能幹,而且時間複雜度比Bellman_Ford要低。
但有一個例外,就是如果限制了邊數的最短路,那麼只能用Bellman_Ford演算法。
演算法邏輯
1)初始化所有點到源點的距離為∞,把源點到自己的距離設定為0;
2)不管3721遍歷n次;每次遍歷m條邊,用每一條邊去更新各點到源點的距離。
值得注意的是
-
⭐️需要把dist陣列進行一個備份,這樣防止每次更新的時候出現串聯;
-
⭐️由於存在負權邊,因此return -1的條件就要改成dist[n]>0x3f3f3f3f/2;
-
⭐️上面所謂的n次遍歷的實際含義是當前的最短路徑最多有n-1條邊,這也就解釋了為啥要i遍歷到n的時候退出迴圈了,因為只有n個點,最短路徑無環最多就存在n-1條邊。
-
⭐️這裡無需對重邊和自環做單獨的處理:
1] 重邊:由於遍歷了所有的邊,總會遍歷到較短的那一條; 2] 自環: 有自環就有自環啊,反正又不會死迴圈; -
⭐️令人愉悅的是,該演算法無非就是迴圈n次然後遍歷所有的邊,因此不需要做什麼特別的儲存,只要把所有的邊的資訊存下來能夠遍歷就行;
6)⭐️bellman_ford演算法可以存在負權迴路,因為它求得的最短路是有限制的,是限制了邊數的,這樣不會永久的走下去,會得到一個解;
- ⭐️SPFA演算法各方面優於該演算法,但是在碰到限制了最短路徑上邊的長度時就只能用bellman_ford了,此時直接把n重迴圈改成k次迴圈即可
理解與感悟
(1)本題明確存在負權邊,所以不能使用Dijkstra演算法,Dijkstra只能用在正權邊。
(2)有邊數限制的最短路徑
bellman - ford演算法擅長解決有邊數限制的最短路問題
這個有邊數限制就很霸氣了,其它演算法沒有這個功能!
比個現實中的場景:旅遊N個城市,1->n沒有直達的飛機,我們需要經其它的城市進行中轉,
票價就是中轉的機票票價之和,每次中轉都會讓人的心情不爽一點,也就是不能超過k次中轉。
(3)Bellman_ford是可以用來判斷負環的,但由於時間複雜度較高,一般使用SPFA來判斷負環。
(4)什麼情況下存在負環,還存在最短路徑?
比如
上面這張圖之所以不存在負環,是因為2號點不在1->n點的路徑上,就是有負環,也無謂!
(5)、為什麼需要backup[a]陣列
為了避免如下的串聯情況, 在邊數限制為一條的情況下,節點3的距離應該是3,但是由於串聯情況,利用本輪更新的節點2更新了節點3的距離,所以現在節點3的距離是2。
正確做法是用上輪節點2更新的距離--無窮大,來更新節點3, 再取最小值,所以節點3離起點的距離是3。
C++ 程式碼
#include <iostream>
#include <cstring>
using namespace std;
const int N = 510, M = 10010;
//bellman_ford演算法可以使用結構體,不用使用鄰接表,很牛B,為什麼Dijkstra不行呢?
struct Edge {
int a, b, w; //起點,終點和權重
} edges[M];
int n, m, k;
int dist[N];
int backup[N];
int bellman_ford() {
//初始化
memset(dist, 0x3f, sizeof dist);
dist[1] = 0;
for (int i = 0; i < k; i++) { //不超過k條邊的最短距離
//把dist陣列備份一下,只用上一次迭代的結果,防止出現串聯,因為本次迭代可以造成幾個節點資料的修改
memcpy(backup, dist, sizeof dist);
for (int j = 0; j < m; j++) {
auto e = edges[j];
dist[e.b] = min(dist[e.b], backup[e.a] + e.w);
}
}
return dist[n];
}
int main() {
//輸入
scanf("%d%d%d", &n, &m, &k); //不超過k次
for (int i = 0; i < m; i++) {
int a, b, w;
scanf("%d%d%d", &a, &b, &w);
edges[i] = {a, b, w};
}
//呼叫bellman_ford演算法
int res = bellman_ford();
//為什麼要用dist[n]>0x3f3f3f3f/2? 這可能是因為在操作過程中dist[n]被更新成0x3f3f3f3f小一點的數,其實還是無法到達
if (res > 0x3f3f3f3f / 2) puts("impossible");
else printf("%d\n", res);
return 0;
}