GESP 8级
最小生成树 · Kruskal
8级 · 高级数据结构与DP
Kruskal 算法是一种求最小生成树(MST)的经典算法。它的核心思想非常直观:把所有边按权重从小到大排序,然后一条一条地"挑"——只要加上这条边不会形成环,就选它。最终选出的 n-1 条边构成一棵总权值最小的生成树。
💡 这是什么?
生活比喻:想象你是一个城市规划师,有 n 个城市,城市之间有一些可以修路的路线,每条路的修建费用不同。你的目标是让所有城市都互相连通,同时总花费最小。
Kruskal 的策略就像一个"精打细算的商人":
① 把所有路线按费用从低到高排列
② 从最便宜的开始,一条一条试——如果修这条路不会让已经通的城市形成"环路"(即多余的路),就修它
③ 修了 n-1 条路后,所有城市就连通了,总费用最小
关键概念回顾:
• 生成树:n 个顶点的连通图,用恰好 n-1 条边把所有顶点连起来,且没有环
• 最小生成树:所有生成树中,边权之和最小的那棵
• 环:从一个点出发,经过若干条边后又回到自己。生成树绝对不能有环
Kruskal 的策略就像一个"精打细算的商人":
① 把所有路线按费用从低到高排列
② 从最便宜的开始,一条一条试——如果修这条路不会让已经通的城市形成"环路"(即多余的路),就修它
③ 修了 n-1 条路后,所有城市就连通了,总费用最小
关键概念回顾:
• 生成树:n 个顶点的连通图,用恰好 n-1 条边把所有顶点连起来,且没有环
• 最小生成树:所有生成树中,边权之和最小的那棵
• 环:从一个点出发,经过若干条边后又回到自己。生成树绝对不能有环
🌟 为什么重要?
• 网络设计:设计计算机网络、公路网、输电网时,用最少的线缆/道路连接所有节点
• 聚类分析:在数据科学中,去掉权重最大的边可以做层次聚类
• GESP考试:8级必考内容,是图论的核心算法之一
• 竞赛高频:在信息学竞赛中经常出现,也是学习 Prim 算法的基础对比
• 聚类分析:在数据科学中,去掉权重最大的边可以做层次聚类
• GESP考试:8级必考内容,是图论的核心算法之一
• 竞赛高频:在信息学竞赛中经常出现,也是学习 Prim 算法的基础对比
📋 前置知识(学这个之前你需要知道)
1. 图的基本概念(GESP 3-4级):顶点、边、邻接表、无向图
2. 并查集(GESP 5级):快速判断两个元素是否在同一集合,快速合并两个集合——这是 Kruskal 判环的关键工具
3. 排序(GESP 1-2级):std::sort 和自定义比较函数
4. 贪心算法思想:每一步选当前最优的,最终得到全局最优
如果你还不熟悉并查集,请先看并查集相关的知识,因为 Kruskal 的核心就是"排序 + 并查集判环"。
2. 并查集(GESP 5级):快速判断两个元素是否在同一集合,快速合并两个集合——这是 Kruskal 判环的关键工具
3. 排序(GESP 1-2级):std::sort 和自定义比较函数
4. 贪心算法思想:每一步选当前最优的,最终得到全局最优
如果你还不熟悉并查集,请先看并查集相关的知识,因为 Kruskal 的核心就是"排序 + 并查集判环"。
📐 Kruskal 算法步骤
① 把图中所有边按权重从小到大排序
② 初始化并查集,每个顶点自成一个集合
③ 依次考虑每条边 (u, v, w):
如果 find(u) ≠ find(v):这条边的两端不在同一集合
→ 选这条边,merge(u, v),mst += w
如果 find(u) == find(v):这条边会形成环
→ 跳过
④ 重复直到选了 n-1 条边(或所有边都考虑完)
② 初始化并查集,每个顶点自成一个集合
③ 依次考虑每条边 (u, v, w):
如果 find(u) ≠ find(v):这条边的两端不在同一集合
→ 选这条边,merge(u, v),mst += w
如果 find(u) == find(v):这条边会形成环
→ 跳过
④ 重复直到选了 n-1 条边(或所有边都考虑完)
💻 完整代码(带详细注释)
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
const int N = 100010;
// ========== 并查集部分 ==========
int parent[N]; // parent[i] 表示节点 i 的父节点
// 查找节点 x 所属集合的代表元素(带路径压缩)
int find(int x) {
if (parent[x] != x) // 如果 x 不是自己的父节点
parent[x] = find(parent[x]); // 递归找根,并压缩路径
return parent[x]; // 返回根节点
}
// 合并两个集合:把 x 和 y 所在的集合连起来
void merge(int x, int y) {
int rx = find(x); // 找到 x 的根
int ry = find(y); // 找到 y 的根
if (rx != ry) // 如果根不同(不在同一集合)
parent[rx] = ry; // 把 x 的根指向 y 的根
}
// ========== 边的结构体 ==========
struct Edge {
int u, v, w; // u-v 之间有一条权重为 w 的边
};
// 自定义排序:按边权从小到大排序
bool cmp(Edge a, Edge b) {
return a.w < b.w; // w 小的排前面
}
// ========== Kruskal 主算法 ==========
int kruskal(vector<Edge>& edges, int n) {
// 第1步:把所有边按权重从小到大排序
sort(edges.begin(), edges.end(), cmp);
// 第2步:初始化并查集——每个节点最初是自己的父节点
for (int i = 0; i < n; i++)
parent[i] = i;
int mstWeight = 0; // 最小生成树的总权值
int edgeCount = 0; // 已经选了多少条边
// 第3步:依次考虑每条边
for (auto& e : edges) {
// 检查:这条边的两个端点是否在同一集合?
if (find(e.u) != find(e.v)) {
// 不在同一集合 → 加入这条边不会形成环
merge(e.u, e.v); // 合并两个集合
mstWeight += e.w; // 累加边权
edgeCount++; // 选中的边数+1
if (edgeCount == n - 1)
break; // 已经选了 n-1 条边,MST 完成
}
// 如果在同一集合 → 加入会形成环,跳过
}
return mstWeight;
}
int main() {
int n, m; // n个顶点,m条边
cin >> n >> m;
vector<Edge> edges(m);
for (int i = 0; i < m; i++)
cin >> edges[i].u >> edges[i].v >> edges[i].w;
cout << kruskal(edges, n) << endl;
return 0;
}
🔍 算法执行过程举例
假设有4个城市,边如下:
(0,1,1) (1,2,2) (0,2,3) (2,3,4)
第1步:排序 → (0,1,1), (1,2,2), (0,2,3), (2,3,4)
第2步:初始化 → {0}, {1}, {2}, {3} 四个独立集合
第3步:处理边 (0,1,1)
find(0)=0, find(1)=1, 0≠1 → 选!merge(0,1), 权值=1, 集合: {0,1}, {2}, {3}
第4步:处理边 (1,2,2)
find(1)=0, find(2)=2, 0≠2 → 选!merge(1,2), 权值=1+2=3, 集合: {0,1,2}, {3}
第5步:处理边 (0,2,3)
find(0)=0, find(2)=0, 0==0 → 跳过!(加了会成环)
第6步:处理边 (2,3,4)
find(2)=0, find(3)=3, 0≠3 → 选!merge(2,3), 权值=3+4=7, 集合: {0,1,2,3}
结果:选了3条边(=4-1),最小生成树权值 = 7
(0,1,1) (1,2,2) (0,2,3) (2,3,4)
第1步:排序 → (0,1,1), (1,2,2), (0,2,3), (2,3,4)
第2步:初始化 → {0}, {1}, {2}, {3} 四个独立集合
第3步:处理边 (0,1,1)
find(0)=0, find(1)=1, 0≠1 → 选!merge(0,1), 权值=1, 集合: {0,1}, {2}, {3}
第4步:处理边 (1,2,2)
find(1)=0, find(2)=2, 0≠2 → 选!merge(1,2), 权值=1+2=3, 集合: {0,1,2}, {3}
第5步:处理边 (0,2,3)
find(0)=0, find(2)=0, 0==0 → 跳过!(加了会成环)
第6步:处理边 (2,3,4)
find(2)=0, find(3)=3, 0≠3 → 选!merge(2,3), 权值=3+4=7, 集合: {0,1,2,3}
结果:选了3条边(=4-1),最小生成树权值 = 7
⏱ 复杂度分析
排序:O(m log m),m 是边数
并查集操作:O(m × α(n)),α 是反阿克曼函数,几乎等于常数
总复杂度:O(m log m)
适用场景:稀疏图(边比较少)效果好,代码也比 Prim 更好写
并查集操作:O(m × α(n)),α 是反阿克曼函数,几乎等于常数
总复杂度:O(m log m)
适用场景:稀疏图(边比较少)效果好,代码也比 Prim 更好写
⚠️ 易错点
1. 忘记排序! Kruskal 必须先把边按权重排序,否则结果不是最小生成树。
2. 并查集没路径压缩 → 复杂度退化,可能超时。find 函数一定要写路径压缩。
3. 边数判断错误 → 如果遍历完所有边还没选到 n-1 条,说明图不连通,没有生成树。
4. 节点编号从0还是从1 → 注意题目要求,并查集初始化时要对应。
5. 有重边/自环 → Kruskal 天然可以处理重边(排序后小的先处理),但自环(u==v的边)记得跳过。
2. 并查集没路径压缩 → 复杂度退化,可能超时。find 函数一定要写路径压缩。
3. 边数判断错误 → 如果遍历完所有边还没选到 n-1 条,说明图不连通,没有生成树。
4. 节点编号从0还是从1 → 注意题目要求,并查集初始化时要对应。
5. 有重边/自环 → Kruskal 天然可以处理重边(排序后小的先处理),但自环(u==v的边)记得跳过。
🎯 练习建议
入门练习:
• 洛谷 P3366 【模板】最小生成树(直接套模板)
• 洛谷 P1546 【模板】最小生成树
进阶练习:
• 洛谷 P1661 扩散(需要建模成最小生成树)
• 洛谷 P2820 电信网络(需要理解 Kruskal 的应用)
学习方法:
① 先手写并查集(find + merge),确保你很熟练
② 然后把排序 + 并查集组合起来,就是 Kruskal
③ 对比 Prim 算法(下一个知识点),理解两种方法的区别
④ 做 3-5 道模板题,确保能快速写出完整代码
• 洛谷 P3366 【模板】最小生成树(直接套模板)
• 洛谷 P1546 【模板】最小生成树
进阶练习:
• 洛谷 P1661 扩散(需要建模成最小生成树)
• 洛谷 P2820 电信网络(需要理解 Kruskal 的应用)
学习方法:
① 先手写并查集(find + merge),确保你很熟练
② 然后把排序 + 并查集组合起来,就是 Kruskal
③ 对比 Prim 算法(下一个知识点),理解两种方法的区别
④ 做 3-5 道模板题,确保能快速写出完整代码