算法与数据结构----并查集

并 查 集

The Suspects—POJ - 1611

Description

严重急性呼吸系统综合症(SARS)是一种病因不明的非典型肺炎,在2003年3月中旬被确认为全球威胁。为了尽量减少传染给他人,最好的策略是把嫌疑人和其他人分开。
在不扩散的你的疾病大学(NSYSU),有许多学生团体.同一组中的学生经常相互交流,一个学生可以加入几个小组。为了防止SARS的可能传播,NSYSU收集所有学生组的成员名单,并在其标准操作过程(SOP)中制定以下规则。
一旦一个组中的一个成员是嫌疑犯,这个组中的所有成员都是嫌疑犯。
然而,他们发现,当学生被认定为嫌疑人时,很难识别出所有的嫌疑人。你的工作是写一个程序找出所有嫌疑人。

Input

输入文件包含几种情况。每个测试用例从一行中的两个整数n和m开始,其中n是学生的数目,m是组的数目。您可以假设0<n<=30000和0<=m<=500。每个学生都由0到n−1之间的唯一整数编号,在所有情况下,最初都将学生0识别为嫌疑犯。这一行后面跟着组的m个成员列表,每组一行。每一行的开头都是一个整数k,表示组中的成员数。在成员数之后,有k个整数代表这个组中的学生。一行中的所有整数至少用一个空格分隔。

n=0和m=0的情况表示输入的结束,不需要处理。

Output

在每一个案件中,输出一行嫌疑犯的数量。

Sample Input

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

Sample Output

4

1

1

题意:

同学之间有一种传染病,在一个小组里如果有一个病人,那么全部人都有病。现在0号有病,问一共多少人有病。

思路:

经典的并查集。注意在读入小组成员的时候,5 1 2 3 4 5, 表示5个人1 2 3 4 5是在一个组里,建关系的时候,直接存一下first=1,然后2 3 4 5 分别和first建立关系就好了。合并同一组的元素,遍历0所在的集合的人数

代码:

#include <iostream>
#include<cstdio>
using namespace std;

const int MAX=3e4+5;
int f[MAX],a,b;
int fd(int x) //查找根 
{
   if(f[x] == x) return x;
   else return f[x] = fd(f[x]); 
}
void hb(int x,int y){ //合并 
	a=fd(x);
	b=fd(y);
	if(a!=b) f[a]=b;
}
int main()
{
	int n,m;
	while((scanf("%d%d",&n,&m)),n)
	{
		int num=1;
		for(int i=0;i<n;i++){ //赋值 
			f[i]=i;
		}
        while(m--)
        {
			int ans,x,y;
			cin>>ans;
			ans--;//要先存第一个数字 
			cin>>x;//5 1 2 3 4 5, 表示5个人1 2 3 4 5是在一个组里,建关系的时候,直接存一下first=1,然后2 3 4 5 分别和first建立关系就好了。
			while(ans--)
			{
				cin>>y;
				hb(x,y);//合并除了第一个以外剩下的 
			}
        }
	
		for(int i=1;i<n;i++){
			if(fd(i)==fd(0)){//和0有公共根 
				num++;
			}
		}
		cout<<num<<endl;
	}
	return 0;
}

#【模板】并查集

题目描述

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

输入格式

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

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

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

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

输出格式

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

样例 #1

样例输入 #1

4 7
2 1 2
1 1 2
2 1 2
1 3 4
2 1 4
1 2 3
2 1 4

样例输出 #1

N
Y
N
Y

提示

对于 30 % 30\% 30% 的数据, N ≤ 10 N \le 10 N10 M ≤ 20 M \le 20 M20

对于 70 % 70\% 70% 的数据, N ≤ 100 N \le 100 N100 M ≤ 1 0 3 M \le 10^3 M103

对于 100 % 100\% 100% 的数据, 1 ≤ N ≤ 1 0 4 1\le N \le 10^4 1N104 1 ≤ M ≤ 2 × 1 0 5 1\le M \le 2\times 10^5 1M2×105 1 ≤ X i , Y i ≤ N 1 \le X_i, Y_i \le N 1Xi,YiN Z i ∈ { 1 , 2 } Z_i \in \{ 1, 2 \} Zi{1,2}

