圖論
題目
94. 城市間貨物運輸 I
Bellmen_ford 隊列優化算法 SPFA
大家可以發現 Bellman_ford 算法每次松弛 都是對所有邊進行松弛。
但真正有效的松弛,是基于已經計算過的節點在做的松弛。
本圖中,對所有邊進行松弛,真正有效的松弛,只有松弛邊(節點1->節點2) 和邊(節點1->節點3) 因此只要記錄上一次松馳過的邊即可
模擬過程
我們依然使用minDist數組來表達起點到各個節點的最短距離,例如minDist[3] = 5 表示起點到達節點3 的最小距離為5初始化,起點為節點1,起點到起點的最短距離為0,所以minDist[1] 為 0。將節點1 加入隊列 (下次松弛從節點1開始)
從隊列里取出節點1,松弛節點1 作為出發點連接的邊(節點1 -> 節點2)和邊(節點1 -> 節點3)
邊:節點1 -> 節點2,權值為1 ,minDist[2] > minDist[1] + 1
,更新 minDist[2] = minDist[1] + 1 = 0 + 1 = 1
。邊:節點1 -> 節點3,權值為5 ,minDist[3] > minDist[1] + 5,更新 minDist[3] = minDist[1] + 5 = 0 + 5 = 5
。將節點2、節點3 加入隊列
從隊列里取出節點2,松弛節點2 作為出發點連接的邊(節點2 -> 節點4)和邊(節點2 -> 節點5)
邊:節點2 -> 節點4,權值為1 ,minDist[4] > minDist[2] + (-3) ,更新 minDist[4] = minDist[2] + (-3) = 1 + (-3) = -2 。邊:節點2 -> 節點5,權值為2 ,minDist[5] > minDist[2] + 2 ,更新 minDist[5] = minDist[2] + 2 = 1 + 2 = 3 。將節點4,節點5 加入隊列
從隊列里出去節點3,松弛節點3 作為出發點連接的邊。
因為沒有從節點3作為出發點的邊,所以這里就從隊列里取出節點3就好,不用做其他操作,如圖:
從隊列中取出節點4,松弛節點4作為出發點連接的邊(節點4 -> 節點6)邊:節點4 -> 節點6,權值為4 ,minDist[6] > minDist[4] + 4
,更新 minDist[6] = minDist[4] + 4 = -2 + 4 = 2
。將節點6加入隊列
從隊列中取出節點5,松弛節點5作為出發點連接的邊(節點5 -> 節點3),邊(節點5 -> 節點6)
邊:節點5 -> 節點3,權值為1 ,minDist[3] > minDist[5] + 1
,更新 minDist[3] = minDist[5] + 1 = 3 + 1 = 4
。邊:節點5 -> 節點6,權值為-2 ,minDist[6] > minDist[5] + (-2)
,更新 minDist[6] = minDist[5] + (-2) = 3 - 2 = 1
。如圖,將節點3加入隊列,因為節點6已經在隊列里所以不用重復添加
所以我們在加入隊列的過程可以有一個優化,用visited數組記錄已經在隊列里的元素,已經在隊列的元素不用重復加入,從隊列中取出節點6,松弛節點6 作為出發點連接的邊。節點6作為終點,沒有可以出發的邊。同理從隊列中取出節點3,也沒有可以出發的邊,所以直接從隊列中取出。
這樣我們就完成了基于隊列優化的bellman_ford的算法模擬過程。大家可以發現基于隊列優化的算法,要比bellman_ford 算法減少很多無用的松弛情況,特別是對于邊數眾多的大圖優化效果明顯。
代碼實現類似于 dijkstra 的優化版本,需要將圖以鄰接表的形式構建
#include <iostream>
#include <vector>
#include <list>
#include <queue>
#include <climits>using namespace std;struct Edge {int to;int val;Edge(int t, int w): to(t), val(w) {}
};int main() {int n, m, x, y, val;cin >> n >> m;vector<list<Edge>> grid(n+1);vector<bool> isInQueue(n+1);// 構造鄰接表for (int i = 0; i < m; ++i) {cin >> x >> y >> val;grid[x].push_back({Edge(y, val)});}int start = 1;int end = n;vector<int> minDist(n+1, INT_MAX);minDist[start] = 0;queue<int> que;que.push(start);// 使用隊列while (!que.empty()) {int cur = que.front();// 從隊列里取出的時候,要取消標記,我們只保證已經在隊列里的元素不用重復加入isInQueue[cur] = false;que.pop();for (Edge e : grid[cur]) {int from = cur;int to = e.to;int price = e.val;if (minDist[to] > minDist[from] + price) {minDist[to] = minDist[from] + price;// 不再添加to這個下標元素 相當于已經訪問過if (isInQueue[to] == false) {que.push(to);isInQueue[to] = true;}}}}if(minDist[end] == INT_MAX) cout << "unconnected" << endl;else cout << minDist[end] << endl;return 0;
}
95. 城市間貨物運輸 II
涉及到負權回路的情況(存在環路,權值為負,會導致無限循環值越來越小)
非負權回路,松弛 n-1 次以上不會有變換,但是設計負權回路就會越來越小
那么解決本題的核心思路,就是在 kama94.城市間貨物運輸I 的基礎上,再多松弛一次,看minDist數組是否發生變化。 如果再松弛一次結果變換則存在負權回路
// Bellman_ford 算法實現
#include <iostream>
#include <vector>
#include <climits>using namespace std;int main() {int n, m, x, y, val;cin >> n >> m;vector<vector<int>> grid;vector<int> minDist(n+1, INT_MAX);for (int i = 0; i < m; ++i) {cin >> x >> y >> val;grid.push_back({x, y, val});}int start = 1;int end = n;bool isCircle = false;minDist[start] = 0;// 多做一次負權回路for (int i = 1; i <= n; ++i) {for (vector<int>& edge : grid) {int from = edge[0];int to = edge[1];int price = edge[2];if (i < n) {if (minDist[from] != INT_MAX && minDist[to] > minDist[from] + price) {minDist[to] = minDist[from] + price;}}else {if (minDist[from] != INT_MAX && minDist[to] > minDist[from] + price) isCircle = true;}}}if (isCircle) cout << "circle" << endl;else if (minDist[end] == INT_MAX) cout << "unconnected" << endl;else cout << minDist[end] << endl;return 0;
}// Bellman_ford 隊列優化版本 SPFA 實現
#include <iostream>
#include <vector>
#include <queue>
#include <list>
#include <climits>
using namespace std;struct Edge { //鄰接表int to; // 鏈接的節點int val; // 邊的權重Edge(int t, int w): to(t), val(w) {} // 構造函數
};int main() {int n, m, p1, p2, val;cin >> n >> m;vector<list<Edge>> grid(n + 1); // 鄰接表// 將所有邊保存起來for(int i = 0; i < m; i++){cin >> p1 >> p2 >> val;// p1 指向 p2,權值為 valgrid[p1].push_back(Edge(p2, val));}int start = 1; // 起點int end = n; // 終點vector<int> minDist(n + 1 , INT_MAX);minDist[start] = 0;queue<int> que;que.push(start); // 隊列里放入起點 vector<int> count(n+1, 0); // 記錄節點加入隊列幾次count[start]++;vector<bool> isInQueue(n+1, false);isInQueue[start] = true;bool flag = false;while (!que.empty()) {int node = que.front(); que.pop();isInQueue[node] = false;for (Edge edge : grid[node]) {int from = node;int to = edge.to;int value = edge.val;if (minDist[to] > minDist[from] + value) { // 開始松弛minDist[to] = minDist[from] + value;if (!isInQueue[to]) {que.push(to);isInQueue[to] = true;count[to]++; if (count[to] == n) {// 如果加入隊列次數超過 n-1次 就說明該圖與負權回路flag = true;while (!que.empty()) que.pop();break;}}}}}if (flag) cout << "circle" << endl;else if (minDist[end] == INT_MAX) {cout << "unconnected" << endl;} else {cout << minDist[end] << endl;}}
96. 城市間貨物運輸 III
給出盡可能路過最多城市的最短路徑
本題是最多經過 k 個城市, 那么是 k + 1條邊相連的節點。 這里可能有錄友想不懂為什么是k + 1,來看這個圖
節點1 最多已經經過2個節點 到達節點4,那么中間是有多少條邊呢,是 3 條邊對吧。
所以本題就是求:起點最多經過k + 1 條邊到達終點的最短距離。
對所有邊松弛一次,相當于計算起點到達與起點一條邊相連的節點的最短距離,那么對所有邊松弛 k + 1次,就是求起點到達與起點k + 1條邊相連的節點的最短距離。 理解以上內容,其實本題代碼就很容易了,bellman_ford 標準寫法是松弛 n-1 次,本題就松弛 k + 1次就好。
但是松弛 k + 1次代碼有錯,具體分析如下
起點為節點1,起點到起點的距離為0,所以 minDist[1] 初始化為0 ,如圖
其他節點對應的minDist初始化為max,因為我們要求最小距離,那么還沒有計算過的節點默認是一個最大數,這樣才能更新最小距離。當我們開始對所有邊開始第一次松弛:邊:節點1 -> 節點2,權值為-1 ,minDist[2] > minDist[1] + (-1)
,更新 minDist[2] = minDist[1] + (-1) = 0 - 1 = -1
邊:節點2 -> 節點3,權值為1 ,minDist[3] > minDist[2] + 1
,更新 minDist[3] = minDist[2] + 1 = -1 + 1 = 0
,如圖
邊:節點3 -> 節點1,權值為-1 ,minDist[1] > minDist[3] + (-1)
,更新 minDist[1] = 0 + (-1) = -1
,如圖:
邊:節點3 -> 節點4,權值為1 ,minDist[4] > minDist[3] + 1
,更新 minDist[4] = 0 + 1 = 1
,如圖:
理論上來說,對所有邊松弛一次,相當于計算 起點到達 與起點一條邊相連的節點 的最短距離。
而且對所有邊的后面幾次松弛,同樣是更新了所有的節點,說明至多經過k 個節點這個限制根本沒有限制住,每個節點的數值都被更新了。
這是為什么?
在上面畫圖距離中,對所有邊進行第一次松弛,在計算邊(節點2 -> 節點3) 的時候,更新了節點3。
理論上來說節點3 應該在對所有邊第二次松弛的時候才更新。 這因為當時是基于已經計算好的 節點2(minDist[2])
來做計算了。minDist[2]
在計算邊:(節點1 -> 節點2)的時候剛剛被賦值為 -1。
這樣就造成了一個情況,即:計算minDist數組的時候,基于了本次松弛的 minDist數值,而不是上一次松弛時候minDist的數值。所以在每次計算 minDist 時候,要基于 對所有邊上一次松弛的 minDist 數值才行,所以我們要記錄上一次松弛的minDist。
具體代碼添加了 minDist 的 copy
// 樸素 Bellman_ford方法
#include <iostream>
#include <vector>
#include <climits>using namespace std;int main() {int n, m, x, y, val, src, dst, k;cin >> n >> m;vector<vector<int>> grid;vector<int> minDist(n+1, INT_MAX);vector<int> minDistCopy(n+1);for (int i = 0; i < m; ++i) { cin >> x >> y >> val;grid.push_back({x, y, val});}cin >> src >> dst >> k;minDist[src] = 0;// 最多經過k個城市,最多走過k+1條邊for (int i = 1; i <= k + 1; ++i) {// 獲取上一次計算結果minDistCopy = minDist;for (vector<int>& side : grid) {int from = side[0];int to = side[1];int price = side[2];// 使用上一次copy計算if (minDistCopy[from] != INT_MAX && minDist[to] > minDistCopy[from] + price) {minDist[to] = minDistCopy[from] + price;}}}if (minDist[dst] == INT_MAX) cout << "unreachable" << endl;else cout << minDist[dst] << endl;
}// bellman_ford 隊列優化 SPFA
#include <iostream>
#include <vector>
#include <queue>
#include <list>
#include <climits>using namespace std;struct Edge {int to;int val;Edge(int t, int w) : to(t), val(w) {}
};int main() {int n, m, x, y, val, src, dst, k;cin >> n >> m;vector<list<Edge>> grid(n+1);vector<int> minDist(n+1, INT_MAX);vector<int> minDistCopy(n+1);for (int i = 0; i < m; ++i) {cin >> x >> y >> val;grid[x].push_back(Edge(y, val));}cin >> src >> dst >> k;k++; // k+1minDist[src] = 0;queue<int> que;que.push(src);// k控制松弛次數int qSize = 0;while (k-- && !que.empty()) {// 開啟新的隊列vector<bool> isInQueue(n+1, false);minDistCopy = minDist;qSize = que.size();// 后--保證執行完全while (qSize--) {int cur = que.front();que.pop();isInQueue[cur] = false;for (Edge e : grid[cur]) {int from = cur;int to = e.to;int price = e.val;if (minDist[to] > minDistCopy[from] + price) {minDist[to] = minDistCopy[from] + price;if (!isInQueue[to]) {que.push(to);isInQueue[to] = true;}}}}}if (minDist[dst] == INT_MAX) cout << "unreachable" << endl;else cout << minDist[dst] << endl;
}
邊的順序會影響每一次擴展的結果,給出邊的順序為
我上面講解中,給出的示例是這樣的:
4 4
1 2 -1
2 3 1
3 1 -1
3 4 1
1 4 3
我將示例中邊的順序改一下,給成:
4 4
3 1 -1
3 4 1
2 3 1
1 2 -1
1 4 3
相同的圖就會有不同的結果
推理一遍,初始化
邊:節點3 -> 節點1,權值為-1 ,節點3還沒有被計算過,節點1 不更新。
邊:節點3 -> 節點4,權值為1 ,節點3還沒有被計算過,節點4 不更新。
邊:節點2 -> 節點3,權值為 1 ,節點2還沒有被計算過,節點3 不更新。
邊:節點1 -> 節點2,權值為 -1 ,minDist[2] > minDist[1] + (-1)
,更新 minDist[2] = 0 + (-1) = -1
可以發現 同樣的圖,邊的順序不一樣,使用版本一的代碼 每次松弛更新的節點也是不一樣的。
而邊的順序是隨機的,是題目給我們的,所以本題我們才需要 記錄上一次松弛的minDist,來保障 每一次對所有邊松弛的結果。
為什么必須使用 copy?
94.城市間貨物運輸I,是沒有負權回路的,那么多松弛多少次,對結果都沒有影響。求節點1 到 節點n 的最短路徑,松弛n-1 次就夠了,松弛 大于 n-1次,結果也不會變。 那么在對所有邊進行第一次松弛的時候,如果基于本次計算的 minDist 來計算 minDist (相當于多做松弛了),也是對最終結果沒影響。
95.城市間貨物運輸II 是判斷是否有負權回路,一旦有負權回路,對所有邊松弛 n-1 次以后,在做松弛 minDist 數值一定會變,根據這一點來判斷是否有負權回路。所以,95.城市間貨物運輸II 只需要判斷minDist數值變化了就行,而 minDist 的數值對不對,并不是我們關心的。
其關鍵在于本題的兩個因素:
本題可以有負權回路,說明只要多做松弛,結果是會變的。
本題要求最多經過 k 個節點,對松弛次數是有限制的。
可以使用 dijkstra 嗎?不可以因為 dijkstra 貪心策略導致找不到
參考:代碼隨想錄