#创作计划#最小生成树
2025-09-29 20:52:12
发布于:浙江
完结
对于最小生成树,我将会从一下几个角度进行讲解
目录
-
1.并查集
-
2.最小生成树的概念
-
3.最小生成树的性质
-
4.算法---Kruskal
-
5.算法---Prim
-
6.附:算法对比
一、并查集
把并查集拆开看:
“并”,顾名思义就是合并;
“查”,顾名思义就是查找;
“集”,还是顾名思义就是集合。
可以把集合理解成几个本互不相干的部落,那么刚开始每个部落的首领就为本身。
部落:0 1 2 3 4
首领:0 1 2 3 4
而合并,可以理解为两个部落打架,输的一方被赢的一方吞并。
部落:0 1 2 3 4
︱ \ / \ /
首领:0 1 4
查找,即查找某个部落的首领。针对上一张表,可得出以下结果
查找0->0
查找1->1
查找2->1
查找3->4
查找4->4
其实细心的人会发现,这张图表十分像树,那么我们可以通过数组 来存储并查集, 表示第 个节点(部落)的父节点(首领)。
接下来上干货:
#include<iostream>
using namespace std;
int f[105];
int main(){
for(int i=1;i<=100;i++) f[i]=i; //初始化,一开始第i个节点的父节点为i
return 0;
}
查找实际上就是从节点开始,一步步往上找,直到找到最终的父节点,可以用以下函数实现。
int find(int x){ //查找节点x的最终父节点
if(x==f[x]) return x; //查找到了
return f[x]=find(f[x]); //新赋值父节点
}
由于函数通过数组直接返回节点父节点值,无需复杂计算,所以该函数时间复杂度为感人的 。
合并就简单了,只要将两个节点的父节点都改成相同的即可。
#include<iostream>
using namespace std;
int f[105];
int main(){
for(int i=1;i<=100;i++) f[i]=i; //初始化,一开始第i个节点的父节点为i
f[13]=f[21]; //节点21将节点13合并,也就是节点13的父节点改为了节点21的父节点
return 0;
}
此代码执行过后,find(f[13])=find(f[21])
。
二、最小生成树的概念
要理解最小生成树,我们先要明白什么是“生成树”。
图:由一组顶点和连接顶点的边组成。
连通图:图中任意两个顶点之间都存在路径。
树:一个没有环的连通图。
生成树:一个连通无向图的生成树是它的一个连通子图,它包含原图的所有顶点,但边数最少(恰好有 条边,其中 是顶点数),并且没有环。
简单来说,生成树就是用最少的边把图中所有的顶点都连通起来,并且保证没有回路。
举个例子:
想象一个城市网络,每个顶点代表一个城市,边代表城市之间的道路。这个城市网络本身可能有很多条路,甚至有多条路连接相同的两个城市(形成环)。那么,这个网络的一个“生成树”就是一套能让你从任何一个城市开车到任何其他城市,且道路数量最少的路线规划。这套规划里不会有“环路”或“冗余的道路”。
现在,我们给图的每条边加上一个权重的概念。权重可以代表距离、成本、时间等。
最小生成树就是:在一个带权连通无向图中,总权重和最小的那棵生成树。
继续上面的例子:
现在每条道路(边)都有修建成本(权重)。我们想找出一套道路规划,既能连接所有城市(生成树),又使得总的修建成本最低。这套规划就是这个城市网络的最小生成树。
核心目标:在保证所有点连通的前提下,选择总权重最低的边。
三、最小生成树的性质
- 唯一性:最小生成树不一定唯一。当图中存在多条权重相同的边,并且可以互换时,就可能产生多个不同的最小生成树,但它们的总权重是相同的。
- 边数:任何最小生成树都必然包含恰好 条边( 是顶点数)。
- 环性质:在最小生成树中,任意加上一条原本不在树中的边,都会形成一个环。并且这个环上新加入的那条边,一定是环上权重最大的边(否则就可以用它替换掉环上更大的边,得到更小的总权重,与“最小”矛盾)。
- 切割性质:把图的顶点分成任意两个集合(一个切割),连接这两个集合的权重最小的边,必然属于图的最小生成树。
四、算法---Kruskal
核心思想:将所有边按权重从小到大排序,然后依次考虑每条边。如果加入当前边不会与已选择的边形成环,就将其加入生成树;否则就跳过。
- 排序:把所有边按权重从低到高排好。
- 检查:从权重最低的边开始检查,如果加上这条边不会使原来的树形成自环,就加入这条边
- 重复:直到修好了 条路。
关键技术:如何高效判断加入一条边是否会形成环?使用并查集数据结构。
初始时,每个顶点自成一个集合。
当考虑一条边的起点 和终点 时,检查 和 是否在同一个集合中。
如果在,说明加入这条边会形成环,舍弃。
如果不在,则加入这条边,并将 和 所在的集合合并。
时间复杂度:排序边需要 ,而并查集操作近乎常数时间,所以主要复杂度为 ,非常适合稀疏图,其中 为边数。
这是一道模板题
题解:
#include<iostream>
#include<algorithm>
using namespace std;
struct node{ //定义结构体,内涵起点、终点、成本
int a,b,w;
}a[105];
int f[105];
bool cmp(node a,node b){ //排序函数
return a.w<b.w;
}
int find(int x){ //查找函数
if(x==f[x]) return f[x];
return f[x]=find(f[x]);
}
int main(){
int n,k;
cin>>n>>k;
for(int i=1;i<=n;i++) f[i]=i;
for(int i=1;i<=k;i++) cin>>a[i].a>>a[i].b>>a[i].w;
sort(a+1,a+k+1,cmp); //按边排序
long long cnt=0;
for(int i=1;i<=k;i++){
if(f[find(a[i].a)]!=f[find(a[i].b)]){ //如果父节点不同(尚未连通)
f[find(a[i].a)]=find(a[i].b); //合并节点,使父节点相同(使其连通)
}
else cnt+=a[i].w; //注意这题是求最大值,如果求最小,应将该语句放入if语句中
}
cout<<cnt;
return 0;
}
五、算法---Prim
核心思想:从某一个顶点开始,每次选择一条连接已构建的树 与未加入树的顶点的权重最小的边,并将该边和对应的顶点加入到树中。
- 起点:随便选一个顶点。
- 生长:每次从这棵树向外延伸出最小权重的边,把一个新的点连进来。
- 重复:直到所有的点都被连入这棵树。
适用数据结构:通常使用优先队列(最小堆) 来高效地获取当前可用的最小边。
时间复杂度:
使用邻接矩阵和简单遍历: ,适合稠密图。
使用邻接表和二叉堆: ,适合稀疏图。(一般直接用Kruskal,很少用)
六、附:算法对比
特性 | Kruskal | Prim |
---|---|---|
核心思想 | 加边法,按权重从小到大选择边 | 加点法,从一点出发逐步扩张树 |
数据结构 | 并查集 + 边排序 | 优先队列(最小堆) |
时间复杂度 | 或 | |
适用场景 | 稀疏图(边较少时效率很高) | 稠密图(边很多时,用邻接矩阵可能更快) |
图是否连通 | 需额外检查是否有 条边来判断图是否连通 | 算法过程自然保证连通性 |
制作不易,给个赞吧
@AC君求加精
全部评论 6
更正一下,prim 堆优化是 ,kruskal 是 ,其中 为边的个数
9小时前 来自 广东
1好的谢谢
9小时前 来自 浙江
1已更正
9小时前 来自 浙江
1
d
9小时前 来自 浙江
1不更完就不赞
(((
昨天 来自 浙江
1昨天 来自 浙江
1被做局了
昨天 来自 浙江
1@ppl我更完了
20小时前 来自 浙江
1
zc
昨天 来自 浙江
1昨天 来自 浙江
1
d
昨天 来自 浙江
1d
昨天 来自 浙江
1
有帮助,赞一个