普通的并查集
首先给出一道例题:luoguP1551亲戚
题目描述
规定: 和 是亲戚, 和 是亲戚,那么 和 也是亲戚。如果 是亲戚,那么 的亲戚都是 的亲戚, 的亲戚也都是 的亲戚。
输入格式
第一行:三个整数 , ,分别表示有 个人, 个亲戚关系,询问 对亲戚关系。
以下 行:每行两个数 , , ,表示 和 具有亲戚关系。
接下来 行:每行两个数 , ,询问 和 是否具有亲戚关系。
输出格式
行,每行一个 或 。表示第 个询问的答案为“具有”或“不具有”亲戚关系。
直观做法
对于每一对 ,我们连一条无向边,处理完所有亲戚关系后,我们找出这个图里的所有连通块,并对同一个连通块里的点编上一个相同的号,然后对于每个询问 ,查询他们的编号是否相同,相同则有亲戚关系,反之则无。
不难发现,这个做法的复杂度是 的,那如此看来,我们要并查集做什么呢?
实际应用
题目描述
如题,现在有一个并查集,你需要完成合并和查询操作。
输入格式
第一行包含两个整数 ,表示共有 个元素和 个操作。
接下来M行,每行包含三个整数
当 时,将 与 所在的集合合并
当 时,输出 与 是否在同一集合内,是的话输出 ;否则话输出
输出格式
对于每一个 的操作,都有一行输出,每行包含一个大写字母,为 或者
并查集做法
刚才的连通块做法已不再适用,我们需要对每种情况都找一次连通块,复杂度可能会被卡到
观察到我们其实不需要知道每个点的直系亲戚是谁,我们只需要知道每个点之间的相对亲戚关系。
那么我们可以删除一些不需要的边,把每个连通块简化成一棵树,那么树内的点相互有亲戚关系。
查询时只需要知道两个点是否在同一棵树内,即它们所在树的树根是否为同一个,如果是,那么两个点就相互有亲戚关系。
考虑出题人给了一个数据,将我们的树变成了一条链,最坏复杂度依然是 的。
路径压缩优化
我们在上面提过,我们只需要考虑它们的相对关系,那么我们可以在查询的时候直接让它从下面“跳”上来,成为根节点的儿子,容易证明这是非常正确的。
然后这棵树就变成了一棵只有两层的树,单次查询即修改只需 ,复杂度总体来说为 ,其中 为一个很小的数,可以忽略不计。
总体做法
初始化:
for(int i=1;i<=n;i++)
father[i]=i;
在查找某点所在集合的根节点时,我们在查找时顺便将其父节点改至根节点,代码如下:
int findfa(int x){
if(father[x]!=x) return father[x]=findfa(father[x]);
else return x;
}
对于每一次合并 ,我们先查找它们所在的集合是否为同一个,如果不是,那么就让集合 的根节点的父亲等于集合 的根节点:
void merge(int x,int y){
int fx=findfa(x),fy=findfa(y);
if(fx!=fy) father[fx]=fy;
}
查询时就查找它们所在集合的根节点是否为同一个。
完整代码
#include<iostream>
using namespace std;
int n,m,father[10005],x,y,z;
int findfa(int x){
if(father[x]!=x) return father[x]=findfa(father[x]);
else return x;
}
int main(){
cin>>n>>m;
for(int i=1;i<=m;i++){
cin>>z>>x>>y;
if(z==1){
if(findfa(x)!=findfa(y))
father[findfa(x)]=findfa(y);
}
if(z==2){
if(findfa(x)==findfa(y))
cout<<"Y"<<endl;
else
cout<<"N"<<endl;
}
}
}
小优化
int findfa(int x){
if(father[x]!=x) return father[x]=findfa(father[father[father[father[x]]]]);
else return x;
}
并查集的扩展域
例题
题目描述
动物王国中有三类动物 ,这三类动物的食物链构成了有趣的环形。 吃 , 吃 , 吃 。
现有 个动物,以 编号。每个动物都是 中的一种,但是我们并不知道它到底是哪一种。
有人用两种说法对这 个动物所构成的食物链关系进行描述:
第一种说法是 ,表示 和 是同类。
第二种说法是 ,表示 吃 。
此人对 个动物,用上述两种说法,一句接一句地说出 句话,这 句话有的是真的,有的是假的。当一句话满足下列三条之一时,这句话就是假话,否则就是真话。
• 当前的话与前面的某些真的话冲突,就是假话
• 当前的话中 或 比 大,就是假话
• 当前的话表示 吃 ,就是假话
你的任务是根据给定的 和 句话,输出假话的总数。
输入格式
第一行两个整数, , ,表示有 个动物, 句话。
第二行开始每行一句话(按照题目要求)
输出格式
一行,一个整数,表示假话的总数。
思路
观察到我们要维护的东西不止一种,不好用普通的并查集来维护,所以我们要用到并查集的扩展域。对于一个点 ,我们对其敌人和同类及食物分别维护。
我们判断一句话是否为假时有两种情况:
如果该话为假,则忽略, ,否则就按要求合并 ,合并方法请读者先自己思考,再看代码核实。
#include<bits/stdc++.h>
using namespace std;
int father[1500005],eat_x,Eat_y,Self_x,Self_y,Enemy_x,Enemy_y,n,m,ans=0;
void pre(){
for(int i=1;i<=n*3;i++){
father[i]=i;
}
}
int findfa(int x){
if(father[x]==x) return x;
return father[x]=findfa(father[x]);
}
void Merge(int x,int y){
int fx=findfa(x),fy=findfa(y);
if(fx!=fy) father[fx]=fy;
}
int main(){
scanf("%d%d",&n,&m);
pre();
for(int i=1;i<=m;i++){
int opt,x,y;
scanf("%d%d%d",&opt,&x,&y);
eat_x=(x-1)*3+1,Self_x=(x-1)*3+2,Enemy_x=x*3;
Eat_y=(y-1)*3+1,Self_y=(y-1)*3+2,Enemy_y=y*3;
if(x>n||y>n){
ans++;
continue;
}
if(opt==1){
if(findfa(eat_x)==findfa(Self_y)){
ans++;
continue;
}
if(findfa(Self_x)==findfa(Eat_y)){
ans++;
continue;
}
Merge(Self_x,Self_y);
Merge(eat_x,Eat_y);
Merge(Enemy_x,Enemy_y);
}
if(opt==2){
if(findfa(Self_x)==findfa(Self_y)){
ans++;
continue;
}
if(findfa(Eat_y)==findfa(Self_x)){
ans++;
continue;
}
Merge(eat_x,Self_y);
Merge(Self_x,Enemy_y);
Merge(Enemy_x,Eat_y);
}
}
cout<<ans<<endl;
}