1493 字
7 分钟

最小生成树学习记录

前置知识和定义#

在开始学习最小生成树之前,先了解一些前置知识。

子图#

对于一张图 G=(V,E)G=(V,E),如果存在一张图 H=(V,E)H=(V',E') 满足 VVV'\subseteq VEEE'\subseteq E,则称 HHGG 的一个子图。记作 HGH\subseteq G

生成子图#

如果存在 HGH \subseteq G 满足 V=VV'=V,则称 HHGG 的一个生成子图

生成子图又称为支撑子图。

生成树#

生成树:一个无向连通图的生成子图,且同时要求是树。也即在图的边集选择了 n1n-1 条边,将图中的所有顶点连接起来。

最小生成树#

我们定义,一个无向连通图的 最小生成树,是指这个图中边权和最小的生成树。

Kruskal 算法#

Kruskal 算法是一种常见简单的用于求最小生成树的算法。此算法的基本思想就是贪心的从小到大加入边。

算法流程#

  1. 对图中的所有边按边权从小到大排序。
  2. 从小到大枚举每一条边 (u,v)(u,v),如果 uuvv 不在同一连通块中,就加入这条边,并将 uuvv 所在的连通块合并。
  3. 重复步骤 2 直到加入了 n1n-1 条边。

显然,我们需要使用并查集来维护每个点所在的连通块。那么,此算法的时间复杂度就为 O(mlogm)O(m\log m),其中 mm 是边数。

证明#

归纳法证明#

我们使用数学归纳法证明 Kruskal 算法的正确性。

基础:算法开始前,显然成立。

归纳:假设算法在加入了 k1k-1 条边后,生成了一个最小生成树 TT。我们需要证明,加入第 kk 条边 (u,v)(u,v) 后,生成的最小生成树 TT' 也成立。

我们可以分两种情况讨论:

  1. 如果 (u,v)(u,v) 不在 TT 中,那么 TT' 就是 TT 加上 (u,v)(u,v) 这条边。显然,TT' 是一个最小生成树。
  2. 如果 (u,v)(u,v)TT 中,那么 TT' 就是 TT 去掉 (u,v)(u,v) 这条边,加上 (u,v)(u,v) 这条边的最小生成树。根据归纳假设,TT 是一个最小生成树,所以 TT' 也是一个最小生成树。

反证法#

我们使用反证法证明 Kruskal 算法的正确性。

假设 Kruskal 算法得到的生成树 TT 不是最小生成树,那么必然存在某个最小生成树 ToptT_{opt},使得 w(Topt)<w(T)w(T_{opt}) < w(T),其中 w()w(\cdot) 表示生成树的边权和。

ek=(u,v)e_k = (u,v) 是 Kruskal 算法选择的第一个不在 ToptT_{opt} 中的边(按边权从小到大排序后的第 kk 条边)。由于 Kruskal 算法会选择这条边,说明在加入 eke_k 之前,uuvv 不在同一个连通块中。

现在考虑 Topt{ek}T_{opt} \cup \{e_k\},这个图必然包含一个环(因为 ToptT_{opt} 是生成树,加入任何边都会形成环)。这个环中至少包含一条边 ee' 满足 w(e)w(ek)w(e') \geq w(e_k)(因为 eke_k 是连接这两个连通块的最小权边)。

