1. 程式人生 > 其它 >最小生成樹詳解

最小生成樹詳解

最小生成樹(MST)是圖的一個子集。本文將介紹兩種常用的演算法:Kruskal和Prim來尋找最小生成樹。

前言

最近剛學了最小生成樹,於是想趁熱打火,先來總結一下~

前置芝士

  • 圖、樹的概念、遍歷與儲存
  • 並查集

本文章所有程式碼均以C++編寫。

最小生成樹的概念

最小生成樹(Minimum Spanning Tree, MST)是一種特殊的圖。它具備樸素樹的所有性質,但也是一張圖中邊權最小但經過每個節點的子樹。

定義

一個有 \(n\) 個結點的連通圖的生成樹是原圖的極小連通子圖,且包含原圖中的所有 \(n\) 個結點,並且有保持圖連通的最少的邊。最小生成樹可以用Kruskal(克魯斯卡爾)演算法或Prim(普里姆)演算法求出。[1]

大意

在一張連通圖 \(G\) 裡,有 \(n\) 個節點和 \(m\)

條邊,第 \(i\) 條邊的權值為 \(w_i\) 。我們設圖 \(G\) 的最小生成樹為 \(T\)

那麼,圖 \(G\) 的最小生成樹 \(T\) 必須具備以下條件:

  • \(T\) 必須包括 \(G\) 的所有節點;
  • \(T\) 的邊數必須等於 \(n - 1\)
  • \(T\) 的邊權和必須在所有生成樹中最小;
  • \(T\) 必須為 \(G\) 的子圖。

假設我們有如下的連通圖 \(G\)(左),那麼它的最小生成樹 \(T\) 如圖所示(右)。

[2]

顯然對於圖 \(G\)\(T\) 的邊權和在所有生成樹中最小。我們稱這樣的樹為圖 \(G\)最小生成樹(簡稱MST)。

演算法

我們不妨來介紹兩種較為常見的求最小生成樹的演算法。

在介紹演算法之前,我們先來看這樣一道題目:

題目連結:洛谷P3366

題目很容易理解,即求出給定圖的最小生成樹邊權和。那麼,我們來看看這些演算法吧!

Kruskal

基本思路

Kruskal(克魯斯卡爾) 是一種貪心策略,類似圖論中的Bellman-Ford演算法。

簡單來說,如果我們挑選了 \(n-1\) 條較小邊,那麼顯而易見,這 \(n-1\) 條邊的權值相加也會是一個較小值。按照這種思路,我們可以挑選 \(n-1\)\(G\) 裡面最小的邊並將它們相連。

但是,你以為這就完了?怎麼可能。

顯然我們上面的做法有一個缺陷:它雖然保證了邊權和最小,但是得出的卻並不一定是一棵樹。相反,它反而有可能得出來一個圖(或森林)。我們需要解決這個問題。

顯然,對於一條 \(u \leftrightarrow v\) 的無向邊,若點 \(u\) 和點 \(v\) 已經連通(直接或間接),那麼我們就不再需要加入當前邊了。

對於每次遍歷,我們都會對當前邊的所到達的節點進行一個排查:如果節點已經連通,則無需加邊;否則連線兩點。

這樣一來,我們就剩下一個最重要的問題沒解決了:如何判斷兩個點有沒有連通?

我們可以使用並查集這個資料結構進行儲存和判重。每次判斷兩點的連通性的時候,我們只需要查詢他們的祖先是否相同即可。同理,對於每次連線操作,我們只需要進行並查集的合併操作來合併 \(u,v\) 兩點即可。

參考程式碼

#include <iostream>
#include <cstdio>
#include <algorithm>
using namespace std;
const int N = 2 * 1e5 + 10;

struct edge {  // 存邊
    int u, v, w;
} e[N];
edge mst[5010];  // 最小生成樹
int vtx[5010], k, ans, n, m;  // vtx並查集陣列,k當前最小生成樹節點數,ans邊權和

bool cmp(edge a, edge b) {
    return a.w < b.w;  // 按照邊權排序
}

int Find(int x) {  // 並查集查詢操作
    if (vtx[x] == x) return x;
    return vtx[x] = Find(vtx[x]);
}

void Union(int u, int v) {  // 並查集合並操作
    int fu = Find(u), fv = Find(v);
    if (fu != fv) vtx[fv] = fu;
}

void kruskal() {  // Kruskal最小生成樹
    for (int i = 0; i < m; i++) {  // 遍歷所有邊
        if (Find(e[i].u) != Find(e[i].v)) {  // 如果兩點沒有連線
            k++;  // MST邊數++
            mst[k].u = e[i].u, mst[k].v = e[i].v, mst[k].w = e[i].w;  // 記錄當前邊
            ans += e[i].w;  // 總權重增加
            Union(e[i].u, e[i].v);  // 連線兩點
        } 
    }
}

int main() {
    scanf("%d%d", &n, &m);
    for (int i = 1; i <= n; i++) vtx[i] = i;  // 並查集初始化,祖先都是自己,即每個點都未連線
    for (int i = 0; i < m; i++) scanf("%d%d%d", &e[i].u, &e[i].v, &e[i].w);
    sort(e, e + m, cmp);  // 對邊權進行排序
    kruskal();  // 獲取MST
    if (k == n - 1) printf("%d\n", ans);  // 如果邊數滿足條件,輸出總權值
    else printf("orz");  // 否則輸出orz
    return 0;
}

Prim

基本思路

Prim(普利姆) 是Dijkstra的一個擴充套件。

Prim演算法與Dijkstra演算法唯一的區別在於:Prim演算法所記錄的距離並非從某個起點到終點的距離,而是當前的生成樹到某個點的最短距離。

其餘部分與Dijkstra演算法一致。同樣,Prim演算法也可以使用堆進行優化,以提高效率。

參考程式碼

待補充。

參考資料