并查集及其扩展域

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接: https://blog.csdn.net/wljoi/article/details/101542362

普通的并查集

首先给出一道例题:luoguP1551亲戚

题目描述

规定: x x y y 是亲戚, y y z z 是亲戚,那么 x x z z 也是亲戚。如果 x , y x,y 是亲戚,那么 x x 的亲戚都是 y y 的亲戚, y y 的亲戚也都是 x x 的亲戚。

输入格式

第一行:三个整数 n , m , p n,m,p n 500000 , m 500000 , p 500000 (n\le500000,m\le500000,p\le500000) ,分别表示有 n n 个人, m m 个亲戚关系,询问 p p 对亲戚关系。

以下 m m 行:每行两个数 M i M_i M j M_j 1 < = M i , M j < = N 1<=M_i,M_j<=N ,表示 M i M_i M j M_j 具有亲戚关系。

接下来 p p 行:每行两个数 P i P_i P j P_j ,询问 P i P_i P j P_j 是否具有亲戚关系。

输出格式

P P 行,每行一个 Y e s Yes N o No 。表示第 i i 个询问的答案为“具有”或“不具有”亲戚关系。

直观做法

对于每一对 ( M i , M j ) (M_i,M_j) ,我们连一条无向边,处理完所有亲戚关系后,我们找出这个图里的所有连通块,并对同一个连通块里的点编上一个相同的号,然后对于每个询问 ( P i , P j ) (P_i,P_j) ,查询他们的编号是否相同,相同则有亲戚关系,反之则无。

不难发现,这个做法的复杂度是 O ( n ) O(n) 的,那如此看来,我们要并查集做什么呢?

实际应用

luoguP3367 [模板]并查集

题目描述

如题,现在有一个并查集,你需要完成合并和查询操作。

输入格式

第一行包含两个整数 N , M N,M ,表示共有 N N 个元素和 M M 个操作。

接下来M行,每行包含三个整数 Z i , X i , Y i Z_i,X_i,Y_i

Z i = 1 Z_i=1 时,将 X i X_i Y i Y_i 所在的集合合并

Z i = 2 Z_i=2 时,输出 X i X_i Y i Y_i 是否在同一集合内,是的话输出 Y Y ;否则话输出 N N

输出格式

对于每一个 Z i = 2 Z_i=2 的操作,都有一行输出,每行包含一个大写字母,为 Y Y 或者 N N

并查集做法

刚才的连通块做法已不再适用,我们需要对每种情况都找一次连通块,复杂度可能会被卡到 O ( n 2 ) O(n^2)

观察到我们其实不需要知道每个点的直系亲戚是谁,我们只需要知道每个点之间的相对亲戚关系。

那么我们可以删除一些不需要的边,把每个连通块简化成一棵树,那么树内的点相互有亲戚关系。

查询时只需要知道两个点是否在同一棵树内,即它们所在树的树根是否为同一个,如果是,那么两个点就相互有亲戚关系。

考虑出题人给了一个数据,将我们的树变成了一条链,最坏复杂度依然是 O ( n 2 ) O(n^2) 的。

路径压缩优化

我们在上面提过,我们只需要考虑它们的相对关系,那么我们可以在查询的时候直接让它从下面“跳”上来,成为根节点的儿子,容易证明这是非常正确的。

然后这棵树就变成了一棵只有两层的树,单次查询即修改只需 O ( 1 ) O(1) ,复杂度总体来说为 O ( α n ) O(\alpha n) ,其中 α \alpha 为一个很小的数,可以忽略不计。

总体做法

初始化:

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;
}

对于每一次合并 ( x , y ) (x,y) ,我们先查找它们所在的集合是否为同一个,如果不是,那么就让集合 x x 的根节点的父亲等于集合 y y 的根节点:

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;
}

并查集的扩展域

例题

luoguP2024 食物链

题目描述

动物王国中有三类动物 A , B , C A,B,C ,这三类动物的食物链构成了有趣的环形。 A A B B B B C C C C A A

现有 N N 个动物,以 1 N 1 - N 编号。每个动物都是 A , B , C A,B,C 中的一种,但是我们并不知道它到底是哪一种。

有人用两种说法对这 N N 个动物所构成的食物链关系进行描述:

第一种说法是 1   X   Y “1\space X\space Y” ,表示 X X Y Y 是同类。

第二种说法是 2   X   Y “2\space X\space Y” ,表示 X X Y Y

此人对 N N 个动物,用上述两种说法,一句接一句地说出 K K 句话,这 K K 句话有的是真的,有的是假的。当一句话满足下列三条之一时,这句话就是假话,否则就是真话。

• 当前的话与前面的某些真的话冲突,就是假话

• 当前的话中 X X Y Y N N 大,就是假话

• 当前的话表示 X X X X ,就是假话

你的任务是根据给定的 N N K K 句话,输出假话的总数。

输入格式

第一行两个整数, N N K K ,表示有 N N 个动物, K K 句话。

第二行开始每行一句话(按照题目要求)

输出格式

一行,一个整数,表示假话的总数。

思路

观察到我们要维护的东西不止一种,不好用普通的并查集来维护,所以我们要用到并查集的扩展域。对于一个点 x x ,我们对其敌人和同类及食物分别维护。

我们判断一句话是否为假时有两种情况:

1. x y 1.x和y为同类
e a t x = s e l f y , x y eat_x=self_y,x吃y
e a t y = s e l f x , y x eat_y=self_x,y吃x
2. x y 2.x吃y
s e l f x = s e l f y , x y self_x=self_y,x和y是同类
e a t y = s e l f x , y x eat_y=self_x,y吃x

如果该话为假,则忽略, a n s + + ans++ ,否则就按要求合并 ( x , y ) (x,y) ,合并方法请读者先自己思考,再看代码核实。

#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;
}

猜你喜欢

转载自blog.csdn.net/wljoi/article/details/101542362