「程式碼隨想錄演算法訓練營」第四十九天 | 圖論 part7

云雀AC了一整天發表於2024-08-30

目錄
  • 最小生成樹的解題
    • prim演算法
      • 舉例說明(來自程式碼隨想錄)
      • 題目:53. 尋寶
    • Kruskal演算法
      • 舉例說明(來自程式碼隨想錄)
      • 題目:53. 尋寶

最小生成樹的解題

最小生成樹型別的題目主要用於解決所有節點的最小連通子圖的問題,即:以最小的成本(邊的權值)將圖中所有節點連結到一起。

最小生成樹可以使用prim演算法,也可以使用kruskal演算法計算出來。

prim演算法

prim演算法的核心有三步:

  1. 第一步,選距離生成樹最近節點。
  2. 第二步,最近節點加入生成樹。
  3. 第三步,更新非生成樹節點到生成樹的距離(即更新minDist陣列)

其中,minDist陣列最為重要,用來記錄每一個節點距離最小生成樹的最近距離

舉例說明(來自程式碼隨想錄)

下面是一個無向有權圖:

image

1. minDist初始化:

minDist陣列裡的數值初始化為最大數,本例中最大值不超過10000,所以初始化最大數為10001就可以。

image

2. 最初隨機選擇一個節點加入生成樹(這裡選擇節點1),並更新minDist陣列:

選擇距離最小生成樹最近的節點,加入到最小生成樹,剛開始還沒有最小生成樹,所以隨便選一個節點加入就好(因為每一個節點一定會在最小生成樹裡,所以隨便選一個就好),那我們選擇節點1 (符合遍歷陣列的習慣,第一個遍歷的也是節點1)

image

3. 在minDist隨機選擇一個最小距離的節點加入生成樹(這裡選擇節點2),並更新minDist陣列:

image

4. 繼續選擇最小距離的節點加入生成樹(這裡選擇節點3),並更新minDist陣列:

image

5. 繼續選擇最小的加入生成樹(這裡選擇節點4),並更新minDist陣列,注意,此時節點5的距離改變了:

image

6. 重複上述,加入節點5,繼續:

image

7. 繼續,加入節點6:

image

8. 繼續,加入節點7:

image

9. 最後把minDist陣列的值累加,得到總距離,當然,也可以輸出路徑。

題目:53. 尋寶

題目連結:https://kamacoder.com/problempage.php?pid=1053
文章講解:https://www.programmercarl.com/kamacoder/0053.尋寶-prim.html
題目狀態:看題解

思路:

prim演算法,看上面。

程式碼:

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

using namespace std;

int main()
{
    int v, e;
    int x, y, k;
    cin >> v >> e;
    // 預設最大值
    vector<vector<int>> grid(v + 1, vector<int>(v + 1, 10001));
    while(e--)
    {
        cin >> x >> y >> k;
        // 因為是雙向圖,兩個方向都要填上
        grid[x][y] = k;
        grid[y][x] = k;
    }
    // 所有節點到最小生成樹的最小距離
    vector<int> minDist(v + 1, 10001);
    // 這個節點是否在樹裡
    vector<bool> isInTree(v + 1, false);

    // 初始化,用來記錄邊的連線
    vector<int> parent(v + 1, -1);

    // 迴圈n-1次,建立n-1條邊,將n個節點連在一起
    for(int i = 1; i < v; ++i)
    {
        // 第一步:選距離生成樹最近的節點
        int cur = -1; // 選擇節點加入生成樹
        int minVal = INT_MAX;
        for(int j = 1; j <= v; ++j)
        {
            // 選取條件
            // 1. 不在最小生成樹裡
            // 2. 距離最小生成樹最近的節點
            if(!isInTree[j] && minDist[j] < minVal)
            {
                minVal = minDist[j];
                cur = j;
            }
        }
        // 第二步:最近節點加入生成樹
        isInTree[cur] = true;
        // 第三步:更新非生成樹節點到生成樹的距離
        for(int j = 1; j <= v; ++j)
        {
            if(!isInTree[j] && grid[cur][j] < minDist[j])
            {
                minDist[j] = grid[cur][j];

                // 記錄邊
                parent[j] = cur;
            }
        }
    }

    // 統計結果
    int result = 0;
    for(int i = 2; i <= v; ++i) result += minDist[i];
    cout << result << endl;

    // 輸出最小生成樹邊的連線情況
    for(int i = 1; i <= v; ++i) cout << i << "->" << parent[i] << endl;
}

