并查集
浅谈并查集
并查集
并查集(冰茶姬)是一种树形的数据结构,它可以处理一些不交集的合并以及查询问题。主要为两种操作:
查找(Find):确定某个元素处于哪个子集;
合并(Merge):将两个子集合并成一个集合。
初始化
1 | void makeSet(int size) |
查找
通俗地讲一个故事:几个家族进行宴会,但是家族普遍长寿,所以人数众多。由于长时间的分离以及年龄的增长,这些人逐渐忘掉了自己的亲人,只记得自己的爸爸是谁了,而最长者(称为「祖先」)的父亲已经去世,他只知道自己是祖先。为了确定自己是哪个家族,他们想出了一个办法,只要问自己的爸爸是不是祖先,一层一层的向上问,直到问到祖先。如果要判断两人是否在同一家族,只要看两人的祖先是不是同一人就可以了。
在这样的思想下,并查集的查找算法诞生了。
1 | int fa[MAXN]; // 记录某个人的爸爸是谁,特别规定,祖先的爸爸是他自己 |
路径压缩
一层一层的找父亲效率太低了,所以我们直接把在路径上的每个节点都直接连接到根上,这就是路径压缩。
1 | int find(int x) |
合并
宴会上,一个家族的祖先突然对另一个家族说:我们两个家族交情这么好,不如合成一家好了。另一个家族也欣然接受了。
我们之前说过,并不在意祖先究竟是谁,所以只要其中一个祖先变成另一个祖先的儿子就可以了。
1 | void MergeSet(int x, int y) |
启发式合并
(奇技淫巧)
在合并集合时,无论将哪一个集合连接到另一个集合的下面,都能得到正确的结果。但不同的连接方法存在时间复杂度的差异。
所以合并时利用点数和深度的估价函数来降低时间复杂度。
“秩”:树的深度(未路径压缩) / 集合大小 。均摊复杂度 。
1 | //记录并初始化子树的大小为 1 |
同时采用 “路径压缩” 和 “按秩合并” 优化的并查集, 每次Get操作复杂度可进一步降低到(一个比对数函数增长还慢的函数,对于,都有,故,可近似看成一个常数,由著名计算机科学家R.E.Tarjan于1975年发表的论文中给出了证明)。
带权并查集
并查集其实就是一个森林,我们可以在树上的每条边上记录一个权值,维护一个数组,用保存节点到父节点之间的边权,在路径压缩的同时不断更新数组。
1 | int find(int x) |
并查集的应用
- 并查集能在一张无向图中维护节点之间的连通性,这是并查集的一个基本用途,实际上,并查集可以动态维护具有传递性的关系。
- 最小生成树算法中的和最近公共祖先中的算法都是基于并查集的算法。
例题🚀️
这道题呢就是用到了带权并查集,在本题中我们可以把每两号相邻的战舰之间的权值看为。
两号战舰之间的战舰数目,其实就是第号战舰的深度与第号战舰的深度的差的绝对值-。
并且我们还需要用一个数组去存每个集合的大小,去更新每个点的深度。
1 |
|
这是一道枚举加并查集,首先我们可以先把每条边按速度从大到小排序然后去枚举最大边和最小边,使速度比最小。
枚举的同时不断加边,用并查集来判断起点和终点是否联通,输出时不要忘记分子分母同除最大公因数。
1 |
|
题目读起来很简单,只需要先用并查集处理是等于的约束条件,之后在处理不等于的条件,如果不等于的两个数在同一联通块就输出,否则输出。
但是本题的数据范围过大,无法把输入的作为数组下标存储,所以我们需要用到离散化。
1 |
|
这是一道种类并查集,需要分析清楚种群,种群,种群之间的关系。
首先这三个种群之间的关系只有同类、猎物和天敌,这三种,所以我们可以开一个三倍的并查集,一倍存同类,二倍存猎物,三倍存天敌,然后不断去判断就好了,具体看代码注释(用到了拓展域的并查集)。
1 |
|
如果我们正着按顺序去摧毁,显然在时间复杂度上不允许,所以我们可以去使用逆向思维,把摧毁改为修建再利用并查集判断联通性就可以了。
1 |
|
可持久化并查集
并查集作为一个数据结构,也是有可持久化版本的。
顾名思义,可持久化并查集=可持久化+并查集=可持久化数组+并查集=主席树+并查集。👀️
首先,因为需要记录历史版本,所以路径压缩显然是不能用的;
其次,为了让并查集的高度尽量保持平衡,我们需要用到按秩合并。(如果并查集退化到一条链的情况下,效率会非常低)
可持久化并查集的操作有以下几种:
- 回到历史版本(
毕竟是可持久化数组);- 合并(
毕竟是并查集);- 查询祖先。
对于第一个操作:
1 | root[i]=root[k]; |
对于第二个操作:其实就是按秩合并;
对于第三个操作:在可持续化数组中查询。
初始建树
1 | int build(int l,int r) |
合并
1 | int merge(int now,int l,int r,int fat,int son) |
按秩合并的修改深度
1 | void add(int p,int l,int r,int x) |
得到元素在当前版本的元素编号
1 | int get_indx(int p,int l,int r,int x) |
查询祖先
1 | int find(int now,int x) |
最后放一下完整代码吧(QWQ)。
code
1 |
|
拓展—可持久化带权并查集
可持久化并查集+边带权(逃)。
最后附上我的题单。
完结撒花~~(终于写完了)🎉️ 🎉️ 🎉️
PS:
(一些资料和图例参考自OIwiki和算法竞赛进阶指南QwQ~,不喜勿喷)