1.常规并查集
并查集是一种支持独立数据间合并与查询是否同组的数据结构。
它的思路很简单:把每一组都看作以某点为首的一棵树,记录每个点的父亲节点,那么就可以不断向上找到根节点,也就代表了这个点的组别。
为了突出标记根节点,一般让根节点与自己 自环。
合并 就是将其中一棵树整体作为子树接在另一个点下(无所谓是哪个点,但为了减低复杂度,会选择根节点)。
查询 就是判断两个点是否同属于一组(即有相同的根)。
int f[];
void init(int n) {for(int i=1;i<=n;i++) f[i]=i;}
int find(int x) {return (x==f[x]?x:find(f[x]));}
void merge(int x,int y) {f[find(x)]=find(y);}
2.优化
能看出常规的并查集的复杂度是不稳定的,因为 f i n d find find 函数可能会退化成 O ( n ) O(n) O(n) 。
所以有两种简单的方法:
1.路径压缩
常规的并查集存储的是每个点的父亲,然而我们要这个 f [ ] f[] f[] 数组其实只是为了求根,那何必要存父亲?任意一个祖先都可以保证正确性,所以我们采用记忆化的方式来压缩路径。
很简单,就改一句话 return (x==f[x]?x:find(f[x]));
→
\rightarrow
→ return (x==f[x]?x:f[x]=find(f[x]));
2.按高度或子树合并
参考平衡树,尽量让集合的深度低一点,所以可以把浅的数放到深的树下,按子树大小同理。
虽然也很好用,但要多开几个数组,代码要改的地方比较多,平常用路径压缩就足够了。
3.种类并查集
以 P1892 [BOI2003]团伙 为例。
发现如果只有朋友就是裸的并查集,但有敌人这个东西。理解一下题意,发现一个人的所有敌人都是朋友,所以想到给他们也开一个并查集,进而想到先给每个人都分配一个不存在的敌人,设其为 i + n i+n i+n ,那么每个敌对关系就转化为了朋友关系。
#include<bits/stdc++.h>
using namespace std;
int n,m,p,q,ans;
char op;
int f[20005];
void init(int n) {for(int i=1;i<=n;i++) f[i]=i;}
int find(int x) {return (x==f[x]?x:f[x]=find(f[x]));}
void merge(int x,int y) {f[find(x)]=find(y);}
int main(){
cin>>n>>m;
init(2*n);
for(int i=1;i<=m;i++){
cin>>op>>p>>q;
if(op=='F')
merge(p,q);
else
merge(p+n,q),merge(q+n,p);
}
for(int i=1;i<=n;i++)
if(i==find(i))
ans++;
cout<<ans<<endl;
return 0;
}
这是 2 2 2 个种类的并查集,P2024 [NOI2001] 食物链 是 3 3 3 个。
4.带权并查集
顾名思义,就是每个并查集再维护个什么东西,比较常见的有维护到根的距离,合并时注意点顺序就好了。
这一类因题而异,有点像 d p dp dp 。