【模板】最小生成树 & 并查集 & Kruskal
最小生成树 & 并查集 & Kruskal
最小生成树是什么?
最小生成树是无向有权连通图中出现的一种结构,简单来讲就是选定一些边,使得这些边能够将图中所有节点连通的同时使总边权最小,那么这些边就构成了一棵最小生成树。
由定义可以得知以下几个关于最小生成树的定理:
- 对于一张具有
个节点的图,它的最小生成树(如果存在)有且仅有 条边。 - 对于一张具有最小生成树的图,它可能有多棵不同的最小生成树。
- 重边不会影响最小生成树,因为更大的重边必然会被最小的重边替代。
接下来介绍一种求解最小生成树的算法,Kruskal算法。
基础并查集详解
在介绍Kruskal算法之前,我们需要了解下面这个基础知识点:并查集。
并查集本体由一个数组和一个搜索函数组成,它可以用来查询和操作节点之间的集合关系。
并查集的通用代码如下:
int fth[MAXN];
int find( int x ){
if( fth[x] == x ) return x;
else return fth[x] = find( fth[x] );
}
使用并查集之前要对并查集数组(即 fth[]
)进行初始化,使得最初每个节点相互独立。
一般使用 for( int i = 1 ; i < MAXN ; i++ ) fth[i] = i;
即可。
在上述通用代码中, fth[]
为并查集数组, fth[x]
即为第 x 个节点所在的集合编号,当然这里的集合编号具体是几意义不大,只需要与其他集合区分开即可。
由于初始化,不同的集合编号必然不会相同,否则则为同一个集合。
并查集的操作有两个:
- 查询两个节点是否处于同一个集合内,只需要用
find()
调出两个节点所处的集合编号并对比即可。 - 将两个集合合并,一般用
fth[ find(x) ] = find(y);
即可。这一步实际上是将y点所处的集合编号赋值给x点所处的集合,此后x点所处集合内其他节点使用find()
的时候else return fth[x] = find( fth[x] );
就会发挥作用将当前节点的集合编号更新为最新编号。
Kruskal算法就是通过并查集来确保环不会出现,或者说使用并查集来保证树的生成。
Kruskal算法详解
介绍完并查集,接下来就可以进入Kruskal算法详解了。
Kruskal的算法思路是这样的:对所有边按照边权递增的顺序进行排序,接下来从小到大,若某条边的两个顶点处于不同的集合,那么就选择这条边并且将两个顶点所处的集合合并,直到剩下一个集合,此时所有点连通,最小生成树完成。
由以上思路,不难发现时间复杂度的瓶颈在于对边的排序,因此时间复杂度为
好了讲完了
对Kruskal算法思路中的几个操作的解释
Q1:为什么按照边权从小到大的顺序遍历就可以找到最小生成树?
A1:这一点很容易用反证法证明,若用这样的方法找到的生成树不是最小生成树,那么必定存在一个更小生成树,而这两棵树之间会有若干不同。我们假设这两棵树之间只有A块与B块的连接边不同,其余部分完全一致。由于我们按照边权升序进行遍历,因此我们会先遍历到小边并且选择小边,遍历到大边时则会放弃大边,则这样的遍历方式一定会采用总边权更小的方案。
Q2:并查集在Kruskal算法中起到的作用是什么?
A2:并查集在Kruskal算法中负责标记连通块,所有互相连通的节点相当于一个集合中的所有元素。如果两个节点处于同一个集合中则意味着这两个节点可以通过已有的路径互相到达,此时这两个节点之间的路径是无用的。倘若两个节点处于不同的集合则意味着这两个节点所处的连通块互相独立,需要一条边来将两个独立的连通块连通。当全场只剩下一个连通块的时候则意味着所有节点都互相连通,此时最小生成树构建完毕。
多说无益,直接上例题和代码,其他的注释都在具体代码中了。
例题 & 代码
//------------------------
//Online Judge : Luogu
//By : KS_tips_CN
//Subject : P3366 【模板】最小生成树
//------------------------
#include<iostream>
#include<iomanip>
#include<cstdio>
#include<cmath>
#include<cstring>
#include<queue>
#include<algorithm>
#include<vector>
#define ll long long
#define reg register int
#define gc getchar()
#define MAXN 5010
#define MAXM 200010
#define MOD
using namespace std;
inline ll read( void ) ;
ll N,M,sum,tot;
struct Edge{
int x,y,val;
}e[MAXM];
//这里直接用结构体把每一条边存下来,排序更简单
inline bool cmp( Edge x , Edge y ){
return x.val < y.val ;
}
//sort使用的cmp函数,将所有边按照边权升序排序
int fth[MAXN];
inline int find( int x ){
if( fth[x] == x ) return x;
else return fth[x] = find( fth[x] );
}
//并查集部分,用来处理连通块
int main( void ) {
N = read();
M = read();
for( reg i = 1 ; i <= N ; i++ )
fth[i] = i ;
//并查集数组的初始化
for( reg i = 1 ; i <= M ; i++ ){
e[i].x = read();
e[i].y = read();
e[i].val = read();
}
sort( e+1 , e+M+1 , cmp );
tot = N ;
//tot用来统计当前剩余连通块数量,由于最初没有采用任何边的时候每个节点各自为一个连通块,因此初始连通块数量 = 节点数量
for( reg i = 1 ; i <= M ; i++ ){
if( find( e[i].x ) == find( e[i].y ) )
continue;
//不考虑连通块内的边
fth[ find( e[i].x ) ] = find( e[i].y ); //集合合并
tot--; //连通块数量-1
sum += e[i].val; //统计最小生成树边权和
}
if( tot == 1 ) cout << sum << endl ; //如果连通块只剩下一个,则所有节点已连通,构成最小生成树
else printf("orz\n");
return 0;
}
inline ll read( void ) {
ll x = 0 , f = 0 ;
char ch = gc ;
while( !isdigit( ch ) )
f |= ( ch == '-' ) , ch = gc ;
while( isdigit( ch ) )
x = ( x << 1 ) + ( x << 3 ) + ( ch ^ 48 ) , ch = gc ;
return f ? -x : x ;
}
以上就是基础Kuskal算法的详解了,本人水平有限,若对该算法或代码有不解的地方随时可以提问,欢迎各位提供建议以及指出不足。
结语
这是本人OI退役后发的第一篇算法博客,自从NOIP2022结束到高考结束已经过去了大半年,在这大半年内我一直沉浸在文化课的摧残中(什),好在高考的结果令人满意。
眼下即将进入大学校园,虽然没有进入理想的计算机类专业进行学习,但是还是想在大学期间重拾我对算法竞赛的执念与热爱,希望这篇博客能够作为我ACM经历上良好的开端。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】