Bootstrap

并查集例题及解析

并查集概述

本章一共有四道例题。

1.入门——冗余关系

2.进阶——关押罪犯

3.提高——家谱

4.创新——搭配购买

1.冗余关系

Description

Mrs.Chen是一个很认真很称职的语文老师......

所以,当她看到学生作文里的人物关系描述得非常的麻烦的时候,她非常生气,于是宣布:凡是作文里有冗余关系的,一率罚抄出师表10次...同学们非常的恐惧,于是,每当他们写出一篇作文,都要拿来你这个语文兼OI天才这里,问你有没有冗余的关系......时间一久,你也烦了,于是就想写个程序来代劳...

现在这里有一篇作文,有n句描述人物关系的句子,描述了 n 个人的关系。

每条句子的定义是这样的:

X Y

它的意思是:X 认识 Y,Y 也认识 X。

现在要你求出文中冗余关系的数目。

注意:假如 A 认识 B,B 认识 C,则 A 也认识 C。

冗余关系的定义是指:即使没有这条关系,原图的所有关系照样成立。

Format

Input

第一行,两个整数,表示句子数量(n),表示人数(m)。

接下来 n 行,每行两个数,意义在描述里已经说了。

1 ≤ n,m ≤ 1000。

Output

一个整数,表示冗余关系的数目。

Samples

输入数据 1

3 3
1 2
1 3
2 3

输出数据 1

1

思路 

这题很简单,一个正常的并查集就可以了。

但是由于要求出冗余关系的数量,所以要求的是有多少条边现在并的两个点祖先已经相同了。

代码

#include<bits/stdc++.h>
using namespace std;
int n,m,x,y,c,d,a[3000010],ans; 
int find(int x){
	if(a[x]==x)return x;
	else return a[x]=find(a[x]);
}
int main(){
	ios::sync_with_stdio(0);
	cin>>n>>m;
	for(int i=1;i<=n;i++){
		a[i]=i;//初始化
	}
	for(int i=1;i<=m;i++){
		cin>>x>>y;
		c=find(x);
		d=find(y);
		if(c!=d){//祖先不同
			a[c]=d;
		}
		else ans++;//祖先相同,可以不用
	}
	cout<<ans;
	return 0;
}

2.关押罪犯 

问题描述

S 城现有两座监狱,一共关押着 N 名罪犯,编号分别为 1∼N。他们之间的关系自然也极不和谐。很多罪犯之间甚至积怨已久,如果客观条件具备则随时可能爆发冲突。我们用“怨气值”(一个正整数值)来表示某两名罪犯之间的仇恨程度,怨气值越大,则这两名罪犯之间的积怨越多。如果两名怨气值为 c 的罪犯被关押在同一监狱,他们俩之间会发生摩擦,并造成影响力为 c 的冲突事件。每年年末,警察局会将本年内监狱中的所有冲突事件按影响力从大到小排成一个列表,然后上报到 S 城 Z 市长那里。公务繁忙的 Z 市长只会去看列表中的第一个事件的影响力,如果影响很坏,他就会考虑撤换警察局长。在详细考察了 N 名罪犯间的矛盾关系后,警察局长觉得压力巨大。他准备将罪犯们在两座监狱内重新分配,以求产生的冲突事件影响力都较小,从而保住自己的乌纱帽。假设只要处于同一监狱内的某两个罪犯间有仇恨,那么他们一定会在每年的某个时候发生摩擦。那么,应如何分配罪犯,才能使 Z 市长看到的那个冲突事件的影响力最小?这个最小值是少?

输入

第一行为两个正整数 N 和 M,分别表示罪犯的数目以及存在仇恨的罪犯对数。

接下来的 M 行每行为三个正整数 aj,bj,cj表示 aj 号和 bj 号罪犯之间存在仇恨,其怨气值为 cj 。数据保证每对罪犯组合只出现一次。

输出