分析

并查集通过一个一维数组来实现,本质上是维护一个森林。刚开始的时候,森林里的每一个结点都是一个集合(也就是只有一个结点的树),之后根据题意,逐渐将一个个集合合并(也就是合并成一棵大树)。之后寻找时不断查找父节点,当查找到父结点为本身的结点时,这个结点就是祖宗结点。合并则是寻找这两个结点的祖宗结点,如果这两个结点不相同,则将其中右边的集合作为左边集合的子集(即靠左,靠右也是同一原理)。

代码

#include<bits/stdc++.h>

using namespace std;
const int N = 2e5 + 5;
int n,m,z,x,y;
int f[N];
int fd(int x){
	//查找x家的祖先,也就是二叉树的祖先节点 
	if(f[x] == x) return x;//x是祖先 
	else return f[x] = fd(f[x]);  //x上面还有爸爸,目标是祖先节点,所以我们要问他爸爸的爸爸是谁,即再用一遍fd(find)函数(其实就是一个递归的过程) 
}

void hb(int x,int y){
	f[fd(y)] = fd(x);//合并x,y子集,直接把x子集的祖先节点与y子集的祖先节点连接起来,即把x的最大祖先变成y子集最大祖先的爸爸 
	
}
int main()
{
	cin>>n>>m;//输入数据
	for(int i=1;i<=n;i++){//初始化 赋值
		f[i] =i;
	}
	for(int i=1;i<=m;i++){
		cin>>z>>x>>y;
		if(z == 1){
			hb(x,y);//合并亲戚
		}
		else {
			if(fd(x)==fd(y)) cout<<"Y"<<endl;
			else cout<<"N"<<endl;
		}
	}

	return 0;
}

# 亲戚

题目背景

若某个家族人员过于庞大,要判断两个是否是亲戚,确实还很不容易,现在给出某个亲戚关系图,求任意给出的两个人是否具有亲戚关系。

题目描述

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

输入格式

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

以下 m m m 行:每行两个数 M i M_i Mi M j M_j Mj 1 ≤ M i ,   M j ≤ N 1 \le M_i,~M_j\le N 1Mi, MjN,表示 M i M_i Mi M j M_j Mj 具有亲戚关系。

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

输出格式

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

样例 #1

样例输入 #1

6 5 3
1 2
1 5
3 4
5 2
1 3
1 4
2 3
5 6

样例输出 #1

Yes
Yes
No

分析

1.初始化每个人的祖先就是自己
2.输入两个人,然后将两个人的祖先合并
3.输入,判断两个人的祖先是否相同(查找两个人的祖先是否相同),输出判断的结果

代码

#include<bits/stdc++.h>

using namespace std;

const int N = 1e4+5;
int n,m,q,f[N],c,d,a,b;

int fd(int x){ //查找x家的祖先,也就是二叉树的祖先节点 
	if(f[x] == x) return x;//x是祖先 
	else return f[x] = fd(f[x]);  //x上面还有爸爸,目标是祖先节点,所以我们要问他爸爸的爸爸是谁,即再用一遍fd(find)函数(其实就是一个递归的过程) 
}

void hb(int x,int y){
	
	f[fd(y)] = fd(x); //合并x,y子集,直接把x子集的祖先节点与y子集的祖先节点连接起来,即把x的最大祖先变成y子集最大祖先的爸爸 
	
}
int main()
{
	cin>>n>>m>>q;//输入数据 
	for(int i=1;i<=n;i++){//初始化 赋值 
		f[i] = i;
	}
	for(int i=1;i<=m;i++){//合并亲戚 
		cin>>c>>d;
		hb(c,d);
	}
	for(int i=1;i<=q;i++){ //查找亲戚关系 
		cin>>a>>b;
		if(fd(a) == fd(b)){ //a,b有共同祖先 
			cout<<"Yes"<<endl;
			
		}
		else cout<<"No"<<endl;
	}
	
	return 0;
} 

# 修复公路

题目背景

