当前位置:网站首页>并查集

并查集

2020-12-06 09:52:56 osc_08vvd847

本人小蒻蒟,做了一些并查集的题目,理解不深,但是还是想写一下关于并查集,可以用来以后回看(嘻嘻~)
带权我不会,请翻书,或者看大佬博客

并查集

据我个人理解呢,就是合并和查找集合
合并很简单,就是把两个集合合并在起,如图中将x,y集合合并(图烂见谅)合并集合
我们可以把两个集合分别看为一个家族,合并就是将y作为x的爸爸,也就是x下的所有孩子的祖宗变为为y。

而查找一般就是查每一个元素的根节点(也就是祖宗,我习惯称为找祖宗hhh),我们需要一步一步往上寻找直到找到祖宗为止。查找
首先呢,我们需要对每个元素的爸爸初始化,让每个元素独立为营

int fa[N];
for(int i = 0;i < n;i ++) fa[i] = i;

然后我们写找祖宗的函数

int find(int x){
   
   
	if(x != fa[x]) fa[x] = find(fa[x]);
	return fa[x];
}

在这个代码模板中,fa[N]这个数组存的是x的父辈,如果x的祖宗不是x我们就进行递归处理,找x的爸爸的祖宗,一直递归~直到我们没有办法继续往上查找时,我们也就找到了x的祖宗!!!

这个find函数我认为需要重点理解一下,因为递归其实还蛮难的。可能是因为我菜
认真思考后会发现,我们的find函数还有一个神奇的功能,那就是路径压缩,写出这个代码的大佬很牛,佩服哇!
那么路径压缩是什么意思呢?看图(好丑)路径压缩
到这里呢,就是我目前了解的并查集,还有一些祖宗拖家带口合并的(很2的题),那样就把小的家族并到大的家族


并查集例题

先简简单单入门题

题目

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

输入

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

以下m行:每行两个数Mi,Mj,1<=Mi,Mj<=N,表示Mi和Mj具有亲戚关系。

接下来p行:每行两个数Pi,Pj,询问Pi和Pj是否具有亲戚关系。

输出

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

输入例子
6 5 3
1 2
1 5
3 4
5 2
1 3
1 4
2 3
5 6
输出
Yes
Yes
No












读完题,我们也可以看出来,这完全就是一个并查集的题目
比较简单,我就直接放代码了

#include <iostream>
#include <cstdio>

using namespace std;

int n,m,p;
int fa[5010];
//找祖宗函数
int find(int x){
   
   
	if(x != fa[x]) fa[x] = find(fa[x]);
	return fa[x]; 
}

int main(){
   
   
	scanf("%d %d %d",&n,&m,&p);//n个人m个亲戚关系  询问p对亲戚关系 
	for(int i = 0;i < n;i ++) fa[i] = i;//他爹初始化 
	
	while(m--){
   
   
		int a,b;
		scanf("%d %d",&a,&b);
		if(find(a) != find(b)) fa[find(a)] = find(b);//亲戚合并 
	}
	while(p--){
   
   //这个循环就是判断是否为亲戚
		int i,j;
		scanf("%d %d",&i,&j);
		if(find(i) == find(j)) printf("Yes\n");
		else printf("No\n");
	}
	return 0;
}

是不是很简单呢?我们再来一个模板题加深印象

题目
一共有n个数,编号是1~n,最开始每个数各自在一个集合中。

现在要进行m个操作,操作共有两种:

1、“M a b”,将编号为a和b的两个数所在的集合合并,如果两个数已经在同一个集合中,则忽略这个操作;
2、“Q a b”,询问编号为a和b的两个数是否在同一个集合中;

输入

第一行输入整数n和m。

接下来m行,每行包含一个操作指令,指令为“M a b”或“Q a b”中的一种。

输出

对于每个询问指令”Q a b”,都要输出一个结果,如果a和b在同一集合内,则输出“Yes”,否则输出“No”。

每个结果占一行。

输入样例
4 5
M 1 2
M 3 4
Q 1 2
Q 1 3
Q 3 4
输出
Yes
No
Yes









嗯!直接放代码,代码块上会有相关解释

#include <iostream>

using namespace std;

const int N = 1e6 + 10;

int n,m;
int p[N];

int find(int x){
   
   //找祖宗函数
    if(p[x] != x)   p[x] = find(p[x]);
    return p[x];
}

int main(){
   
   
    scanf("%d%d",&n,&m);
    for(int i = 1;i <= n;i ++)  p[i] = i;//他爹初始化 嘻嘻~
    while(m--){
   
   
        char op[2];
        int a,b;
        scanf("%s%d%d",op,&a,&b);
        //根据题意判断是合并集合还是查找
        if(op[0] == 'M')    p[find(a)] = find(b);
        else{
   
   
            if(find(a) == find(b))  printf("Yes\n");
            else    printf("No\n");
        }
    }
    return 0;
}