Kruskal演算法

Kruskal演算法是維護邊的集合。思路如下:

  • 邊的權重排序,因為要有限選最小的邊加入到生成樹裡。
  • 遍歷排序後的邊。
    • 如果邊首尾的兩個節點在同一個集合,說明如果連上這條邊圖中會出現環。
    • 如果邊首尾的兩個節點不在同一個結合,加入到最小生成樹,並把兩個節點加入同一個集合。

舉例說明(來自程式碼隨想錄)

image

排序後的邊順序為[(1,2) (4,5) (1,3) (2,6) (3,4) (6,7) (5,7) (1,5) (3,2) (2,4) (5,6)]

使用並查集來將兩個節點加入同一個集合,並且判斷兩個節點是否在同一個集合中。

流程如下:

image

image

image

image

image

image

題目:53. 尋寶

題目連結:https://kamacoder.com/problempage.php?pid=1053
文章講解:https://www.programmercarl.com/kamacoder/0053.尋寶-prim.html
題目狀態:看題解

思路:

Kruskal演算法,看上面。

程式碼:

#include <iostream>
#include <vector>
#include <algorithm>

using namespace std;

// l、r為兩邊的節點,val為邊的權值
struct Edge
{
    int l, r, val;
};

// 節點數量
int n = 10001;
// 並查集標記節點關係的陣列
vector<int> father(n, -1); // 節點編號是從1開始的,n要大一些

// 並查集初始化
void init()
{
    for(int i = 0; i < n; ++i)
    {
        father[i] = i;
    }
}

// 並查集的查詢操作
int find(int u)
{
    return u == father[u] ? u : father[u] = find(father[u]);
    // 路徑壓縮
}

// 並查集的加入集合
void join(int u, int v)
{
    u = find(u); // 尋找u的根
    v = find(v); // 尋找v的根
    if(u == v) return; // 如果發現根相同,則說明在一個集合,不用兩個節點相連直接返回
    father[v] = u;
}

int main()
{
    int v, e;
    int v1, v2, val;
    vector<Edge> edges;
    int result_val = 0;
    cin >> v >> e;
    while(e--)
    {
        cin >> v1 >> v2 >> val;
        edges.push_back({v1, v2, val});
    }

    // 執行kruskal演算法
    // 按邊的許可權對邊進行從小到大排序
    sort(edges.begin(), edges.end(), [](const Edge &a, const Edge &b) {
        return a.val < b.val;
    });

    vector<Edge> result; // 儲存最小生成樹的邊

    // 並查集初始化
    init();

    // 從頭開始遍歷邊
    for(Edge &edge : edges)
    {
        // 並查集,搜出兩個節點的祖先
        int x = find(edge.l);
        int y = find(edge.r);

        // 如果祖先不同,則不在同一個集合
        if(x != y)
        {
            result.push_back(edge); // 儲存最小生成樹的邊
            result_val += edge.val; // 這條邊可以作為生成樹的邊
            join(x, y); // 兩個節點加入到同一個集合
        }
    }
    cout << result_val << endl;
    
    // 列印最小生成樹的邊
    for(auto &edge : result)
    {
        cout << edge.l << "-" << edge.r << " : " << edge.val << endl;
    }
    return 0;
}

Kruskal與prim的關鍵區別在於,prim維護的是節點的集合,而Kruskal維護的是邊的集合。 如果一個圖中,節點多,但邊相對較少,那麼使用Kruskal更優。

相關文章