A 星演算法
A 星和 Dijkstra 演算法唯一區別在於堆中排序的依據。distance 陣列仍然儲存實際代價,預估代價隻影響堆的彈出順序。
-
Dijkstra 根據
源點到當前點的實際代價
進行排序。 -
A 星根據
源點到當前點的實際代價 + 當前點到終點的預估代價
進行排序
預估函式要求:當前點到終點的預估代價 <= 當前點到終點的實際代價
,越接近越快
常用選擇:曼哈頓距離、歐氏距離、對角線距離(行差值列差值絕對值的最大值)
Floyd 演算法
Floyd 演算法是一種用於解決所有節點對之間最短路徑問題的演算法。它透過動態規劃的思想,逐步計算出所有節點對之間的最短路徑。
Floyd 演算法使用一個二維陣列 distance 來記錄節點對之間的最短距離。初始時,distance[i][j] 表示節點 i 到節點 j 的直接距離(如果存在邊),否則為無窮大。演算法透過三重迴圈不斷更新 distance 陣列,最終得到所有節點對之間的最短路徑。
Floyd 演算法的核心思想是動態規劃。外層迴圈控制中間節點 k,內層兩個迴圈分別遍歷起點 i 和終點 j。如果透過節點 k 可以使 i 到 j 的距離更短,則更新 distance[i][j]。重複此過程,直到所有節點都被遍歷過。
diatance[i][j] 表示 i 和 j 的最短距離,更新:distance[i][j] = min(distance[i][j], distance[i][k] + distance[k][j])
- 時間複雜度:
O(n^3)
,空間複雜度:O(n^2)
,常數時間小,容易實現 - 不適用於存在負環的圖
int main() {
// n * n 的矩陣
int n = 10;
// 其實就是帶權圖的鄰接矩陣
vector<vector<int>> distance(n, vector<int>(n, INT_MAX));
// 省略 distance 根據給出的邊進行初始化
// i 經過 k 到達 j
for (int k = 0; k < n; ++k)
for (int i = 0; i < n; ++i)
for (int j = 0; j < n; ++j)
if (distance[i][k] != INT_MAX
&& distance[k][j] != INT_MAX
&& distance[i][j] > distance[i][k] + distance[k][j])
distance[i][j] = distance[i][k] + distance[k][j];
}
P2910 [USACO08OPEN] Clear And Present Danger S
#include <iostream>
#include <vector>
#include <algorithm>
#include <queue>
using namespace std;
int main() {
int n, m;
cin >> n >> m;
// 找寶藏的路徑
vector<int> path(m);
for (int i = 0; i < m; ++i) {
// 序號從 1 開始
cin >> path[i];
// 序號從 0 開始
path[i]--;
}
vector<vector<int>> distance(n, vector<int>(n, 0x7fffffff));
for (int i = 0; i < n; ++i)
for (int j = 0; j < n; ++j)
cin >> distance[i][j];
for (int k = 0; k < n; ++k)
for (int i = 0; i < n; ++i)
for (int j = 0; j < n; ++j)
if (distance[i][k] != 0x7fffffff
&& distance[k][j] != 0x7fffffff
&& distance[i][j] > distance[i][k] + distance[k][j])
distance[i][j] = distance[i][k] + distance[k][j];
int res = 0;
for (int i = 0; i + 1 < m; ++i)
res += distance[path[i]][path[i + 1]];
cout << res;
}
Bellman-Ford 演算法
解決可以有負邊但不能有負環的圖,求單源最短路徑的演算法。
Bellman-Ford 過程:每一輪考察每條邊,每條邊都嘗試進行鬆弛操作,那麼若干點的 distance 會變小。當某一輪發現不再有鬆弛操作出現時,演算法停止。
Bellman-Ford 演算法時間複雜度:假設點的數量為 N,邊的數量為 M,每一輪時間複雜度 O(M)。最短路存在的情況下,因為 1 次鬆弛操作會使 1 個點的最短路的邊數 +1。而從源點出發到任何點的最短路最多走過全部的 n 個點,所以鬆弛的輪數必然 <= n - 1。所以Bellman-Ford演算法時間複雜度 O(M*N)
重要推廣:判斷從某個點出發能不能到達負環。上面已經說了,如果從A出發存在最短路(沒有負環),那麼鬆弛的輪數必然 <= n - 1。而如果從A點出發到達一個負環,那麼鬆弛操作顯然會無休止地進行下去。所以,如果發現從A點出發,在第n輪時鬆弛操作依然存在,說明從A點出發能夠到達一個負環。可以透過設定一個虛擬源點(與原來所有的點都有連線),判斷圖是否有負環。
ChatGPT 給出的介紹
Bellman-Ford 演算法的關鍵思想是透過多次鬆弛操作(relaxation)逐步逼近每個頂點的最短路徑,最終在迭代結束時,能夠正確確定從源點到所有點的最短路徑。要理解為什麼每一輪鬆弛能確定一些點的最短距離,可以從以下幾個方面解釋:
1. 鬆弛操作的定義
鬆弛操作的過程是透過檢查從某一頂點到另一頂點之間的邊,看看是否可以透過這個頂點找到一條更短的路徑。如果可以,那麼就更新目標頂點的最短距離。
對於每條邊 (u, v) ,如果:dist[v] > dist[u] + weight(u, v)
那麼就更新:dist[v] = dist[u] + weight(u, v)
2. 多輪鬆弛的作用
假設圖中有 V 個頂點,最壞情況下,從源點到一個頂點的最短路徑可能包含最多 V - 1 條邊。因此,Bellman-Ford 演算法執行 V - 1 輪鬆弛操作,確保可以找到源點到任意頂點的最短路徑。
每一輪鬆弛都會檢查所有邊,並嘗試透過更短的路徑更新節點的最短距離。對於一條最短路徑上恰好有 k 條邊的節點,最多需要 k 輪鬆弛來確定該節點的最短距離。
3. 每輪鬆弛確定某些點的最短距離
在第 i 輪鬆弛中,Bellman-Ford 演算法會確保從源點到任何節點的最短路徑最多經過 i 條邊。因此,經過 i 輪鬆弛後,所有最短路徑包含 i 條或更少邊的節點的最短距離都會被確定。隨著輪數的增加,更多節點的最短路徑距離會被確定。
特別是,當執行到第 V - 1 輪時,所有節點的最短距離都會被確定,因為最長的簡單路徑(不包含環)最多包含 V - 1 條邊。
4. 確定最短路徑
透過多次鬆弛操作,Bellman-Ford 演算法最終能保證:
- 如果某個點的最短路徑被更新為一個值,那麼這個值是正確的,因為此時它已經不能透過任何進一步的鬆弛獲得更短的路徑。
- 所以,每一輪鬆弛後,至少有一些點的最短距離是最終值,不會再發生變化。
5. 總結
每一輪鬆弛都能確定一部分節點的最短路徑,因為它逐漸考慮到從源點到每個節點經過不同數量邊的路徑。經過最多 V - 1 輪後,所有點的最短路徑都會被確定。因此,每一輪鬆弛後,某些點的最短路徑是最終的且不會再改變。
787. K 站中轉內最便宜的航班
#include <iostream>
#include <vector>
#include <algorithm>
#include <queue>
using namespace std;
class Solution {
public:
// 閹割版 Bellman-Ford
int findCheapestPrice(int n, vector<vector<int>> &flights, int src, int dst, int k) {
vector<int> cur(n, INT_MAX);
cur[src] = 0;
for (int i = 0; i <= k; ++i) {
vector<int> nxt(cur);
for (const auto &edge: flights) {
if (cur[edge[0]] == INT_MAX) continue;
// 從舊錶中取資料更新,標準的 Bellman—Ford 是從新表中取
nxt[edge[1]] = min(nxt[edge[1]], cur[edge[0]] + edge[2]);
}
cur = nxt;
}
return cur[dst] == INT_MAX ? -1 : cur[dst];
}
};
P3385 【模板】負環
#include <iostream>
#include <vector>
#include <algorithm>
#include <queue>
using namespace std;
int n, m, T;
const int MAX_VAL = 0x7fffffff;
// 鏈式前向星
vector<int> head;
vector<int> nxt;
vector<int> to;
vector<int> weight;
int cnt;
void initGraph() {
// 點的下標從 1 開始
head.resize(n + 1, 0);
nxt.resize((m << 1) + 1);
to.resize((m << 1) + 1);
weight.resize((m << 1) + 1);
fill(begin(head), end(head), 0);
cnt = 1;
}
void addEdge(int u, int v, int w) {
nxt[cnt] = head[u];
to[cnt] = v;
weight[cnt] = w;
head[u] = cnt;
cnt++;
}
// Bellman-Ford
// 從 1 到各個點的最短距離
vector<int> distances;
// 存放上一輪鬆弛中有變動的點
queue<int> q;
// 是否在佇列中
vector<bool> enter;
// 記錄點的鬆弛次數
vector<int> updateCnt;
void initBellmanFord() {
distances.resize(n + 1, MAX_VAL);
enter.resize(n + 1, false);
updateCnt.resize(n + 1, 0);
fill(begin(distances), end(distances), MAX_VAL);
fill(begin(enter), end(enter), false);
fill(begin(updateCnt), end(updateCnt), 0);
}
void clearQueue() {
queue<int> empty;
swap(q, empty);
}
// 從頂點 1 出發是否能到達負環
bool hasNegativeCircle() {
distances[1] = 0;
updateCnt[1]++;
q.emplace(1);
enter[1] = true;
while (!q.empty()) {
int u = q.front();
q.pop();
enter[u] = false;
for (int ei = head[u]; ei > 0; ei = nxt[ei]) {
int v = to[ei];
int w = weight[ei];
// 沒法鬆弛就跳過
if (distances[v] <= distances[u] + w) continue;
distances[v] = distances[u] + w;
// 在佇列就跳過
if (enter[v]) continue;
// 到 v 點的路徑被鬆弛了一次
updateCnt[v]++;
if (updateCnt[v] >= n) return true;
q.emplace(v);
enter[v] = true;
}
}
return false;
}
int main() {
cin >> T;
// 每組測試用例
for (int i = 0; i < T; ++i) {
cin >> n >> m;
// 初始化
initGraph();
initBellmanFord();
clearQueue();
// 建圖
for (int j = 0, u, v, w; j < m; ++j) {
cin >> u >> v >> w;
if (w >= 0) {
addEdge(u, v, w);
addEdge(v, u, w);
} else {
addEdge(u, v, w);
}
}
cout << (hasNegativeCircle() ? "YES" : "NO") << endl;
}
}