这道题就是简单的模板题了,我觉得对理解并查集这个东西很有用,可能是难的题我看不懂

下边看两道有点思考程度的简单并查集,可能对于大佬来说一看就会~

The Suspects

题目

在一所大学里有n个学生(这些学生的编号为0∼n−1)。这些学生由于兴趣爱好等原因组成了m个群体。

由于非典(SARS)流行,该大学的学生会需要排除可能的非典患者。

由于非典传染性强,学生会的成员假定:如果一个群体中有一个人是非典患者,那么这个群体中的所有人都是非典患者。

现在已知编号为0的学生为非典患者。请你找出这些学生中非典患者的人数。

输入

输入数据由多组数据组成。

每组数据包括m+1行:

第1行有两个由空格隔开的非负整数n和m,其意义如题目所述。

第2∼m+1行表示每个群体的人员信息,每行的第一个数字k表示该群体的人数,其后有k个用空格隔开的非负整数,表示这个群体的各个成员的编号。

当n=m=0时,表示输入结束,不需要处理之后的数据。

输出

对于每组输入数据,输出一个整数,表示这组数据中非典患者的数量。每组数据的输出以换行符结尾。

输入样例
100 4
2 1 2
5 10 13 11 12 14
2 0 1
2 99 2
200 2
1 5
5 1 2 3 4 5
1 0
0 0
输出
4
1
1













这道题呢,大致意思可以理解为,有很多集合,集合里有的有相同元素,我们需要合并一些集合,并把含有0的集合里的元素个数输出出来。

简单的合并和查找处理

int p,x;
scanf("%d",&p);//学生人数 
scanf("%d",&x);//第一个学生编号 
j = find(x);//用j存第一个学生编号的祖宗~ 
for(int i = 1;i < p;i ++){
   
   
	scanf("%d",&x);
	q[find(x)] = j;//让每一个学生的祖宗的父辈都是第一个学生的祖宗~ 
}

这个里面,让每一个学生的祖宗的父辈都是第一个学生的祖宗就是,合并操作,可以再纸上演算一下这个过程,就可以加深理解

int p = find(0);//用p存编号为0的学生的祖宗 
	for(int i = 0;i < n;i ++){
   
   
		if(find(i) == find(0)) cnt++;//如果编号为i的学生和编号为0是同一个祖宗cnt就加一 
	}
	printf("%d\n",cnt);
	cnt = 0;
}

这个就是查找的时候的灵活应用

代码

#include <iostream>

using namespace std;

const int N = 30010;

int n,m,j,cnt;
int q[N];

int find(int x){
   
   
	if(q[x] != x)	q[x] = find(q[x]);
	return q[x];
}

int main(){
   
   
	while(1){
   
   
		scanf("%d %d",&n,&m);
		if(n == 0 && m == 0)	break; 
		for(int i = 0;i < n;i ++) q[i] = i;//初始化 
		while(m--){
   
   
			int p,x;
			scanf("%d",&p);//学生人数 
			scanf("%d",&x);//第一个学生编号 
			j = find(x);//用j存第一个学生编号的祖宗~ 
			for(int i = 1;i < p;i ++){
   
   
				scanf("%d",&x);
				q[find(x)] = j;//让每一个学生的祖宗的父辈都是第一个学生的祖宗~ 
			}
		}
		int p = find(0);//用p存编号为0的学生的祖宗 
		for(int i = 0;i < n;i ++){
   
   
			if(find(i) == find(0)) cnt++;//如果编号为i的学生和编号为0是同一个祖宗cnt就加一 
		}
		printf("%d\n",cnt);
		cnt = 0;
	}
	return 0;
}

下面是一道全英文题目

Wireless Network

题目

An earthquake takes place in Southeast Asia. The ACM (Asia Cooperated Medical team) have set up a wireless network with the lap computers, but an unexpected aftershock attacked, all computers in the network were all broken. The computers are repaired one by one, and the network gradually began to work again. Because of the hardware restricts, each computer can only directly communicate with the computers that are not farther than d meters from it. But every computer can be regarded as the intermediary of the communication between two other computers, that is to say computer A and computer B can communicate if computer A and computer B can communicate directly or there is a computer C that can communicate with both A and B.

In the process of repairing the network, workers can take two kinds of operations at every moment, repairing a computer, or testing if two computers can communicate. Your job is to answer all the testing operations.

输入

