德育未来集训笔记-Day 3-MST

最小生成树(Minimum Spanning Tree, MST)

概念简介

最小生成树是无向连通带权图的一个子集,满足以下两个核心条件:

  1. 包含原图中所有顶点(确保“生成树”的完整性,无孤立顶点);
  2. 所有边的权值之和最小(确保“最小”的核心特性);
  3. 边的数量为顶点数 \(n-1\)(确保“树”的结构,无环且连通)。

核心价值:在需要连接所有节点且追求成本最低的场景中(如通信网络搭建、道路铺设、管道建设等),最小生成树是最优解决方案。

注意:

  • 仅无向连通图存在最小生成树;非连通图可求“最小生成森林”(各连通分量的最小生成树集合)。
  • 若图中存在相同权值的边,可能存在多个不同结构但权值和相同的最小生成树。

核心性质

  1. 切割性质:对于任意一个将顶点集分为 \(S\)\(V-S\) 的“切割”(无公共顶点的子集划分),切割中权值最小的边一定属于最小生成树。
  2. 回路性质:对于图中的任意回路,回路中权值最大的边一定不属于最小生成树。
    这两个性质是经典 MST 算法(Kruskal 算法、Prim 算法)的核心理论基础。

经典算法详解

1. Kruskal 算法(克鲁斯卡尔算法)

基本思想

“加边法”:按边权值从小到大排序,依次选择权值最小的边,若该边连接两个不同的连通分量(避免形成环),则加入 MST,直到加入 \(n-1\) 条边为止。

核心步骤

  1. 数据结构准备
  • 存储所有边(包含起点 \(u\)、终点 \(v\)、权值 \(w\));
  • 并查集(Disjoint Set Union, DSU):用于快速判断两条边的顶点是否属于同一连通分量,避免环的形成。
  1. 边排序:将所有边按权值 \(w\) 从小到大升序排列。
  2. 筛选边
  • 初始化 MST 边数为 0,总权值和为 0;
  • 遍历排序后的边,对每条边 \((u, v, w)\)
  • 用并查集查找 \(u\)\(v\) 的根节点;
  • 若根节点不同(不在同一连通分量):将该边加入 MST,总权值和累加 \(w\),MST 边数加 1;用并查集合并 \(u\)\(v\) 的连通分量;
  • 若 MST 边数达到 \(n-1\),直接终止遍历(已找到完整 MST)。
  1. 结果判断:若最终 MST 边数不足 \(n-1\),说明原图非连通,无最小生成树。

时间复杂度

  • 边排序时间:\(O(m \log m)\)\(m\) 为边数);
  • 并查集操作(路径压缩 + 按秩合并):近似 \(O(1)\)
  • 总体时间复杂度:\(O(m \log m)\),适合稀疏图(边数少、顶点多)。

2. Prim 算法(普里姆算法)

基本思想

“加点法”:从任意一个起点顶点开始,逐步将权值最小的“跨接边”(连接 MST 已选顶点集和未选顶点集的边)对应的未选顶点加入 MST,直到所有顶点都被加入为止。

核心步骤

  1. 数据结构准备
  • 邻接表 \(adj[n+1]\):存储图的边信息(每个顶点对应其邻接顶点及边权),适合稀疏图;若为稠密图,可使用邻接矩阵提升效率;
  • 距离数组 \(low[n+1]\)\(low[v]\) 表示未选顶点 \(v\) 到 MST 已选顶点集的最小边权;
  • 选中标记数组 \(selected[n+1]\):标记顶点是否已加入 MST;
  • 总权值和 \(sum\):记录 MST 的总权值。
  1. 初始化
  • 任选起点(如顶点 1),设置 \(selected[1] = true\)\(sum = 0\)
  • 初始化 \(low\) 数组:起点的邻接顶点 \(v\)\(low[v]\) 设为对应边权,其余未选顶点的 \(low[v] = ∞\)(初始无跨接边)。
  1. 核心循环(共执行 \(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\)(更新跨接边的最小权值)。
  1. 循环结束\(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. 边排序(按权值从小到大):(1,4,1)、(2,3,1)、(1,2,2)、(2,4,3)、(1,3,4)、(3,4,5);
  2. 并查集初始化:每个顶点自成一个连通分量 {1}, {2}, {3}, {4};
  3. 筛选边:
  • 选 (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;
  1. 终止遍历,MST 总权值=4,边为 (1,4)、(2,3)、(1,2)。

Prim 算法求解过程(起点选 1)

  1. 初始化:\(selected[1]=true\)\(low=[∞, 0, 2, 4, 1]\)(索引 0 无用),sum=0;
  2. 第 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,不更新);
  1. 第 2 次循环:
  • 未选顶点中 low[v] 最小为 2(顶点 2),加入 MST,sum=1+2=3,selected[2]=true;
  • 更新 low 数组:顶点 2 的邻接边 (2,3,1),未选顶点 3 的 low[3] 从 4 更新为 1;
  1. 第 3 次循环(n-1=3 次):
  • 未选顶点中 low[v] 最小为 1(顶点 3),加入 MST,sum=3+1=4,selected[3]=true;
  1. 循环结束,MST 总权值=4,边为 (1,4)、(1,2)、(2,3)(与 Kruskal 结果一致)。

应用场景

  1. 网络搭建:通信网络、计算机网络中,用最少的线缆连接所有节点,降低建设成本;
  2. 路径规划:城市道路、管道铺设中,覆盖所有区域且总长度最短;
  3. 数据聚类:机器学习中,基于距离的聚类算法(如 Kruskal 算法可用于生成聚类树);
  4. 电路设计:芯片设计中,连接多个组件的最小布线成本。

最小生成树(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;
}
posted @ 2026-01-13 13:01  jtbg  阅读(1)  评论(0)    收藏  举报