「程式碼隨想錄演算法訓練營」第五十一天 | 圖論 part9

云雀AC了一整天發表於2024-09-01

目錄
  • Bellman_ford演算法
    • 模擬過程
    • 題目:94. 城市間貨物運輸I
  • Bellman_ford佇列最佳化演算法(又名SPFA)
    • 模擬過程
    • 題目:94. 城市間貨物運輸I
  • Bellman_ford演算法之判斷負權迴路
    • 題目:95. 城市間貨物運輸II
  • Bellman_ford演算法之單源有限最短路
    • 題目:96. 城市間貨物運輸III

Bellman_ford演算法

Bellman_ford演算法解決的問題和dijkstra樸素版要解決的問題類似,唯一不同的是dijkstra解決的問題中邊的權值不能為負數,而Bellman_ford演算法要解決的問題是帶負權值的單源最短路問題。

Bellman_ford演算法的核心思想是對所有邊進行鬆弛n-1次操作(n為節點數量),從而求得目標最短路

鬆弛的概念就是噹噹前節點的minDist值大於從其他節點過來的權值,則更新當前節點的minDist值,如下面的程式碼:

if(minDist[B] > minDist[A] + value) minDist[B] = minDist[A] + value;

其中value表示從A節點到B節點中的權值,上面程式碼的意思就是A到B的路徑所需要的權值要小於B當前的權值,因此要更新B的權值。

模擬過程

注意:這個模擬過程僅僅是將具有改變的過程展現出來,而有些節點在當前鬆弛階段並沒有改變其minDist值,比如節點5到節點6……,這裡並沒有將這些過程展現出來。

  1. 加入節點1,更新minDist陣列。

image

  1. 進行一次鬆弛計算:

節點1到節點2:minDist[2] > minDist[1] + 1,因此更新minDist[2]。

image

節點2到節點5:minDist[5] > minDIst[2] + 2,因此更新minDist[5]。

image

節點2到節點4:minDist[4] > minDist[2] + (-3),因此更新minDist[4]。

image

節點4到節點6:minDist[6] > minDist[4] + 4,因此更新minDist[6]。

image

節點1到節點3:minDist[3] > minDist[1] + 5,因此更新minDist[3]。

image

  1. 上面只是一次鬆弛的結果,相當於計算起點到達與起點一條邊相連的節點的最短距離。而從起點到終點最多有n-1條邊,因此需要進行n-1次鬆弛。

題目:94. 城市間貨物運輸I

題目連結:https://kamacoder.com/problempage.php?pid=1152
文章講解:https://www.programmercarl.com/kamacoder/0094.城市間貨物運輸I.html
題目狀態:看題解

思路:

使用Bellman_ford演算法。

程式碼:

#include <iostream>
#include <vector>
#include <list>
#include <climits>

using namespace std;

int main()
{
    int n, m, p1, p2, val;
    cin >> n >> m;

    vector<vector<int>> grid;

    // 將所有邊儲存起來
    for(int i = 0; i < m; ++i)
    {
        cin >> p1 >> p2 >> val;
        grid.push_back({p1, p2, val});
    }

    int start = 1; // 起點
    int end = n; // 終點

    vector<int> minDist(n + 1, INT_MAX);
    minDist[start] = 0;
    
    // 對所有邊鬆弛n-1次
    for(int i = 1; i < n; ++i)
    {
        // 每一次鬆弛,都是對所有邊進行鬆弛
        for(vector<int> &side : grid)
        {
            int from = side[0]; // 邊的出發點
            int to = side[1]; // 邊的到達點
            int price = side[2]; // 邊的權值
            // 鬆弛操作
            // minDist[from] != INT_MAX 防止從未計算過的節點出發
            if(minDist[from] != INT_MAX && minDist[to] > minDist[from] + price)
            {
                minDist[to] = minDist[from] + price;
            }
        }
        cout << "對所有邊鬆弛" << i << "次" << endl;
        for(int k = 1; k <= n; ++k)
        {
            cout << minDist[k] << " ";
        }
        cout << endl;
    }
    if(minDist[end] == INT_MAX) cout << "unconnected" << endl;
    // 不能到達終點
    else cout << minDist[end] << endl; // 到達終點最短路徑
}