我们可以构造一个新的生成树 T=Topt{ek}{e}T' = T_{opt} \cup \{e_k\} \setminus \{e'\}。显然:

  • TT' 仍然是一个生成树(因为去除了环中的一条边)
  • w(T)=w(Topt)+w(ek)w(e)w(Topt)w(T') = w(T_{opt}) + w(e_k) - w(e') \leq w(T_{opt})(因为 w(ek)w(e)w(e_k) \leq w(e')

但是这与 ToptT_{opt} 是最小生成树的假设矛盾,因为我们找到了一个权值不大于 ToptT_{opt} 的生成树 TT',且 TT' 包含了 eke_k

通过重复这个过程,我们可以证明 Kruskal 算法得到的生成树 TT 必须是最小生成树,从而证明了算法的正确性。

代码实现#

#include <bits/stdc++.h>
using namespace std;
// #define MULTI_CASES
#define ll long long
#define int ll
#define endl '\n'
#define vi vector<int>
#define PII pair<int, int>
const int MaxN = 2e5 + 100;
const int INF = 1e9;
const int mod = 1e9 + 7;
int T = 1, N, M;
int a[MaxN];
// vector<PII> G1[MaxN];
struct edge
{
int x, y, z;
} e[MaxN];
bool cmp(edge a, edge b)
{
return a.z < b.z;
}
int fa[MaxN];
struct MST
{
vector<int> fn;
MST(int n)
{
for (int i = 0; i <= n; i++)
{
fn.push_back(i);
}
}
int find(int x)
{
if (fn[x] == x)
{
return x;
}
return fn[x] = find(fn[x]);
}
void join(int x, int y)
{
int fx = find(x), fy = find(y);
if (fx != fy)
{
fn[fx] = fy;
}
}
void kruskal(){
int cnt=0,ans=0;
sort(e+1,e+M+1,cmp);
for(int i=1;i<=M;i++){
int u=find(e[i].x);
int v=find(e[i].y);
if(u==v)continue;
fn[u]=v;
ans+=e[i].z;
cnt++;
if(cnt>N-1)break;
}
if(cnt==N-1){
cout<<ans<<endl;
}
else{
cout<<"orz"<<endl;
}
}
};
inline void Solve()
{
cin >> N >> M;
MST mst(N);
for (int i = 1; i <= M; i++)
{
cin >> e[i].x >> e[i].y >> e[i].z;
}
mst.kruskal();
}
signed main()
{
#ifdef NOI_IO
freopen(".in", "r", stdin);
freopen(".out", "w", stdout);
#endif
ios::sync_with_stdio(0);
cin.tie(nullptr), cout.tie(nullptr);
#ifdef MULTI_CASES
cin >> T;
while (T--)
#endif
Solve();
return 0;
}

Prim 算法#

Prim 算法是另一种常见的求最小生成树的算法,它的基本思想是从一个点开始,不断加点。

算法流程#

  1. 选择图中的任意一个点作为起点,将其加入生成树。
  2. 从生成树中选择一个点 uu,将 uu 到其他所有点的边加入候选边集合。
  3. 从候选边集合中选择一条边 (u,v)(u,v),权值最小,将 vv 加入生成树。
  4. 重复步骤 2 和 3,直到生成树包含所有点。

优化#

如果直接暴力枚举所有边,时间复杂度是 O(n2)O(n^2),但是我们可以使用类似堆优化Dijkstra的方法,将时间复杂度优化到 O(mlogn)O(m\log n)

代码实现#

#include <bits/stdc++.h>
using namespace std;
// #define MULTI_CASES
#define ll long long
#define int ll
#define endl '\n'
#define vi vector<int>
#define PII pair<int, int>
const int MaxN = 2e5 + 100;
const int INF = 0x3f3f3f3f;
const int mod = 1e9 + 7;
int T = 1, N, M;
vector<PII> G1[MaxN];
int dist[MaxN];
bool vis[MaxN];
inline void Solve()
{
cin >> N >> M;
for (int i = 1; i <= M; i++)
{
int u, v, w;
cin >> u >> v >> w;
G1[u].push_back({v, w});
G1[v].push_back({u, w});
}
priority_queue<PII, vector<PII>, greater<PII>> pq;
memset(dist, 0x3f, sizeof(dist));
dist[1] = 0;
pq.push({0, 1});
int ans = 0;
while (!pq.empty())
{
auto [d, u] = pq.top();
pq.pop();
if (vis[u])
continue;
vis[u] = 1;
ans += d;
for (auto [v, w] : G1[u])
{
if (!vis[v] && dist[v] > w)
{
dist[v] = w;
pq.push({w, v});
}
}
}
for (int i = 1; i <= N; ++i)
{
if (!vis[i])
{
cout << "orz" << endl;
return;
}
}
cout << ans << endl;
}
signed main()
{
#ifdef NOI_IO
freopen(".in", "r", stdin);
freopen(".out", "w", stdout);
#endif
ios::sync_with_stdio(0);
cin.tie(nullptr), cout.tie(nullptr);
#ifdef MULTI_CASES
cin >> T;
while (T--)
#endif
Solve();
return 0;
}
最小生成树学习记录
https://blog.introl.top/posts/图论学习记录/最小生成树学习记录/
作者
Introl
发布于
2025-11-12
许可协议
CC BY-NC-SA 4.0