并查集+最小生成树 学习笔记
图论系列:
前言:
咲いた野の花よ
ああどうか教えておくれ
人は何故傷つけあって
争うのでしょう
相关题单:
终于补全了,┭┮﹏┭┮。
一.并查集
1.基础定义与操作
(1)定义
并查集是一种用于管理元素所属集合的数据结构,实现为一个森林,其中每棵树表示一个集合,树中的节点表示对应集合中的元素。
(2)操作
合并(merge):合并两个元素所属集合(合并对应的树)
查询(find):查询某个元素所属集合(查询对应的树的根节点),这可以用于判断两个元素是否属于同一集合
并查集在经过修改后可以支持单个元素的删除、移动;使用动态开点线段树还可以实现可持久化并查集。
2.算法流程:
在并查集中,我们将一个个元素看成一个个点,将逻辑所属关系转化为图上的连通性问题。
(1)初始化
一开始每一个元素都属于一个单独的集合,将元素看作点,相当于现在有很多个只有根节点的树。方便起见,我们将根节点的父亲设为自己。(如图)
代码:
for(int i=1;i<=n;i++) fa[i]=i;
(2)查询
查询很简单,由于我们记录了每个点的父亲节点(而根节点的父亲就是自己),那么我们就一直跳父亲直到跳到一个点满足
如图,对于点 5 ,
代码:
inline int find(int x)
{
if(x!=fa[x]) return find(fa[x]);//当前不是根节点就跳根节点
return fa[x];//说明是根节点了,x=fa[x],返回的实际上就是根节点
}
(3)合并
对于两个点
这个时候我们再去判断原本属于
如上图,现在我们要合并 5 所在的集合与 9 所在的集合。首先找到经过跳父亲找到
代码:
inline void merge(int x,int y)
{
x=find(x),y=find(y);//找到两点的根节点
if(x==y) return ;//如果已经在一个集合内了就不管
fa[x]=y;//否则将其中一个根节点的父亲设为另一个点的根节点
}
(4)路径压缩
但是暴力跳父亲明显存在一定的问题,比如对于一条长为
考虑优化。首先对于从链尾到根节点路径上经过的所有点,肯定都是属于根节点代表的这个集合内的,那么每次跳父亲也太费劲了。在查询的过程中,由于需要一层一层的跳父亲,于是通过递归,找到根节点之后,将路径上所有点的父亲都设为根节点,这样以后查询就只需要跳一次了。
优化过后的并查集时间复杂度是反阿克曼函数,将当作一个较小的常数即可。
如上图,现在我们用路径压缩来查询 9 节点的根节点,跳父亲就是
代码:
inline int find(int x)
{
if(x!=fa[x]) fa[x]=find(fa[x]);
return fa[x];//如果当前是根节点,那么返回的就是根节点,如果不是(因为上面那句没有加 return ),所以路径上的所有点都会遍历这一句,此时fa[x]都被修改为根节点了,所以返回的还是根节点
}
(5)启发式合并
由于得知了并查集找根的实质与路径压缩的优化过程,所以对于一个集合代表的树,树高越小时间复杂度自然越小。对于两个集合,那么元素个数小的集合合并到元素个数大的集合时间复杂度会更优(学术上有证明,真的跑的很快,后面会介绍树上启发式合并)。
代码:
//查询的代码不变,多维护一个siz数组
inline void merge(int x,int y)
{
x=find(x),y=find(y);
if(x==y) return ;
if(siz[x]>siz[y]) swap(x,y);
fa[x]=y,siz[y]+=siz[x];//保证是小集合合并到大集合,同时记录当前集合的大小
}
for(int i=1;i<=n;i++) fa[i]=i,siz[i]=1;//初始化每个集合大小为1
(6)删除
这个很牛啊,想了很久,结果有板子题。
对于删除单点
既然直接改变
但是这里又产生了第二个问题:我怎么才能知道真正的
当然在
于是初始化的时候
那么删除的时候就非常简单了,初始化
可能有点抽象,画图理解,对于上图删去点 2(红色值表示的就是
除了初始化,其余操作代码均不变。
(7)移动
将某个元素移动到另一个集合去。显然的,删了以后再加不久行了。
(8)带权/拓展域
带权并查集,这个在例题里结合来讲可能会清晰一点。
3.应用
我觉得 oi wiki 的专题已经比较全面了,其他的可能结合后面的题加深对一些并查集套路的理解。
https://oi-wiki.org/topic/dsu-app/
二.最小生成树
这里讨论的都是无向图连通图内的最小生成树,有向图最小生成树是最小树形图,以后可能会写吧。
1.相关定义
生成树:对于连通图,形态是一棵树的生成子图称为生成树。(所以生成树是一颗连通了图上所有点的树。非连通图不存在生成树。)
生成森林:由每个连通分量的生成树组成的子图称为 生成森林。
非树边:对于某棵生成树,原图的不在生成树上的边称为非树边。
给定一张带权连通图,求其边权和最小的生成树,称为 最小生成树(Minimum Spanning Tree,MST)。对于非连通图,对每个连通分量求最小生成树即得最小生成森林。
2.算法
最小生成树常见有三种(Kruskal,Prim与Boruvka)求法,其实只用学 Kruskal 与 Boruvka 就可以了(毕竟 Kruskal 后面有 Kruskal 重构树,Boruvka 可以解决一些特殊图)。
理解 Kruskal 实际上就是按边权大小从小到大排序之后,使用并查集维护当前边连接的两点是否已经在同一个集合内了,如果在就跳过,不在就加入这条边,然后在并查集中将这两点合并起来。
我好懒啊,其实算法挺简单的,oiwiki 这部分写的挺全面的 https://oi-wiki.org/graph/mst
主要是 oiwiki 上的动图比较形象。
至于第
练习题:
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探
· 为什么 退出登录 或 修改密码 无法使 token 失效