A 地区在地震过后,连接所有村庄的公路都造成了损坏而无法通车。政府派人修复这些公路。

题目描述

给出 A 地区的村庄数 N N N,和公路数 M M M,公路是双向的。并告诉你每条公路的连着哪两个村庄,并告诉你什么时候能修完这条公路。问最早什么时候任意两个村庄能够通车,即最早什么时候任意两条村庄都存在至少一条修复完成的道路(可以由多条公路连成一条道路)。

输入格式

1 1 1 行两个正整数 N , M N,M N,M

下面 M M M 行,每行 3 3 3 个正整数 x , y , t x,y,t x,y,t,告诉你这条公路连着 x , y x,y x,y 两个村庄,在时间t时能修复完成这条公路。

输出格式

如果全部公路修复完毕仍然存在两个村庄无法通车,则输出 − 1 -1 1,否则输出最早什么时候任意两个村庄能够通车。

样例 #1

样例输入 #1

4 4
1 2 6
1 3 4
1 4 5
4 2 3

样例输出 #1

5

提示

1 ≤ x , y ≤ N ≤ 1 0 3 1\leq x, y\leq N \le 10 ^ 3 1x,yN103 1 ≤ M , t ≤ 1 0 5 1\leq M, t \le 10 ^ 5 1M,t105

分析

并查集是对树的一种操作,旨在找到某个节点的公共祖先(最老公共祖先)。我们先讲一下并。

###并 并就是讲两个节点合并到一个集合里面(这个集合必须是树),每个节点对应一个祖先,最老公共祖先的祖先就是自己,而每个节点在合并前的初始值也是自己,也就是:若有一个节点a,设它的祖先为s[a],那么它的初始值就是s[a]=a。这个合并操作并不难,只要判断一下这两个节点是否有祖先,没有就很好办,直接随便连,比如:a和b,他们都没有祖先(也就是祖先是自己),那么就可以s[a]=b了,如果s[a] != a,那么就让b的最老祖先(可能是自己)再往上多一个祖先s[a],此时b的最老祖先也就是a的最老祖先了(不一定是a)。

代码

#include<bits/stdc++.h>

using namespace std;

int fa[989898];
struct node{
    int x,y,t;//起点,终点,时间 
}p[989898];

bool cmp(node a,node b){ //按时间从小到大排列 
    return a.t<b.t;
}
int find(int x){
    if(fa[x] == x) return x; //x是终点 
	else return fa[x] = find(fa[x]);  //标记,继续寻找 
}
int n,m,num,ans;
int main()
{
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;i++) fa[i]=i; //赋值 
	for(int i=1;i<=m;i++)
		scanf("%d%d%d",&p[i].x,&p[i].y,&p[i].t);//输入 
	sort(p+1,p+1+m,cmp); //重新排序 
	for(int i=1;i<=m;i++){
		int x=find(p[i].x),y=find(p[i].y);
		if(x==y)continue; //之前判断过 
		fa[x]=y; //合并 
		num++;//计数 
		ans = max(ans,p[i].t);//寻找时间 
	}
	if(num!=n-1) printf("-1\n");
	else printf("%d\n",ans);
	return 0;
}

# 一中校运会之百米跑

题目背景

在一大堆秀恩爱的 ** 之中,来不及秀恩爱的苏大学神踏着坚定(?)的步伐走向了 100 100 100 米跑的起点。这时苏大学神发现,百米赛跑的参赛同学实在是太多了,连体育老师也忙不过来。这时体育老师发现了身为体育委员的苏大学神,便来找他帮忙。

可是苏大学神需要热身,不然跑到一半就会抽(筋)、于是他就找到了你。。。如果你帮助体育老师解决了问题,老师就会给你 5 5 5 个积分。

题目描述

假设一共有 N N N 2 ≤ N ≤ 2 × 1 0 4 2\leq N\leq 2\times 10^4 2N2×104)个参赛选手。(尼玛全校学生都没这么多吧)

老师会告诉你这 N N N 个选手的名字。

接着会告诉你 M M M 1 ≤ M ≤ 1 0 6 1\leq M\leq 10^6 1M106)句话,即告诉你学生 A 与学生 B 在同一个组里。

