A星、Floyod、Bellman-Ford

n1ce2cv發表於2024-09-20

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;
    }
}

相關文章