并查集⚓︎
特点:思维性较强,代码较短,常出现于面试。
定义⚓︎
并查集的操作:
- 将两个集合合并(并);
- 询问两个元素是否在一个集合当中(查)。
并查集维护的是一堆集合(集)。
算法⚓︎
理论考察⚓︎
原始可以使用两个数组:第一个数组保存所有元素;第二个数组使用数组的下标来代表数组一中元素,对应位置上存放的值表示元素所属的集合。但第二个数组中保存的是第一个数组中各个元素所属集合,所以合并集合的时候,第二个数组中需要修改元素比较多。
修正:可以为每个集合选出一个代表它的元素,数组二中存放代表元素。第一个数组保存了所有元素,第二个数组保存了能代表该元素的元素。这个时候,如果要合并两个集合,只需要修改代表元素即可。
步骤:
- 用一个数组保存对应位置元素所属集合的代表元素;
- \(A,B\) 两个集合合并:将 \(B\) 集合代表元素的代表元素设置为 \(A\) 集合的代表元素;
- 查找 \(C\) 元素属于哪个集合:找 \(C\) 元素的代表元素,如果不是他自己,就重复查找代表元素的代表元素,直到查找到一个元素的代表元素是它自己, \(C\) 就属于整个代表元素所代表的集合。
时间复杂度:“近乎” \(O(1)\)。
实现的基本原理⚓︎
用树的形式维护集合,每个集合用一棵树来表示,树根的编号就是整个集合的编号,每个节点存储它的父节点,p[x]
表示x
的父节点。
- 问题1:如何判断树根?
if (p[x] == x)
; - 问题2:如何求
x
的集合编号?while (p[x] != x) x = p[x];
; - 问题3:如何合并两个集合?设 \(p_x\) 是
x
的集合编号, \(p_y\) 是y
的集合编号,令p[x] = y
。
并查集的优化: 路径压缩 (常用);按秩合并(略)。
不带路径压缩的查找如下:
带路径压缩的查找和合并如下(能够加快以后的查找进程):
为了把小集合的根指向大集合的根,合并时可以采用启发式合并(也叫按秩合并,不常用):
模板与例题⚓︎
朴素并查集⚓︎
例题:合并集合
模板:
维护size的并查集⚓︎
例题:连通块中点的数量
维护到祖宗节点距离的并查集⚓︎
例题:食物链
思路是用“距离”来描述关系、判断关系,所有的距离都以根节点为基准,按照mod
类别数3分为三类。
- “距离”:
x
吃y
表示y
到x
的距离为1,y
是第0
代,吃y
的x
是第1代,吃x
的是第2代,根节点是第0代等 - 三种关系:用点到根节点之间的距离表示其余根节点之间的关系
- 模3余1:可以吃根节点
- 模3余2:可以被根节点吃
- 模3余0:和根节点同类
这样就把集合中所有的点划分为上述三类。
d[i]
理解是第i
个节点到其父节点的距离,find()
函数进行了路径压缩,当查询某个节点i
时,如果i
的父节点不为根节点的话,就会进行递归调用,将i
节点沿途路径上所有节点均指向父节点,此时的d[i]
存放的是i
到父节点,也就是根节点的距离。不路径压缩取得的是到父节点的距离,路径压缩后既是到根的距离,也是到父节点的距离,因为压缩后将父亲指向根了,父亲和根是一个东西。而由于每次取d[x]
都必然经过路径压缩,因此d[x]
在结果上既是到根的距离,也是到父亲的距离。但是在过程中它仅代表到父亲的距离,因为没更新的时候它的父亲并不是根。[总结:d[x]
始终代表到父节点的距离,只不过在find
之后x
的父节点直接变成了根节点(祖宗),所以逻辑上成了到祖宗的距离]