文章目录
- [题型:最小生成树(Minimum Spanning Tree, MST)](#题型:最小生成树(Minimum Spanning Tree, MST))
-
- [1. 核心思路](#1. 核心思路)
-
- [1.1 基本概念](#1.1 基本概念)
- [1.2 算法选择](#1.2 算法选择)
- [2. Prim算法](#2. Prim算法)
-
- [2.1 核心思想](#2.1 核心思想)
- [2.2 算法模板](#2.2 算法模板)
- [3. Kruskal算法](#3. Kruskal算法)
-
- [3.1 核心思想](#3.1 核心思想)
- [3.2 算法模板](#3.2 算法模板)
- [4. 典型应用场景](#4. 典型应用场景)
- [5. 算法对比总结](#5. 算法对比总结)
题型:最小生成树(Minimum Spanning Tree, MST)
1. 核心思路
最小生成树是在一个连通无向加权图中,找到一棵包含所有顶点且边权值之和最小的树。
1.1 基本概念
- 生成树:包含图中所有顶点的连通子图,且是树结构(无环,n个顶点有n-1条边)
- 最小生成树:所有生成树中边权值之和最小的那棵
- 应用场景 :
- 网络布线:用最少的成本连接所有节点
- 道路规划:用最少的成本连接所有城市
- 集群连接:用最少的成本连接所有服务器
示例:
原图: 最小生成树:
A---3---B A---3---B
| \ | | |
1 4 2 | |
| \ | | |
C---5---D C D
(总权重: 3+2+1=6)
1.2 算法选择
最小生成树有两种经典算法:Prim算法 和Kruskal算法
| 比较项 | Prim算法 | Kruskal算法 |
|---|---|---|
| 核心思想 | 从节点角度,维护节点集合 | 从边角度,维护边集合 |
| 数据结构 | 邻接矩阵/邻接表 + minDist数组 | 边列表 + 并查集 |
| 时间复杂度 | O(V²) 或 O((V+E)logV) | O(ElogE) |
| 适用场景 | 稠密图(边多) | 稀疏图(边少) |
| 实现难度 | 中等 | 简单 |
选择建议:
- 稠密图 (边数接近V²)→ 使用 Prim算法
- 稀疏图 (边数远小于V²)→ 使用 Kruskal算法
2. Prim算法
2.1 核心思想
从节点角度采用贪心策略,每次寻找距离最小生成树最近的节点并加入到最小生成树中。
算法流程:
- 初始化:选择任意一个节点作为起点,加入生成树
- 重复以下步骤,直到所有节点都加入生成树:
- 步骤1:从非生成树节点中,选择距离生成树最近的节点
- 步骤2:将该节点加入生成树
- 步骤3:更新非生成树节点到生成树的最小距离(更新minDist数组)
关键数据结构:
minDist[i]:记录节点i到最小生成树的最小距离isInTree[i]:标记节点i是否已在生成树中
2.2 算法模板
cpp
//https://kamacoder.com/problempage.php?pid=1053
#include<iostream>
#include<vector>
#include<climits> //包含一堆宏,用来告诉不同整数类型的最小值与最大值
using namespace std;
int main(){
int v,e;//v是顶点数,e是边数
int x,y,k;//节点和值
cin>>v>>e;
vector<vector<int>> grid(v+1,vector<int>(v+1,10001));//题目说的val最大值是10000
while(e--){
cin>>x>>y>>k;
//双向图
grid[x][y]=k;
grid[y][x]=k;
}
//所有节点到最小生成树的最小距离
vector<int>minDist(v+1,10001);
//该节点是否在最小生成树中
vector<int>isInTree(v+1,false);
//v个顶点,即v-1条边
for(int i=1;i<v;i++){//去掉i=0,使得下标和节点相对应
//1. prim第一步:选取距离生成树最近的节点加入生成树
int cur=-1;//选取哪个节点加入生成树
int minVal=INT_MAX;
for(int j=1;j<=v;j++){//这里需要=v是因为最后一个节点也要最后加入,但是i就不需要,因为i是边
//选取最小生成树节点的条件:
//1. 不在最小生成树中
//2. 距离最小生成树最近的节点
if(!isInTree[j]&&minDist[j]<minVal){
minVal=minDist[j];
cur=j;//遍历出来最近节点
}
}
//2. 最近节点加入最小生成树
isInTree[cur]=true;
//3. 更新非生成树节点到生成树的距离,即更新minDist数组
//就是因为cur节点加入,所以要更新
//变化的只有和cur这个新加入的节点相关联的非最小生成树的节点,所以我们要比较的就是这些与cur节点关联的非生成树节点的距离是否比原来非生成树节点到生成树节点到距离更小
for(int j=1;j<=v;j++){
//更新条件
//1. 节点是非生成树中的节点
//2. 与cur相连的非生成树节点权值比该节点到最小生成树距离要小
if(!isInTree[j]&&grid[cur][j]<minDist[j]){
minDist[j]=grid[cur][j];
}
}
}
//统计结果
int result=0;
for(int i=2;i<=v;i++){
//不计第一个节点,因为是v-1个边
result+=minDist[i];
}
cout<<result<<endl;
- 基础版本 :O(V²)
- 外层循环:V-1次
- 内层循环:每次遍历V个节点找最小值,再遍历V个节点更新距离
- 优化版本 (使用优先队列):O((V+E)logV)
- 使用堆优化找最小值的操作
3. Kruskal算法
3.1 核心思想
从边角度采用贪心策略,按边权值从小到大排序,依次选择不会形成环的边加入生成树。
算法流程:
- 排序:将所有边按权值从小到大排序
- 遍历 :依次考虑每条边
- 如果边的两个端点不在同一集合(不会形成环)→ 加入生成树,合并两个集合
- 如果边的两个端点在同一集合(会形成环)→ 跳过
- 判断:当生成树有V-1条边时,算法结束
关键数据结构:
- 边列表:存储所有边的信息(起点、终点、权值)
- 并查集:判断两个节点是否在同一集合,避免形成环
3.2 算法模板
cpp
//https://kamacoder.com/problempage.php?pid=1053
#include<iostream>
#include<vector>
#include<algorithm>
using namespace std;
//l,r是节点,val是值
struct Edge{
int l,r,val;
};
//节点数量
int n=10001;
//并查集
vector<int> father(n,-1);//节点编号从1开始,最多10000个节点
//并查集初始化
void init(){
for(int i=0;i<n;i++){
father[i]=i;
}
}
//并查集查找
int find(int u){
return u==father[u] ? u:father[u]=find(father[u]);//路径压缩
}
//并查集的加入
void join(int u,int v){
u=find(u);
v=find(v);
if(u==v) return;
father[v]=u;
}
int main(){
int v,e;//v是节点数,e是边数
int v1 ,v2,val;
vector<Edge> edges;
int result_val=0;
cin>>v>>e;
while(e--){
cin>>v1>>v2>>val;
edges.push_back({v1,v2,val});
}
//kruskal算法
//先对边权值进行排序
sort(edges.begin(),edges.end(),[](const Edge& a,const Edge& b){
return a.val<b.val;
});
//并查集初始化
init();
//对排序之后的边进行遍历
for(Edge edge:edges){
//首先通过并查集找出两个节点的祖先
int x=find(edge.l);
int y=find(edge.r);
if(x!=y){//如果祖先不同,那么就可以加入
result_val+=edge.val;//这条边可以作为生成树的边
join(x,y);//两个节点加入到同一个集合
}
}
cout<<result_val<<endl;
return 0;
}
4. 典型应用场景
-
网络布线问题
- LeetCode 1584. 连接所有点的最小费用
- 用最少的成本连接所有网络节点
-
道路规划问题
- 用最少的成本连接所有城市
- 保证所有城市都能到达
-
集群连接问题
- 用最少的成本连接所有服务器
- 保证所有服务器都能通信
-
资源分配问题
- 用最少的成本分配资源
- 保证所有需求都能满足
5. 算法对比总结
| 特性 | Prim算法 | Kruskal算法 |
|---|---|---|
| 维护对象 | 节点集合 | 边集合 |
| 数据结构 | 邻接矩阵/邻接表 | 边列表 + 并查集 |
| 时间复杂度 | O(V²) 或 O((V+E)logV) | O(ElogE) |
| 空间复杂度 | O(V²) 或 O(V+E) | O(V+E) |
| 适用图类型 | 稠密图 | 稀疏图 |
| 实现难度 | 中等 | 简单 |
| 是否需要排序 | 否 | 是(边排序) |
| 是否需要并查集 | 否 | 是 |
选择建议:
- 稀疏图 (E << V²)→ Kruskal算法更优
- 稠密图 (E ≈ V²)→ Prim算法更优
- 一般情况 → Kruskal算法实现更简单,推荐使用