共1 行,为Z 市长看到的那个冲突事件的影响力。如果本年内监狱

中未发生任何冲突事件,请输出 0。

样例输入
4 6
1 4 2534
2 3 3512
1 2 28351
1 3 6618
2 4 1805
3 4 12884

样例输出

3512

提示

罪犯之间的怨气值如下面左图所示,右图所示为罪犯的分配方法,市长看到的冲突事件

影响力是3512(由2 号和3 号罪犯引发)。其他任何分法都不会比这个分法更优。

img

数据范围

对于30%的数据有N≤ 15。

对于70%的数据有N≤ 2000,M≤ 50000。

对于100%的数据有N≤ 20000,M≤ 100000。

NOIP2010提高组第三题

思路

这道题用到了扩展域并查集,专门用来处理敌人的敌人是朋友这类情况。

因为对于并查集存祖先的数组内任何一个>=1且<=n的数+n都没有用过,所以如果a和b是敌人,那么就把a和b+n并集,同时把a+n和b并集。因为如果还有个c和b是敌人,那么把c和b+n并集,同时把c+n和b并集,a和c就是朋友了。

而且还要使用贪心算法,先把怨气大的解决掉,如果两个囚犯的祖先已经一样了(一定会冲突),那么直接输出。如果没有冲突,输出0。

代码

#include<bits/stdc++.h>
using namespace std;
struct stu{//结构体 
	int x,y,z;
}w[100010];
int n,m,x,y,a[3000010],ans; 
int find(int x){
	if(a[x]==x)return x;
	else return a[x]=find(a[x]);
}
bool cmp(const stu&c,const stu&d){
	return c.z>d.z;//结构体排序(贪心) 
}
int main(){
	cin>>n>>m;
	for(int i=1;i<=2*n;i++)a[i]=i;//初始化 
	for(int i=1;i<=m;i++){
		cin>>w[i].x>>w[i].y>>w[i].z;//输入 
	}
	sort(w+1,w+m+1,cmp);//排序 
	for(int i=1;i<=m;i++){
		if(find(w[i].x)==find(w[i].y)){
			cout<<w[i].z;
			return 0;//已经做完了 
		}
		a[find(w[i].x+n)]=a[find(w[i].y)];
		a[find(w[i].y+n)]=a[find(w[i].x)];//扩展域并查集 
	}
	cout<<0;//没有冲突 
	return 0;
}

3.家谱

题目描述

​ 现代的人对于家族血统越来越感兴趣,现在给出充足的父子关系,请你编写程序找到某个人的最早的祖先。

输入

由多行组成:

输入由多行组成,首先是一系列有关父子关系的描述,其中每一组父子关系中父亲只有一行,儿子可能有若干行,用 #name 的形式描写一组父子关系中的父亲的名字,用 +name 的形式描写一组父子关系中的儿子的名字;接下来用?name的形式表示要求该人的最早的祖先;

最后用单独的一个$表示输入结束。

规定每个人的名字都有且只有 6 个字符,而且首字母大写,且没有任意两个人的名字相同。最多可能有 1000 组父子关系,总人数最多可能达到 50000人,家谱中记载不超过 30 代。

输出

按照输入顺序,求出每一个要找祖先的人的祖先,格式:本人的名字+一个空格+祖先的名字+回车。

样例输入
#George
+Rodney
#Arthur
+Gareth
+Walter
#Gareth
+Edward
?Edward
?Walter
?Rodney
?Arthur
$
样例输出
Edward Arthur
Walter Arthur
Rodney George
Arthur Arthur

思路 

可以用一个map存每个人的是第几个出现的,另一个数组用来存第几个出现的名字是什么,再用并查集。每次遇到#就更新现在要做祖先的点,遇到+就和当前要做祖先的点并起来。遇到?就输出当前的名字和最终祖先的名字。

代码