Bellman_ford佇列最佳化演算法(又名SPFA)

在上一節的Bellman_ford演算法中的鬆弛階段是對每一個節點都進行鬆弛,而有些節點在上一個節點沒有被鬆弛的情況下進行鬆弛是沒必要的。

比如當前節點5的minDist[5]為INT_MAX,而節點5到節點6需要判斷minDist[6]是否大於minDist[5] + value,顯然是不必要的。

而佇列最佳化版本則只需要對上一次鬆弛的時候更新過的節點作為出發節點所連線的邊進行鬆弛就夠了

模擬過程

  1. 將節點1壓入佇列,並更新minDist陣列。

image

  1. 從佇列中取出節點1,以節點1為起點,壓入與其有邊的節點2和節點3,並更新minDist陣列。

image

  1. 從佇列中取出節點2,以節點2為起點,壓入與其有邊的節點4和節點5,並更新minDist陣列。

image

  1. 從佇列中取出節點3,以節點3為起點,壓入與其有邊的節點(該例中沒有),並更新minDist陣列。

image

  1. 從佇列中取出節點4,以節點4為起點,壓入與其有邊的節點6,並更新minDist陣列。

image

  1. 從佇列中取出節點5,以節點5為起點,壓入與其有邊的節點6和節點3(因為節點6還在佇列中,因此不用重複壓入),並更新minDist陣列。

image

  1. 分別從佇列中取出節點6和節點3,因為其都沒有下一個節點,因此沒有壓入操作,而需要更新minDist陣列。

image

完成。

題目:94. 城市間貨物運輸I

題目連結:https://kamacoder.com/problempage.php?pid=1152
文章講解:https://www.programmercarl.com/kamacoder/0094.城市間貨物運輸I.html
題目狀態:看題解

思路:

Bellman_ford演算法佇列最佳化版本

程式碼:

#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);

    // 加入最佳化,已經在佇列裡的元素不用重複新增
    vector<bool> isInQueue(n + 1);

    // 將所有邊儲存起來
    for(int i = 0; i < m; ++i)
    {
        cin >> p1 >> p2 >> val;
        grid[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);

    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] == false)
                {
                    // 已經在佇列裡的元素不用重複新增
                    que.push(to);
                    isInQueue[to] = true;
                }
            }
        }
    }

    // 不能到達終點
    if(minDist[end] == INT_MAX) cout << "unconnected" << endl;
    // 到達終點最短路徑
    else cout << minDist[end] << endl;
}

Bellman_ford演算法之判斷負權迴路

上面兩節都是沒有迴路的情況,而在本節的示例中出現了負權迴路的情況,也就是說一直在這個負權迴路中轉到話,其minDist會一直變小,因此需要判斷是否存在負權迴路。

前兩節都是進行n-1次鬆弛,因為n-1次和n次是相同的,但在本節中是不一樣的,因為有負權迴路的存在,n次和n-1次的結果是不同的,因此可以根據這個條件來判斷是否存在負權迴路。

題目:95. 城市間貨物運輸II

題目連結:https://kamacoder.com/problempage.php?pid=1153
文章講解:https://www.programmercarl.com/kamacoder/0095.城市間貨物運輸II.html
題目狀態:看題解

思路一:

在Bellman_ford演算法基礎上增加負權迴路判斷。

程式碼一:

#include <iostream>
#include <vector>
#include <list>
#include <climits>

using namespace std;

int main()
{
    int n, m, p1, p2, val;
    cin >> n >> m;

    vector<vector<int>> grid;

    for(int i = 0; i < m; ++i)
    {
        cin >> p1 >> p2 >> val;
        grid.push_back({p1, p2, val});
    }
    int start = 1; // 起點
    int end = n; // 終點

    vector<int> minDist(n + 1, INT_MAX);
    minDist[start] = 0;
    bool flag = false;
    // 鬆弛n次,最後一次判斷負權迴路
    for(int i = 1; i <= n; ++i)
    {
        for(vector<int> &side : grid)
        {
            int from = side[0];
            int to = side[1];
            int price = side[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) flag = true;
            }
        }
    }

    if(flag) cout << "circle" << endl;
    else if(minDist[end] == INT_MAX) cout << "unconnected" << endl;
    else cout << minDist[end] << endl;

    return 0;
}