如果学生 A 与学生 B 在同一组里,学生 B 与学生 C 也在同一组里,就说明学生 A 与学生 C 在同一组。

然后老师会问你 K K K 1 ≤ K ≤ 1 0 6 1\leq K\leq 10^6 1K106)句话,即学生 X 和学生 Y 是否在同一组里。

若是则输出 Yes.,否则输出 No.

输入格式

第一行输入 N N N M M M

接下来 N N N 行输入每一个同学的名字。

再往下 M M M 行每行输入两个名字,且保证这两个名字都在上面的 N N N 行中出现过,表示这两个参赛选手在同一个组里。

再来输入 K K K

接下来输入 K K K 个体育老师的询问。

输出格式

对于每一个体育老师的询问,输出 Yes.No.

样例 #1

样例输入 #1

10 6
Jack
Mike
ASDA
Michel
brabrabra
HeHe
HeHE
papapa
HeY
Obama
Jack Obama
HeHe HeHE
brabrabra HeHe
Obama ASDA
papapa Obama
Obama HeHE
3
Mike Obama
HeHE Jack
papapa brabrabra

样例输出 #1

No.
Yes.
Yes.

分析

既然检查两个选手是否在一个组里,自然而然的就想起了并查集。但是这个并查集和普通的并查集有些许的不同,里面多了个字符串,所以我们在查找根节点的时候,需要做一些改动,访问数组里面的编号项。如果他们在同一个组里,他们的根节点是肯定相同的,所以我们在输入时判断这两个人在没在同一个组里,如果在同一个组里,不用管,如果不在,那么合并对于k次询问,只需要判断这两个人有没有在同一个根节点,如果在就是Yes. 不在就是No.

代码

#include<bits/stdc++.h>
#define _ 0 
using namespace std;
int n,m,k;
struct node{		//用来储存名字和编号 
	string s;
	int num;
};
node uset[20000+50]; 
int find(int n){	//上文提到的查找 
	if(uset[n].num!=n) uset[n].num=find(uset[n].num);
	return uset[n].num;
}
void merge(int a,int b){	//合并并查集 
	a=find(a);
	b=find(b);		
	if(a==b) return ;
	else uset[a].num=uset[b].num;
}
int sfind(string a){	//查找这个名字 
	for(int i=1;i<=n;i++){
		if(a==uset[i].s) return i;
	}
}
int main(){
	cin>>n>>m;
	for(int i=1;i<=n;i++){
		cin>>uset[i].s;		//对于每次输入,存编号 
		uset[i].num=i;
	}	
	for(int i=1;i<=m;i++){	//m次输入 
		string sa,sb;	//管理大大,我莫得骂人,我只是不知道叫什么了 
		cin>>sa>>sb;
		if(find(sfind(sa))!=find(sfind(sb))){	//查找两个名字
			merge(sfind(sa),sfind(sb));		//合并 
		}
	} 
	cin>>k;	 
	for(int i=1;i<=k;i++){		//k次询问 
		string sx,bs;
		cin>>sx>>bs;
		if(find(sfind(sx))==find(sfind(bs))){	//也是查找两个名字
			cout<<"Yes."<<endl;			//在一个组里 
		}else {
			cout<<"No."<<endl;			//不在一个组 
		}
	}
	return 0;
}

(优化数据版)POJ2524-Ubiquitous Religions

Description

There are so many different religions in the world today that it is difficult to keep track of them all. You are interested in finding out how many different religions students in your university believe in.

You know that there are n students in your university (0 < n <= 50000). It is infeasible for you to ask every student their religious beliefs. Furthermore, many students are not comfortable expressing their beliefs. One way to avoid these problems is to ask m (0 <= m <= n(n-1)/2) pairs of students and ask them whether they believe in the same religion (e.g. they may know if they both attend the same church). From this data, you may not know what each person believes in, but you can get an idea of the upper bound of how many different religions can be possibly represented on campus. You may assume that each student subscribes to at most one religion.

Input