The first line contains two integers N and d (1 <= N <= 1001, 0 <= d <= 20000). Here N is the number of computers, which are numbered from 1 to N, and D is the maximum distance two computers can communicate directly. In the next N lines, each contains two integers xi, yi (0 <= xi, yi <= 10000), which is the coordinate of N computers. From the (N+1)-th line to the end of input, there are operations, which are carried out one by one. Each line contains an operation in one of following two formats:

  1. “O p” (1 <= p <= N), which means repairing computer p.
  2. “S p q” (1 <= p, q <= N), which means testing whether computer p and q can communicate.

The input will not exceed 300000 lines.

输出

For each Testing operation, print “SUCCESS” if the two computers can communicate, or “FAIL” if not.

输入样例
4 1
0 1
0 2
0 3
0 4
O 1
O 2
O 4
S 1 4
O 3
S 1 4
输出样例
FAIL
SUCCESS













题意:首先输入n个电脑的位置和每台电脑的信号覆盖半径d,刚开始电脑都是坏的,你需要去维修。
O表示修复一台电脑
S检查是否可以联通,通过另一台电脑间接联通也可以
只有修理好的才可以进行通信,如果两台计算机的距离不超过d,则两台电脑之间可以通信。


这个题就需要我们很灵活的处理,只有修复并且可以通信的电脑我们才将他们合并

int q[M];//存父辈
double x[M],y[M];//计算机坐标 
int z[M];//修复好的电脑
int c[M][M];//记录距离小于等于d的两台电脑

在这道题中 c[M][M] 数组和 z[M] 的加入就是我们对这道题的灵活处理

for(int i = 1;i <= N;i ++)//算出两个计算机的距离并判断是否小于d,若小于,记录两个计算机。 
		for(int j = i;j <= N;j ++){
   
   
			if((x[i] - x[j]) * (x[i] - x[j]) + (y[i] - y[j]) * (y[i] - y[j]) <= d * d){
   
   
				c[i][j] = c[j][i] = 1;//记录 可以联通的计算机 
			}
		}

我们先预处理可以联通的电脑,并把它存下来

scanf("%d",&a); 
z[a] = 1;

当输入O的时候,我们把维修的电脑激活,用z[M]记录下来,以便于后边的判断。

if(i != a && z[a] == 1 && z[i] == 1&& c[i][a] == 1){
   
   //判断计算机是否被修过,然后和可以连通的计算机合并 
	if(find(a) != find(i))	q[find(a)] = find(i); //合并为一个集合 
}

判断,这个电脑和另一个电脑可以联通,并且都被维修过,我们就将两个电脑合并

整体代码如下

#include <iostream>

using namespace std;

const int M = 1010;

int N,d;
int q[M];//存他爹 
double x[M],y[M];//计算机坐标 
int z[M];//修复好的电脑
int c[M][M];//记录距离小于等于d的两台电脑

int find(int x){
   
   
	if(q[x] != x)	q[x] = find(q[x]);
	return q[x];
}

int main(){
   
   
	int a[M];
	char op[2];
	scanf("%d %d",&N,&d);
	for(int i = 1;i <= N;i ++) q[i] = i;
	for(int i = 1;i <= N;i ++) scanf("%lf %lf",&x[i],&y[i]);
	for(int i = 1;i <= N;i ++)//算出两个计算机的距离并判断是否小于d,若小于,记录两个计算机。 
		for(int j = i;j <= N;j ++){
   
   
			if((x[i] - x[j]) * (x[i] - x[j]) + (y[i] - y[j]) * (y[i] - y[j]) <= d * d){
   
   
				c[i][j] = c[j][i] = 1;//记录 可以联通的计算机 
			}
		}
	getchar();//清除空格
	while(~scanf("%s",op)){
   
   
		int a,b;
		if(op[0] == 'O'){
   
   
			scanf("%d",&a); 
			z[a] = 1;
			for(int i = 1;i <= N;i ++){
   
   
				if(i != a && z[a] == 1 && z[i] == 1&& c[i][a] == 1){
   
   //判断计算机是否被修过,然后和可以连通的计算机合并 
					if(find(a) != find(i))	q[find(a)] = find(i); //合并为一个集合 
				}
			} 
		}else{
   
   
			scanf("%d %d",&a,&b);
			if(find(a) == find(b))	printf("SUCCESS\n");
			else printf("FAIL\n");
		}
		getchar();
	}
	return 0;

这个题给我的启发就是,关于并查集的一些灵活运用,当然也是在熟悉模板之后的。

并查集目前我就理解了这么点点,我才是个刚接触编程不久的小蒟蒻,嘻嘻~
带权并查集太难了以后有机会一定写(可能明年叭)

版权声明
本文为[osc_08vvd847]所创,转载请带上原文链接,感谢
https://my.oschina.net/u/4367530/blog/4776643