并查集是一种处理不相交集合的查询的数据结构,本质上是维护一片森林,但是和普通的树的存储不同的是,正常的树的存储是存储节点直接的逻辑关系,而并查集只存储一个节点的根节点。它初始化的时间复杂度为 (Theta(n)),查询节点是否存在亲戚关系的时间复杂度 (Theta(log_{}n)),远远优于其他数据结构。
并查集的原理
举一个简单的例子,假如有一些元素,构成一些集合,我们就把这个集合看成这样一片森林(每颗树代表一个集合):
我们经过观察发现,如果两个节点的根节点相同,那么这两个节点就一定在同一颗树(集合)里,如节点 4
和节点 7
的根节点都是节点 3
,那么它们就在同一颗树里;如果两个节点的根节点不同,那么它们一定不在同一颗树(集合)里,如节点 5
的根节点为 3
,节点 6
的根节点为 1
,那么它们就不在一颗树里。
那么我们就可以用一个数组 (f) 来记录根节点情况,(f_i) 表示节点 (i) 的根节点,当 (f_i=f_j) 时,我们就可以判断 (i) 和 (j) 在同一颗树里。
查找根节点
我们在一开始将整个 (f) 数组赋初值,(f_i=i),这时候代表每个节点都是一个集合,在后面并查集的合并操作中逐渐改变。这时候,我们就可以用递归来寻找根节点。因为 (f_i=i) 时,就代表这个节点暂时未根节点,所以 (f_i=i) 就可以作为递归边界,每次递归不断往上走,直到找到祖先。
那么代码就如下:
int f[10005]; //记录根节点。
inline int find(int x) //找 x 的根节点。
{
if(f[x]=x) //如果当前的节点暂时为根节点,就返回编号。
return x;
return find(f[x]); //否则继续找。
}
但是在实际运用的时候,这种方法很容易超时,那么是为什么呢?请看这样一棵树:
在这种情况下,树退化成了一条链。时间复杂度也从理想的 (Theta(log_{}n)) 退化成 (Theta(n))。如果出题人特意构造这样的数据,这种不带优化的就很容易被卡。
那么该如何解决呢?就要用到一种神奇的方法:路径压缩
路径压缩
路径压缩本质上是将一颗树重构。和一般的重构不同的是,路径压缩可能会破坏父节点和子节点之间的拓扑关系,只保留节点和根节点的关系,即路径压缩后就没办法确定原来的树(不可逆过程)。但是并查集也只需要知道根节点。
在路径压缩后,上面的那张图中的树就变为:
这样一来,就解决接近链状态下时间复杂度退化严重的问题了。
怎么用代码实现呢?就可以在递归的语句中动点手脚,就能轻松解决。在递归的过程中将递归的结果同时赋值给 (f_i):
int f[10005]; //记录根节点。
inline int find(int x) //找 x 的根节点。
{
if(f[x]=x) //如果当前的节点暂时为根节点,就返回编号。
return x;
return f[x]=find(f[x]); //路径压缩,防止复杂度退化。
}
合并
并查集并查集,顾名思义,是要合并的。那么怎么合并?是将一个节点变成另一个节点的儿子吗?不不不,并查集是不能保存父子关系的。那么我们就可以将一个节点的根节点的根节点赋值为另一个节点的根节点。
是不是很绕口?就是当我们要把 (x) 和 (y) 合并时,就将 (x) 的根节点作为 (y) 的根节点的子孙节点,没办法知道隔几代那就只知道它是老祖宗就行了。
代码(就一行):
f[find(x)]=find(y);
查询
查找 (x) 和 (y) 是否为亲戚,按照我们上面的思路,只要确定它们的老祖宗是不是同一个。代码怎么写?可能大多数人首先想到的是:
if(f[x]==f[y])
但是我们经过实际操作,我们发现这样是不行的,为什么呢?
我们来看一组数据:
1 2
3 1
3 4
我们不妨模拟一下,在一开始,1
作为 2
的根节点,然后 3
又成为 1
的根节点,但是此时 2
的根节点没有更新,还是 1
。所以在询问 2
和 3
的关系时,用上面的方法就会出错。总的来说,如果两个元素隔了“两代”,且作为子节点的元素后来没有再次出现,就会出错。所以我们就要采取另一种方法:
if(find(x)==find(y))
这样就会一直查询到根节点。
代码:
#include<bits/stdc++.h>
using namespace std;
int n,m,f[10010],x,y,z;
int find(int k)
{
if(f[k]==k)
return k;
return f[k]=find(f[k]);
}
int main()
{
cin>>n>>m;
for(register int i=1;i<=n;++i)
f[i]=i;
for(register int i=1;i<=m;++i)
{
cin>>x>>y>>z;
if(x==1)
f[find(y)]=find(z);
else
if(find(y)==find(z))
cout<<"Y"<<endl;
else
cout<<"N"<<endl;
}
return 0;
}
拓展:并查集判断节点的联通性
给出一个无向图的邻接矩阵,并且有 (m) 次询问,对于第 (i) 次询问,(x_i) 与 (y_i) 是否相连。
代码懒得写了(以后补上)
总结
并查集的运用很广泛,尤其在图论中,有很多问题都需要并查集来辅助解决,是很多算法所需的前置知识,所以很重要编不下去了