并查集笔记

并查集笔记

并查集,一种可以高效处理连通区块问题的数据结构,可以优化集合合并操作,判断集合是否连通

它的原理可以简单概括为:

设置一个数组,把每个元素的祖先节点存在数组中,当要查询这个节点所在集合时,通过层层向上,最终找到一个最远祖先

初始化祖先数组时,我们将每个元素的祖先都设置为他自己,当我们查询节点时,如果当前走到的节点的祖先就是他自己,那么他就是这个集合的最远祖先

合并集合时,我们只需要通过一次操作,查找这两个集合各自的最远祖先,然后对最远祖先进行操作,将其中一个的祖先指向另外一个(而不是指向自己),那么就完成区间的合并,这一步存在多种优化方案,如路径压缩,按秩合并等

这里以洛谷P3367为例

在进行操作前,我们首先要设计祖先数组,这个数组存储的是各个节点的祖先节点的位置

int fa[N];

然后对数组进行初始化操作

void init(int x) {
for (int i = 0; i <= n; i++) fa[i] = i;
}

遍历所有用得到的节点,将他们的祖先初始化为他们自己

构造查找函数,目的是返回这个元素的最远祖先

int find(int x) {
return x == fa[x] ? x : find(fa[x]);
}

使用递归写法,如果 x==fa[x] 说明已经走到了最远祖先的位置,那么就返回当前的 x ,如果不是,则继续查找 fa[x] 的祖先,即查找 x 的祖先的祖先,直到走到最远祖先的位置

merge 函数,目的是对两个集合进行合并操作

void merge(int i, int j) {
i = find(i), j = find(j);//查找各自的最远祖先
if (i == j) return;//如果祖先相同,则无需操作
fa[i] = j;//将i的祖先指向j
// fa[j] = i; 也可以
}

以上就是朴素的并查集写法,没有任何的优化

不难发现,如果的我们的数据量较大时,那么某个集合的边会变得十分长,从树状结构退化为一条链,大大增加了查找的时间复杂度

这里首先介绍一下路径压缩的优化方案

路径压缩,指的是将查找最远祖先的过程 从开始查找的节点到最远祖先节点的路径上的所有经过的节点,重新将他们的祖先节点指向最远祖先节点

img

从初始的链结构变化为树结构

img

这个操作改变了并查集树的结构,具有不可逆性,但是时间复杂度非常优秀,能够做到在近似 \(O(1)\) 的时间内找到一个节点的祖先

代码的修改也十分简洁,只需要在原有查找函数的中间,进行祖先的重定向操作

int find(int x) {
return fa[x] = x == fa[x] ? x : find(fa[x]);
}

这一步把 fa[x] 赋值为 xfind(fa[x]) 即将路径上经过的点的祖先节点更改为他们的最远祖先节点

然后是按秩合并的优化方案

我们知道,并查集的操作就是对不同的树进行操作,每棵树都有其对应的高度

我们在合并两颗树时,合并后的树的高度不会超过合并前两个树的最大高度再加 \(1\) ,即:

\(h\_end<=max(h\_1,h\_2)+1\)

我们需要额外增加一个数组来存储各个节点的秩(当前节点的高度)

int rnk(N);

一开始,我们每个节点的秩都为 \(0\) ,所以将 rnk 开在全局变量里面即可

然后对合并操作进行改写,我们的目标肯定是降低合并后树的高度,树的高度越低,我们查找节点的祖先就越快

void merge(int i, int j) {
i = find(i), j = find(j);
if (i == j) return;
if (rnk[i] > rnk[j]) swap(i, j); //这里保证 rnk[i] <= rnk[j]
fa[i] = j; //将秩较小的集合合并到秩较大的集合上
if (rnk[i] == rnk[j]) rnk[j]++;//如果他们的秩相同的话,那么合并到的集合的秩就需要加1
}
posted @   才瓯  阅读(19)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· AI技术革命,工作效率10个最佳AI工具
点击右上角即可分享
微信分享提示