思路二:

在Bellman_ford的佇列最佳化版本中加入負權迴路判斷。

程式碼二:

#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;
        grid[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]++;

    bool flag = false;
    while(!que.empty())
    {
        int node = que.front(); que.pop();

        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;
                que.push(to);
                count[to]++;
                // 如果加入佇列次數超過n-1次就說明該圖有負權迴路
                if(count[to] == n)
                {
                    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;

    return 0;
}

Bellman_ford演算法之單源有限最短路

這節是在第一節的基礎上加的內容,主要是限制兩個節點中間經過的節點數。

比如,題目要求最多隻能經過k個節點,那麼也就意味著起始位置到終止位置中間最多隻能有k+1條邊。

我們只需要限制Bellman_ford演算法的鬆弛次數即可,即從第一節中的n-1變為k+1。但是要注意在每次計算minDist陣列的時候要基於所有邊上一次鬆弛的minDist數值進行計算

題目:96. 城市間貨物運輸III

題目連結:https://kamacoder.com/problempage.php?pid=1154
文章講解:https://www.programmercarl.com/kamacoder/0096.城市間貨物運輸III.html
題目狀態:看題解

思路一:

在Bellman_ford演算法的基礎上修改。

程式碼一:

#include <iostream>
#include <vector>
#include <list>
#include <climits>

using namespace std;

int main()
{
    int src, dst, k, p1, p2, val, m, n;
    cin >> n >> m;
    vector<vector<int>> grid;
    for(int i = 0; i < m; ++i)
    {
        cin >> p1 >> p2 >> val;
        grid.push_back({p1, p2, val});
    }

    cin >> src >> dst >> k;

    vector<int> minDist(n + 1, INT_MAX);
    minDist[src] = 0;
    vector<int> minDist_copy(n + 1); // 用來記錄上一次遍歷的結果
    for(int i = 1; i <= k + 1; ++i)
    {
        minDist_copy = minDist; // 獲取上一次計算的結果
        for(vector<int> &side : grid)
        {
            int from = side[0];
            int to = side[1];
            int price = side[2];
            // 注意使用minDist_copy來計算minDist
            if(minDist_copy[from] != INT_MAX && minDist[to] > minDist_copy[from] + price)
                minDist[to] = minDist_copy[from] + price;
        }
    }
    // 不能到達終點
    if(minDist[dst] == INT_MAX) cout << "unreachable" << endl;
    // 到達終點最短路徑
    else cout << minDist[dst] << endl;
}

思路二:

在Bellman_ford的佇列最佳化版本的基礎上修改。

程式碼二:

#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,權值為 val
        grid[p1].push_back(Edge(p2, val));
    }
    int start, end, k;
    cin >> start >> end >> k;

    k++;

    vector<int> minDist(n + 1 , INT_MAX);
    vector<int> minDist_copy(n + 1); // 用來記錄每一次遍歷的結果

    minDist[start] = 0;

    queue<int> que;
    que.push(start); // 佇列裡放入起點

    int que_size;
    while (k-- && !que.empty()) {

        minDist_copy = minDist; // 獲取上一次計算的結果
        que_size = que.size(); // 記錄上次入佇列的節點個數
        while (que_size--) { // 上一輪鬆弛入佇列的節點,這次對應的邊都要做鬆弛
            int node = que.front(); que.pop();
            for (Edge edge : grid[node]) {
                int from = node;
                int to = edge.to;
                int price = edge.val;
                if (minDist[to] > minDist_copy[from] + price) {
                    minDist[to] = minDist_copy[from] + price;
                    que.push(to);
                }
            }

        }
    }
    if (minDist[end] == INT_MAX) cout << "unreachable" << endl;
    else cout << minDist[end] << endl;

}

相關文章