#include<bits/stdc++.h>
using namespace std;
int n,m,x,y,w,c,d,a[3000010],ans,num; 
map<string,int>mp;//用来存名字是第几个出现的
string s,s2,qw[100010];//qw数组数组用来存名字
int find(int x){
	if(a[x]==x)return x;
	else return a[x]=find(a[x]);//路径优化
}
int main(){
	ios::sync_with_stdio(0);
	for(int i=1;i<=50000;i++){
		a[i]=i;
	}
	while(cin>>s){
		if(s=="$")return 0;
		s2=s;
		s2.replace(0,1,"");
		if(!mp[s2])mp[s2]=++num;
		qw[mp[s2]]=s2;
		if(s[0]=='#')w=mp[s2];//更新现在要做祖先的点
		if(s[0]=='+'){
			if(find(mp[s2])!=find(w))a[find(mp[s2])]=a[find(w)];//并起来
		}
		if(s[0]=='?'){
			cout<<s2<<' '<<qw[find(mp[s2])]<<endl;//输出
		}
	}
}

4. 搭配购买

Description

Joe觉得云朵很美,决定去山上的商店买一些云朵,商店里有 n 朵云,云朵被编号为 1,2,......,n,并且每朵云都有一个价值。但是商店老板跟他说,一些云朵要搭配来买才好,所以买一朵云则与这朵云有搭配的云都要买,但是Joe的钱有限,所以他希望买的价值越多越好。

Format

Input

第 1 行 n,m,w 表示 n 朵云,m 个搭配,Joe有 w 的钱。

第 2 至 n + 1 行,每行 ci,di 表示 i 朵云的价钱和价值。

第 n + 2 至 n + 1 + m 行,每行 ui、vi 表示买 ui 必须买 vi,同理,如果买 vi 就必须买 ui。

Output

一行,表示可以获得的最大价值。

Samples

输入数据 1

5 3 10
3 10
3 10
3 10
5 100
10 1
1 3
3 2
4 2

输出数据 1

1

Explanation

对于 30% 的数据,n ≤ 100;

对于 50% 的数据,n ≤ 1000;m ≤ 100;w ≤ 10000;

对于 100% 的数据,n ≤ 10000;0 ≤ m ≤ 5000;w ≤ 10000。

思路

这题很好想,但比较难实现。把一定要并的做一遍并查集,然后再做一遍01背包枚举每个组要/不要。

代码

#include<bits/stdc++.h>
using namespace std;
int n,m,w,x,y,c,d,a[100010],b[100010],e[100010],ans,s,s2,num,q[100010],q2[100010],dp[100010]; 
vector<int>v[100010];//用于存每组有哪几个云朵 
int find(int x){
	if(a[x]==x)return x;
	else return a[x]=find(a[x]);
}
int main(){
	ios::sync_with_stdio(0);
	cin>>n>>m>>w;
	for(int i=1;i<=n;i++){
		a[i]=i;
		cin>>b[i]>>e[i];
	}
	for(int i=1;i<=m;i++){
		cin>>x>>y;
		c=find(x);
		d=find(y);
		if(c!=d){
			a[c]=d;//并查集 
		}
	}
	for(int i=1;i<=n;i++){
		v[find(i)].push_back(i);//最终祖先加上 
	}
	for(int i=1;i<=10000;i++){//最大为一万 
		s=0,s2=0;
		for(int j=0;j<v[i].size();j++){
			s+=b[v[i][j]];//加上需要的钱数 
			s2+=e[v[i][j]];//加上价值 
		}
		q[++num]=s;
		q2[num]=s2;
	}
	for(int i=1;i<=num;i++){
		for(int j=w;j>=q[i];j--){
			dp[j]=max(dp[j],dp[j-q[i]]+q2[i]);//01背包 
		}
	}
	for(int i=0;i<=w;i++)ans=max(ans,dp[i]);//取最大值 
	cout<<ans;
	return 0;
} 

;