并查集是一种树型的数据结构,用于处理一些不相交集合的合并及查询问题(即所谓的并、查)。比如说,我们可以用并查集来判断一个森林中有几棵树、某个节点是否属于某棵树等。
并查集两个操作“并”和“查”:
并:
将两个集合合并查:
询问两个数是否在一个集合中
(1)初始化。定义数组int s[]是以结点i为元素的并查集,开始的时候,还没有处理点与点之间的朋友关系,所以每个点属于独立的集,并且以元素i的值表示它的集s[i],例如元素1的集s[1]=1。
下面是图解,左边给出了元素与集合的值,右边画出了逻辑关系。为了便于讲解,左边区分了结点i和集s:把集的编号加上了下划线;右边用圆圈表示集,方块表示元素。
图1 并查集的初始化
(2)合并,例如加入第一个朋友关系(1, 2)。在并查集s中,把结点1合并到结点2,也就是把结点1的集1改成结点2的集2。
图2 合并(1, 2)
(3)合并,加入第二个朋友关系(1, 3)。查找结点1的集,是2,再递归查找元素2的集是2,然后把元素2的集2合并到结点3的集3。此时,结点1、2、3都属于一个集。右图中,为简化图示,把元素2和集2画在了一起。
图3 合并(1, 3)
(4)合并,加入第三个朋友关系(2, 4)。结果如下,请读者自己分析。
图4 合并(2, 4)
(5)查找。上面步骤中已经有查找操作。查找元素的集,是一个递归的过程,直到元素的值和它的集相等,就找到了根结点的集。从上面的图中可以看到,这棵搜索树的高度,可能很大,复杂度是O(n)的,变成了一个链表,出现了树的“退化”现象。
(6)统计有多少个集。如果s[i] = i,这是一个根结点,是它所在的集的代表;统计根结点的数量,就是集的数量。
具体代码
find()函数
int find(int x){ //查找
return x==s[x]? x:find(s[x]);
}
路径压缩算法:优化find( )函数
这里并没有明确谁是谁的前驱的规则,而是我直接指定后面的数据作为前面数据的前驱。那么这样就导致了最终的树状结构无法预计,即有可能是良好的 n 叉树,也有可能是单支树结构(一字长蛇形)。试想,如果最后真的形成单支树结构,那么它的效率就会及其低下(树的深度过深,那么查询过程就必然耗时)。
而我们最理想的情况就是所有人的直接上级都是教主,这样一来整个树的结构就只有两级,此时查询教主只需要一次。因此,这就产生了路径压缩算法。
实现:
从上面的查询过程中不难看出,当从某个元素出发去寻找它的代表元时,我们会途径一系列的元素,在这些节点中,除了代表元外,其余所有节点都需要更改直接前驱为曹操。
因此,基于这样的思路,我们可以通过递归的方法来逐层修改返回时的某个节点的直接前驱(即pre[x]的值)。简单说来就是将x到根节点路径上的所有点的pre(上级)都设为根节点。下面给出具体的实现代码:
int find(int x) //查找结点 x的根结点
{
if(s[x] == x) return x; //递归出口:x的上级为 x本身,即 x为根结点
return s[x] = find(s[x]); //此代码相当于先找到根结点 rootx,然后s[x]=rootx
}
join函数
join函数的作用是将两个集合合并,合并两个集合的关键是找到两个集合的代表元,将其中一个作为另一个的前驱结点即可。
void join(int x,int y)
{
int fx=find(x), fy=find(y);
if(fx != fy)
s[fx]=fy; //fy做fx的前驱结点
}
附上一道模板题
P3367 【模板】并查集
链接:https://www.luogu.com.cn/problem/P3367
题目描述
如题,现在有一个并查集,你需要完成合并和查询操作。
输入格式
第一行包含两个整数 N,MN,M ,表示共有 NN 个元素和 MM 个操作。
接下来 MM 行,每行包含三个整数 Z_i,X_i,Y_iZi,Xi,Yi 。
当 Z_i=1Zi=1 时,将 X_iXi 与 Y_iYi 所在的集合合并。
当 Z_i=2Zi=2 时,输出 X_iXi 与 Y_iYi 是否在同一集合内,是的输出 Y
;否则输出 N
。
输出格式
对于每一个 Z_i=2Zi=2 的操作,都有一行输出,每行包含一个大写字母,为 Y
或者 N
。
输入输出样例
输入 #1
4 7 2 1 2 1 1 2 2 1 2 1 3 4 2 1 4 1 2 3 2 1 4
输出 #1
N Y N Y
说明/提示
对于 30\%30% 的数据,N \le 10N≤10,M \le 20M≤20。
对于 70\%70% 的数据,N \le 100N≤100,M \le 10^3M≤103。
对于 100\%100% 的数据,1\le N \le 10^41≤N≤104,1\le M \le 2\times 10^51≤M≤2×105,1 \le X_i, Y_i \le N1≤Xi,Yi≤N,Z_i \in \{ 1, 2 \}Zi∈{1,2}。
AC代码
#include <bits/stdc++.h>
using namespace std;
int n,m,z,x,y,s[10005];//s[i]是第i号节点的祖先
int find(int x) {
if (x==s[x]) return x;
return s[x]=find(s[x]);
//路径压缩,可以缩短时间
}
void join(int x,int y) {
int fx=find(x), fy=find(y);
if(fx != fy)
s[fx]=fy; //fy做fx的前驱结点
}
int main() {
cin>>n>>m;
for(int i=1; i<=n; ++i) {
s[i]=i;
//并查集的初始化
}
while(m--) {
cin>>z>>x>>y;
if(z==1) {
join(x,y);
} else {
if(find(x)==find(y))
printf("Y\n");
else
printf("N\n");
}
}
return 0;
}