德育未来集训笔记-Day 3-MST
最小生成树(Minimum Spanning Tree, MST)
概念简介
最小生成树是无向连通带权图的一个子集,满足以下两个核心条件:
- 包含原图中所有顶点(确保“生成树”的完整性,无孤立顶点);
- 所有边的权值之和最小(确保“最小”的核心特性);
- 边的数量为顶点数 \(n-1\)(确保“树”的结构,无环且连通)。
核心价值:在需要连接所有节点且追求成本最低的场景中(如通信网络搭建、道路铺设、管道建设等),最小生成树是最优解决方案。
注意:
- 仅无向连通图存在最小生成树;非连通图可求“最小生成森林”(各连通分量的最小生成树集合)。
- 若图中存在相同权值的边,可能存在多个不同结构但权值和相同的最小生成树。
核心性质
- 切割性质:对于任意一个将顶点集分为 \(S\) 和 \(V-S\) 的“切割”(无公共顶点的子集划分),切割中权值最小的边一定属于最小生成树。
- 回路性质:对于图中的任意回路,回路中权值最大的边一定不属于最小生成树。
这两个性质是经典 MST 算法(Kruskal 算法、Prim 算法)的核心理论基础。
经典算法详解
1. Kruskal 算法(克鲁斯卡尔算法)
基本思想
“加边法”:按边权值从小到大排序,依次选择权值最小的边,若该边连接两个不同的连通分量(避免形成环),则加入 MST,直到加入 \(n-1\) 条边为止。
核心步骤
- 数据结构准备:
- 存储所有边(包含起点 \(u\)、终点 \(v\)、权值 \(w\));
- 并查集(Disjoint Set Union, DSU):用于快速判断两条边的顶点是否属于同一连通分量,避免环的形成。
- 边排序:将所有边按权值 \(w\) 从小到大升序排列。
- 筛选边:
- 初始化 MST 边数为 0,总权值和为 0;
- 遍历排序后的边,对每条边 \((u, v, w)\):
- 用并查集查找 \(u\) 和 \(v\) 的根节点;
- 若根节点不同(不在同一连通分量):将该边加入 MST,总权值和累加 \(w\),MST 边数加 1;用并查集合并 \(u\) 和 \(v\) 的连通分量;
- 若 MST 边数达到 \(n-1\),直接终止遍历(已找到完整 MST)。
- 结果判断:若最终 MST 边数不足 \(n-1\),说明原图非连通,无最小生成树。
时间复杂度
- 边排序时间:\(O(m \log m)\)(\(m\) 为边数);
- 并查集操作(路径压缩 + 按秩合并):近似 \(O(1)\);
- 总体时间复杂度:\(O(m \log m)\),适合稀疏图(边数少、顶点多)。
2. Prim 算法(普里姆算法)
基本思想
“加点法”:从任意一个起点顶点开始,逐步将权值最小的“跨接边”(连接 MST 已选顶点集和未选顶点集的边)对应的未选顶点加入 MST,直到所有顶点都被加入为止。
核心步骤
- 数据结构准备:
- 邻接表 \(adj[n+1]\):存储图的边信息(每个顶点对应其邻接顶点及边权),适合稀疏图;若为稠密图,可使用邻接矩阵提升效率;
- 距离数组 \(low[n+1]\):\(low[v]\) 表示未选顶点 \(v\) 到 MST 已选顶点集的最小边权;
- 选中标记数组 \(selected[n+1]\):标记顶点是否已加入 MST;
- 总权值和 \(sum\):记录 MST 的总权值。
- 初始化:
- 任选起点(如顶点 1),设置 \(selected[1] = true\),\(sum = 0\);
- 初始化 \(low\) 数组:起点的邻接顶点 \(v\) 的 \(low[v]\) 设为对应边权,其余未选顶点的 \(low[v] = ∞\)(初始无跨接边)。
- 核心循环(共执行 \(n-1\) 次):
- 找到未选顶点中 \(low[v]\) 最小的顶点 \(u\)(若 \(low[u] = ∞\),说明原图非连通,无 MST);
- 将 \(u\) 加入 MST,\(sum += low[u]\),标记 \(selected[u] = true\);
- 更新未选顶点的 \(low\) 数组:遍历 \(u\) 的所有邻接顶点 \(v\),若 \(v\) 未被选中且 \(u→v\) 的边权 \(w < low[v]\),则更新 \(low[v] = w\)(更新跨接边的最小权值)。
- 循环结束:\(sum\) 即为 MST 的总权值,选中的顶点和边构成最小生成树。
时间复杂度
- 朴素版(邻接矩阵 + 线性查找最小 \(low[v]\)):\(O(n^2)\),适合稠密图(边数多、顶点少);
- 优化版(邻接表 + 优先队列):\(O(m \log n)\),适合稀疏图,效率接近 Kruskal 算法。
算法对比与适用场景
| 算法 | 核心思想 | 时间复杂度(主流版本) | 适用场景 | 核心优势 |
|---|---|---|---|---|
| Kruskal | 加边法(避环) | \(O(m \log m)\) | 稀疏图(\(m \ll n^2\)) | 实现简单,依赖并查集 |
| Prim | 加点法(选最小跨接边) | \(O(n^2)\)(朴素)/\(O(m \log n)\)(优化) | 稠密图(\(m \approx n^2\))/ 稀疏图 | 稠密图中效率极高 |
实例演示
问题描述
给定无向连通图,顶点数 \(n=4\)(编号 1-4),边信息如下:
| 边 | 权值 |
|---|---|
| (1,2) | 2 |
| (1,3) | 4 |
| (1,4) | 1 |
| (2,3) | 1 |
| (2,4) | 3 |
| (3,4) | 5 |
| 求该图的最小生成树。 |
Kruskal 算法求解过程
- 边排序(按权值从小到大):(1,4,1)、(2,3,1)、(1,2,2)、(2,4,3)、(1,3,4)、(3,4,5);
- 并查集初始化:每个顶点自成一个连通分量 {1}, {2}, {3}, {4};
- 筛选边:
- 选 (1,4,1):连通 {1,4},MST 边数=1,sum=1;
- 选 (2,3,1):连通 {2,3},MST 边数=2,sum=2;
- 选 (1,2,2):连通 {1,4} 和 {2,3},MST 边数=3(\(n-1=3\)),sum=4;
- 终止遍历,MST 总权值=4,边为 (1,4)、(2,3)、(1,2)。
Prim 算法求解过程(起点选 1)
- 初始化:\(selected[1]=true\),\(low=[∞, 0, 2, 4, 1]\)(索引 0 无用),sum=0;
- 第 1 次循环(选最小 low[v]):
- 未选顶点中 low[v] 最小为 1(顶点 4),加入 MST,sum=1,selected[4]=true;
- 更新 low 数组:顶点 4 的邻接边 (4,2,3)、(4,3,5),未选顶点 2 的 low[2] 仍为 2(3>2,不更新),顶点 3 的 low[3] 仍为 4(5>4,不更新);
- 第 2 次循环:
- 未选顶点中 low[v] 最小为 2(顶点 2),加入 MST,sum=1+2=3,selected[2]=true;
- 更新 low 数组:顶点 2 的邻接边 (2,3,1),未选顶点 3 的 low[3] 从 4 更新为 1;
- 第 3 次循环(n-1=3 次):
- 未选顶点中 low[v] 最小为 1(顶点 3),加入 MST,sum=3+1=4,selected[3]=true;
- 循环结束,MST 总权值=4,边为 (1,4)、(1,2)、(2,3)(与 Kruskal 结果一致)。
应用场景
- 网络搭建:通信网络、计算机网络中,用最少的线缆连接所有节点,降低建设成本;
- 路径规划:城市道路、管道铺设中,覆盖所有区域且总长度最短;
- 数据聚类:机器学习中,基于距离的聚类算法(如 Kruskal 算法可用于生成聚类树);
- 电路设计:芯片设计中,连接多个组件的最小布线成本。
最小生成树(MST)经典例题
例题1:洛谷P3366 【模板】最小生成树
题目描述
给定一个无向连通图,求其最小生成树的总权值。若图不连通,无法构成最小生成树,则输出 orz。
输入格式
第一行输入两个整数 n、m,其中:
- n 表示图的顶点数量(1 ≤ n ≤ 5000)
- m 表示图的边数量(1 ≤ m ≤ 2×10^5)
接下来 m 行,每行输入三个整数 u、v、w,表示顶点 u 和顶点 v 之间存在一条权值为 w 的无向边。
输出格式
若图连通,输出最小生成树的总权值;若图不连通,输出 orz。
样例输入
5 7
1 2 2
1 3 3
2 3 1
2 4 4
3 4 5
3 5 2
4 5 3
样例输出
8
答案
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
// 边的结构体定义
struct Edge {
int u, v, w;
// 重载小于号,用于按权值升序排序
bool operator<(const Edge& other) const {
return w < other.w;
}
};
vector<int> parent; // 并查集父节点数组
vector<int> rank_arr; // 按秩合并的秩数组,优化合并效率
// 查找根节点(带路径压缩)
int find(int x) {
if (parent[x] != x) {
parent[x] = find(parent[x]);
}
return parent[x];
}
// 合并两个集合(按秩合并)
void unite(int x, int y) {
int root_x = find(x);
int root_y = find(y);
if (root_x == root_y) return; // 已在同一集合,无需合并
// 秩小的树合并到秩大的树下
if (rank_arr[root_x] < rank_arr[root_y]) {
parent[root_x] = root_y;
} else {
parent[root_y] = root_x;
if (rank_arr[root_x] == rank_arr[root_y]) {
rank_arr[root_x]++;
}
}
}
int main() {
// 关闭同步加速输入输出,应对大数据量
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n, m;
cin >> n >> m;
// 初始化并查集
parent.resize(n + 1);
rank_arr.resize(n + 1, 0);
for (int i = 1; i <= n; ++i) {
parent[i] = i;
}
vector<Edge> edges(m);
for (int i = 0; i < m; ++i) {
cin >> edges[i].u >> edges[i].v >> edges[i].w;
}
// 按边权从小到大排序
sort(edges.begin(), edges.end());
int mst_sum = 0; // 最小生成树总权值
int edge_count = 0; // 已选入MST的边数
// 遍历所有边,构建MST
for (const Edge& e : edges) {
if (find(e.u) != find(e.v)) {
unite(e.u, e.v);
mst_sum += e.w;
edge_count++;
// 选够n-1条边,提前退出(优化)
if (edge_count == n - 1) {
break;
}
}
}
// 判断是否连通(是否选够n-1条边)
if (edge_count == n - 1) {
cout << mst_sum << endl;
} else {
cout << "orz" << endl;
}
return 0;
}
本文来自博客园,作者:jtbg,转载请注明原文链接:https://www.cnblogs.com/jtbg/articles/19476655
博客最新公告
浙公网安备 33010602011771号