来源:算法讲解061【必备】最小生成树 · 2026-10-26
依据:视频字幕 2000 字
最小生成树(Minimum Spanning Tree, MST)是在连通无向带权图中选出 N−1 条边使所有点连通且总权值最小的树;Kruskal 算法按边权从小到大加边、用并查集判环,Prim 算法从一点出发用小根堆不断扩展。
给一张连通的无向带权图(N 个点、M 条边),要求选出一个边集:
输出通常只要这个最小总权值;有时也要求输出选了哪些边。典型场景:修路/铺网线/建电网,要求花最少钱把所有城市连起来。
find(x) 查 x 所在集合的代表元;union(a,b) 合并两个集合。加边前查两端是否同集合,同集合就说明加这条边会成环。Kruskal 的直觉(贪心 + 割性质):把边按权值从小到大看。当前这条边 (u,v,w),如果 u、v 已经连通,加它必然成环,扔掉;如果不连通,就加它。
为什么加它不会亏?设当前 u 在一个连通块 A 里、v 在另一个连通块 B 里。任何一棵生成树都必须至少有一条边跨过 A 和 B 之间(否则 A、B 不连通)。而 w 是所有还没考虑的边里最小的,也就是所有跨 A、B 的边里最小的(更小的边要么已经加过、要么会成环被扔,都不跨 A、B)。所以一定存在一棵 MST 包含这条边——这就是「割性质(cut property)」。每次加边都保持「存在一棵 MST 包含已选边集」这个不变式,加满 N−1 条就得到 MST。
Prim 的直觉:从任意点出发,维护「已选点集 S」和「S 到外部的最小边」。每次取这条最小边把外部点拉进 S。同样由割性质保证正确:S 与外部之间的最小边一定属于某棵 MST。
两者关系:都是贪心 + 割性质,只是「扩展方式」不同——Kruskal 按全局边权扩展(需要并查集判环),Prim 按点集边界扩展(需要堆取最小边)。
(u, v, w),存进数组(不用建邻接表)。cnt = 0,答案 ans = 0。 - 若 find(u) != find(v):union(u,v),ans += w,cnt++;
- 若 cnt == N-1:提前结束。
cnt < N-1,说明图不连通,无 MST;否则 ans 就是最小总权值。vector<vector<pair<int,int>>> g(N+1),存 (邻点, 边权)。dist[] 全部初始化为 INF,vis[] 全 false;dist[1] = 0。(0, 1)((距离, 点))。 - 弹出 (d, u);若 vis[u] 跳过(懒删除);
- vis[u] = true,ans += d,cnt++;
- 遍历 u 的邻边 (v, w):若 !vis[v] && w < dist[v],更新 dist[v] = w,push (w, v)。
cnt == N 则有 MST,否则不连通。N = 5,边(按权值已排好序):
(1,2,1) (3,4,1) (2,3,3) (1,3,4) (2,4,6) (4,5,7)
Kruskal 过程:
| 步 | 边 | 两端是否已连通 | 动作 | 已选边数 | ans |
|---|---|---|---|---|---|
| 1 | (1,2,1) | 否 | 加 | 1 | 1 |
| 2 | (3,4,1) | 否 | 加 | 2 | 2 |
| 3 | (2,3,3) | 否({1,2} 与 {3,4}) | 加 | 3 | 5 |
| 4 | (1,3,4) | 是(1、3 已同块) | 跳过 | 3 | 5 |
| 5 | (2,4,6) | 是 | 跳过 | 3 | 5 |
| 6 | (4,5,7) | 否 | 加 | 4 = N−1 | 12 |
结束,MST 总权值 = 12,选边 {(1,2),(3,4),(2,3),(4,5)}。
注意:若把 (2,3,3) 换成 (1,3,3),同样得到权值 12 的另一棵 MST——MST 不唯一,但最小总权值唯一。
import sys
def solve():
input = sys.stdin.readline
n, m = map(int, input().split())
edges = []
for _ in range(m):
u, v, w = map(int, input().split())
edges.append((w, u, v))
edges.sort() # 按权值升序
parent = list(range(n + 1))
def find(x):
while parent[x] != x:
parent[x] = parent[parent[x]] # 路径压缩
x = parent[x]
return x
ans, cnt = 0, 0
for w, u, v in edges:
ru, rv = find(u), find(v)
if ru != rv:
parent[ru] = rv
ans += w
cnt += 1
if cnt == n - 1:
break
print(ans if cnt == n - 1 else "orz") # 不连通时按题目要求输出
solve()
#include <bits/stdc++.h>
using namespace std;
struct DSU {
vector<int> fa, sz;
DSU(int n) : fa(n + 1), sz(n + 1, 1) {
iota(fa.begin(), fa.end(), 0);
}
int find(int x) {
while (fa[x] != x) { fa[x] = fa[fa[x]]; x = fa[x]; }
return x;
}
bool unite(int a, int b) {
a = find(a); b = find(b);
if (a == b) return false;
if (sz[a] < sz[b]) swap(a, b);
fa[b] = a; sz[a] += sz[b];
return true;
}
};
struct Edge { int u, v; long long w; };
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n, m;
cin >> n >> m;
vector<Edge> edges(m);
for (auto &e : edges) cin >> e.u >> e.v >> e.w;
sort(edges.begin(), edges.end(),
[](const Edge &a, const Edge &b) { return a.w < b.w; });
DSU dsu(n);
long long ans = 0;
int cnt = 0;
for (auto &e : edges) {
if (dsu.unite(e.u, e.v)) {
ans += e.w;
if (++cnt == n - 1) break;
}
}
if (cnt == n - 1) cout << ans << '\n';
else cout << "orz\n"; // 图不连通,按题目要求输出
return 0;
}
Kruskal
Prim(堆优化)
// Kruskal + 并查集,适用于 N ≤ 2e6, M ≤ 2e6
struct DSU {
vector<int> fa, sz;
DSU(int n) : fa(n + 1), sz(n + 1, 1) { iota(fa.begin(), fa.end(), 0); }
int find(int x) { while (fa[x] != x) { fa[x] = fa[fa[x]]; x = fa[x]; } return x; }
bool unite(int a, int b) {
a = find(a); b = find(b);
if (a == b) return false;
if (sz[a] < sz[b]) swap(a, b);
fa[b] = a; sz[a] += sz[b];
return true;
}
};
struct Edge { int u, v; long long w; };
long long kruskal(int n, vector<Edge> &edges) { // 返回 -1 表示不连通
sort(edges.begin(), edges.end(),
[](const Edge &a, const Edge &b) { return a.w < b.w; });
DSU dsu(n);
long long ans = 0;
int cnt = 0;
for (auto &e : edges) {
if (dsu.unite(e.u, e.v)) {
ans += e.w;
if (++cnt == n - 1) return ans;
}
}
return -1;
}
// Prim 堆优化,适用于稠密图 / 需要邻接表的场景
long long prim(int n, vector<vector<pair<int,long long>>> &g) { // 1-indexed
const long long INF = LLONG_MAX / 2;
vector<long long> dist(n + 1, INF);
vector<char> vis(n + 1, 0);
priority_queue<pair<long long,int>, vector<pair<long long,int>>,
greater<pair<long long,int>>> pq;
dist[1] = 0; pq.push({0, 1});
long long ans = 0; int cnt = 0;
while (!pq.empty()) {
auto [d, u] = pq.top(); pq.pop();
if (vis[u]) continue;
vis[u] = 1; ans += d; cnt++;
for (auto [v, w] : g[u])
if (!vis[v] && w < dist[v]) { dist[v] = w; pq.push({w, v}); }
}
return cnt == n ? ans : -1;
}
cnt < N-1,不能直接输出 ans,要按题目要求输出 orz / -1 / impossible。多测时尤其容易忘。edges、并查集、vis、dist 每轮都要重置。用局部变量 + 重新构造最省心。ans 用 long long,边权也建议 long long。fa 大小开 n+1 且 iota 从 0 开始,1-indexed 输入直接用;0-indexed 输入要保证 fa 大小 ≥ n。u == v 在 Kruskal 里 find 相等自动跳过,没问题;重边排序后正常处理,不影响正确性。cnt == 0 == N-1 直接返回 0,注意别写成 cnt == N-1 之前就 break 出错。<= 当比较器会 UB(严格弱序要求),必须用 <。vis[u] 为真必须 continue,否则重复加点。INF 别用 INT_MAX,加法会溢出;用 LLONG_MAX/2 或 0x3f3f3f3f3f3f3f3f。cnt == N-1 要在加边之后判断,别在加边前判断。cnt == N-1 的 break 和最终判连通,直接跑完所有边,答案就是各连通块 MST 之和。map 或 set 存边——常数爆炸,1e6 规模必 TLE,用 vector + sort。priority_queue 但忘了 greater,默认是大根堆,结果全错。edges.clear() 但没 shrink_to_fit,内存反复分配;更好的做法是每轮重新声明局部 vector。int,累加用 int——1e5 条边 × 1e9 直接溢出。题 1:N=4,边 (1,2,1)、(2,3,2)、(3,4,3)、(1,4,10)、(1,3,5)。求 MST 总权值。
排序后:(1,2,1)、(2,3,2)、(3,4,3)、(1,3,5)、(1,4,10)。
答案 6。选边 {(1,2),(2,3),(3,4)}。
</details>
题 2:上题若把 (3,4,3) 改成 (3,4,100),MST 总权值是多少?图连通吗?
排序:(1,2,1)、(2,3,2)、(1,3,5)、(3,4,100)、(1,4,10)。
答案 103。图仍连通(4 通过 3 连出去),只是边权很大。
</details>
题 3:N=3,边只有 (1,2,5)。求 MST。
Kruskal 跑完只加了 1 条边,cnt=1 < N-1=2,图不连通,不存在 MST。按题目要求输出 orz / -1 / impossible。这是多测和边界最容易 WA 的地方。
</details>