The input consists of a number of cases. Each case starts with a line specifying the integers n and m. The next m lines each consists of two integers i and j, specifying that students i and j believe in the same religion. The students are numbered 1 to n. The end of input is specified by a line in which n = m = 0.

Output

For each test case, print on a single line the case number (starting with 1) followed by the maximum number of different religions that the students in the university believe in.

Sample Input

10 9
1 2
1 3
1 4
1 5
1 6
1 7
1 8
1 9
1 10
10 4
2 3
4 5
4 8
5 8
0 0

Sample Output

Case 1: 1
Case 2: 7

分析

并查集裸题。 现在注意了初始化坑点。

代码

#include <cstdio>
#include <iostream>
//优化数据 
using namespace std;
const int maxn = 50000+5;
int s[maxn];
int height[maxn];

int fd(int x){	//查找根 
	return x == s[x] ? x:fd(s[x]);
}
void hb(int x,int y){
	x = fd(x);
	y = fd(y);
	if(height[x] == height[y]){ //深度相同 
		height[x] = height[x] + 1;
		s[y] = x; //让x成为y的根 
	}else{
		if(height[x] < height [y]) s[x] = y; //y比x的根深,让y成为x的根 
		else s[y] = x; //反之 
	}
}
 
int main(){
	int t,n,m,x,y;
	int k=1;
	while(cin >> n >>m && m && n){ //多组输入 
		for(int i = 1;i<=n;i++){
			//赋值 
		s[i]=i;
		height[i]=0;
	}
		for(int i = 1;i<=m;i++){
			cin >>x>>y;
			hb(x,y);//合并 
		}
		int ans = 0;
		for(int i=1;i<=n;i++){	//统计有多少个集合 
			if(s[i] == i)
				ans++;
		}
		cout<<"Case "<<k++<<": "<<ans<<endl;
	}
	return 0;
}

How Many Tables 普通并查集

题目

那天是伊格那丢的生日。他邀请了很多朋友。现在该吃晚饭了。伊格那丢想知道他至少需要多少张桌子。你要注意,并不是所有的朋友都认识彼此,所有的朋友都不想和陌
生人呆在一起。
这个问题的一个重要规则是如果我告诉你A认识B, B认识C,这意味着A B C彼此认识,所以它们可以留在一个表中。
例如,如果我告诉你A知道B, B知道C, D知道E,那么A, B, C可以留在一个表中,而D, E必须留在另一个表中。所以伊格那丢至少需要两张桌子。

输入以整数T(1<=T<=25)开始,表示测试用例的数量。
然后是T测试用例。
每个测试用例都以两个整数N和M开始(1<=N,M<=1000)。
N表示朋友的数量,从1到N,后面有M行。
每一行由两个整数A和B组成(A!=B),表示朋友A和朋友B认识。
两种情况之间将有一条空行。

对于每个测试用例,只输出Ignatius至少需要多少个桌子。不要打印任何空白。

input

2
5 3
1 2
2 3
4 5
f[1~5]:1->2 2->3 3->3 4->5 5->5根节点只有2个
5 1
2 5

output

2
4

错误思路

开个father[N],默认为0,再写个初始化函数,每个案例先变0,再初始化成i,路径压缩,最后在1~n查询有多少个不同的根节点
错因:没有理解合并并查集的操作

正确思路:

合并并查集后再查询

代码

#include<bits/stdc++.h>

using namespace std;

const int N = 1e3 +5;

int f[N];
int n,m,t,x,y,a,b;
int fd(int x){ // 查找 
	if(f[x] == x) return x;
	else return f[x] = fd(f[x]);
}
void hb(int x,int y){ //合并 
	f[fd(y)] = fd(x);//把x合并到y上,y的根成为x的根 
}
int main()
{
	cin>>t;
	while(t--){//t个测试数据 
		cin>>n>>m;
		for(int i=1;i<=n;i++) f[i] = i;//初始化 
		while(m--){
			cin>>x>>y;
			hb(x,y);//合并 
		}
		int ans = 0;
		for(int i=1;i<=n;i++){
			if(f[i] == i)	ans++;//统计 
		}
		cout<<ans<<endl;
	}
	
	return 0;
}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值