最小生成樹詳解
前言
最近剛學了最小生成樹,於是想趁熱打火,先來總結一下~
前置芝士
- 圖、樹的概念、遍歷與儲存
- 並查集
本文章所有程式碼均以C++
編寫。
最小生成樹的概念
最小生成樹(Minimum Spanning Tree, MST)是一種特殊的圖。它具備樸素樹的所有性質,但也是一張圖中邊權最小但經過每個節點的子樹。
定義
一個有 \(n\) 個結點的連通圖的生成樹是原圖的極小連通子圖,且包含原圖中的所有 \(n\) 個結點,並且有保持圖連通的最少的邊。最小生成樹可以用
Kruskal
(克魯斯卡爾)演算法或Prim
(普里姆)演算法求出。[1]
大意
在一張連通圖 \(G\) 裡,有 \(n\) 個節點和 \(m\)
那麼,圖 \(G\) 的最小生成樹 \(T\) 必須具備以下條件:
- \(T\) 必須包括 \(G\) 的所有節點;
- \(T\) 的邊數必須等於 \(n - 1\);
- \(T\) 的邊權和必須在所有生成樹中最小;
- \(T\) 必須為 \(G\) 的子圖。
假設我們有如下的連通圖 \(G\)(左),那麼它的最小生成樹 \(T\) 如圖所示(右)。
顯然對於圖 \(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演算法也可以使用堆進行優化,以提高效率。
參考程式碼
待補充。