并查集算法ヽ(=・ω・=)丿
一、前言
这星期在翻题单,偶然看到了并查集,我之前新生赛被这个东西卡了一手┭┮﹏┭┮,所以就很想弄明白,所以这周就是并查集了。然后并查集可能相对于新手来说不是特别常见的概念,所以这里大概解释一下
二、什么是并查集 一 一+
并查集,在一些有N个元素的集合应用问题中,我们通常是在开始时让每个元素构成一个单元素的集合,然后按一定顺序将属于同一组的元素所在的集合合并,其间要反复查找一个元素在哪个集合中。这一类问题近几年来反复出现在信息学的国际国内赛题中,其特点是看似并不复杂,但数据量极大,若用正常的数据结构来描述的话,往往在空间上过大,计算机无法承受;即使在空间上勉强通过,运行的时间复杂度也极高,根本就不可能在比赛规定的运行时间(1~3秒)内计算出试题需要的结果,只能用并查集来描述。
这是百度的解释,有点抽象噢X﹏X大概就是说让一些元素有一个共同的老大,然后我们查找的时候,找到他们的老大就行了。
为啥要共同的老大?因为路径压缩,如果元素之间关系比较复杂的话,查找的效率会大大下降(时间复杂度从O(logn)到 O(n))
核心代码
int find(int k){
if(f[k]==k)return k;
return f[k]=find(f[k]);
}
看起来还是很简单的哈,让我们一起刷一些题看一看
三、go go go
1、并查集
题目描述
如题,现在有一个并查集,你需要完成合并和查询操作。
输入格式
第一行包含两个整数 N*,M,表示共有 N 个元素和 MM 个操作。
接下来 M行,每行包含三个整数 Xi,Yi,Zi
当 Zi=1 时,将 Xi与 Yi 所在的集合合并。
当 Zi=2 时,输出 Xi,Yi 是否在同一集合内,是的输出 Y
;否则输出 N
。
输出格式
对于每一个 Zi = 2 的操作,都有一行输出,每行包含一个大写字母,为 Y
或者 N
。
输入输出样例
输入 #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% 的数据,N ≤10,M ≤20 。
对于 70% 的数据,N ≤100,1 M≤10^3 。
对于 100% 的数据,1≤N≤104,1≤*M*≤2×105 。
这题完全就是并查集的模板,按照刚刚说的代码直接套即可,相当于大家一起先上个手。
AC code
#include <iostream>
#include <bits/stdc++.h>
#pragma GCC optimize(2)
using namespace std;
int fa[10009];
int find(int k){
if(fa[k] == k){
return fa[k];
}
else{
return fa[k] = find(fa[k]);
}
}
int main()
{
ios::sync_with_stdio(false);
int n,m;
cin >> n>> m;
//初始化,自己是自己的老大
for(int i = 0; i <n; i++){
fa[i] = i;
}
for(int i = 0 ; i < m; i++){
int x,y,z;
cin >> z >> x >> y;
if(z == 1){
fa[find(x)] = find(y);
}
if(z == 2){
if(find(x) == find(y)){
cout<<"Y"<<endl;
}
else{
cout<<"N"<<endl;
}
}
}
return 0;
}
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个询问的答案为“具有”或“不具有”亲戚关系。
输入输出样例
输入 #1
6 5 3
1 2
1 5
3 4
5 2
1 3
1 4
2 3
5 6
输出 #1
Yes
Yes
No
这道题是并查集的应用,其实和第一道题一样,没有太大难度,直接上代码
AC code
#include <iostream>
#include <bits/stdc++.h>
#pragma GCC optimize(2)
using namespace std;
int fa[6000];
int find(int k){
if(fa[k] == k){
return k;
}
return fa[k] = find(fa[k]);
}
int main()
{
ios::sync_with_stdio(false);
int a,b,c;
cin >> a >> b >> c;
for(int i = 0 ; i < a; i++){
fa[i] = i;
}
int m,n;
for(int i = 0; i < b; i++){
cin >> m >> n;
fa[find(m)] = find(n);
}
for(int i = 0; i < c; i++){
cin >> m >> n;
if(find(m) == find(n)){
cout<<"Yes"<<endl;
}
else{
cout<<"No"<<endl;
}
}
return 0;
}
3、奶酪
题目描述
现有一块大奶酪,它的高度为 h,它的长度和宽度我们可以认为是无限大的,奶酪中间有许多半径相同的球形空洞。我们可以在这块奶酪中建立空间坐标系,在坐标系中,奶酪的下表面为 z=0,奶酪的上表面为 z=h。
现在,奶酪的下表面有一只小老鼠 Jerry,它知道奶酪中所有空洞的球心所在的坐标。如果两个空洞相切或是相交,则 Jerry 可以从其中一个空洞跑到另一个空洞,特别地,如果一个空洞与下表面相切或是相交,Jerry 则可以从奶酪下表面跑进空洞;如果一个空洞与上表面相切或是相交,Jerry 则可以从空洞跑到奶酪上表面。
位于奶酪下表面的 Jerry 想知道,在不破坏奶酪的情况下,能否利用已有的空洞跑 到奶酪的上表面去?
空间内两点 P1(x1,y1,z1) P2(x2,y2,z2) 的距离公式如下:
dist(P1,P2)=[(x1−x2)^2 +(y1−y2)^2 +(z1−z2)^2 ]^(1/2)
输入格式
每个输入文件包含多组数据。
第一行,包含一个正整数 T,代表该输入文件中所含的数据组数。
接下来是 T 组数据,每组数据的格式如下: 第一行包含三个正整数 n,h,r,两个数之间以一个空格分开,分别代表奶酪中空洞的数量,奶酪的高度和空洞的半径。
接下来的 n 行,每行包含三个整数 x,y,z,两个数之间以一个空格分开,表示空洞球心坐标为 (x,y,z)(x,y,z)。
输出格式
T 行,分别对应 T 组数据的答案,如果在第 i 组数据中,Jerry 能从下表面跑到上表面,则输出 Yes
,如果不能,则输出 No
。
输入输出样例
输入 #1
3
2 4 1
0 0 1
0 0 3
2 5 1
0 0 1
0 0 4
2 5 2
0 0 2
2 0 4
输出 #1
Yes
No
Yes
说明/提示
【输入输出样例 11 说明】
第一组数据,由奶酪的剖面图可见:
第一个空洞在 (0,0,0) 与下表面相切;
第二个空洞在 (0,0,4) 与上表面相切;
两个空洞在(0,0,2) 相切。
输出 Yes
。
第二组数据,由奶酪的剖面图可见:
两个空洞既不相交也不相切。
输出 No
。
第三组数据,由奶酪的剖面图可见:
两个空洞相交,且与上下表面相切或相交。
输出 Yes
。
【数据规模与约定】
对于 20% 的数据,n=1,1≤h,r≤10^4,坐标的绝对值不超过 10^4。
对于 40% 的数据,1≤n≤8,1≤h,r≤10^4,坐标的绝对值不超过 10^4。
对于 80% 的数据,1≤n≤10^3 ,1≤h,r≤10^4,坐标的绝对值不超过 10^4。
对于 100% 的数据,1≤n≤1×10^3,1≤h,r≤109,T≤20,坐标的绝对值不超过 10^9。
相比前两道题,这道题难度就大一些了。我当时新生赛也是卡在一道类似的题上面。但现在回过来看的话,也是有一些思路的。我们来将这道题的信息和并查集的特点来对应的话,会发现难点主要是这道题没告诉我们哪些元素是一个集合,所以我们要自己判断,就是拿距离公式看哪些元素是可以放在一个集合的。还有一个难点是输出,并查集模板给出了要输出的两个元素,这道题其实同样是两个元素,分别是接触上面的元素和接触下面的元素,判断方法也是一样的,只不过这道题需要我们对之前录入的元素一一比对。
#include <iostream>
#include <bits/stdc++.h>
#define ll long long
#pragma GCC optimize(2)
using namespace std;
ll bottom[200000],top[200000];
ll x[200000], y[200000], z[200000];
ll fa[200000];
int find(int k){
if(fa[k] == k){
return k;
}
return fa[k] = find(fa[k]);
}
int main()
{
ios::sync_with_stdio(false);
int T;
cin >> T;
for(int i = 0 ; i <T; i++){
ll n,h,r,tbottom = 0,ttop = 0;
cin >> n >> h >> r;
//数据初始化
for(int j = 0; j < n; j++){
fa[j] = j;
}
for(int j = 0; j < n; j++){
cin >> x[j] >> y[j] >> z[j];
//判断是否能触碰底部
if(z[j] + r >= h){
top[ttop] = j;
ttop++;
}
//判断是否能触碰顶部
if(z[j] - r <= 0){
bottom[tbottom] = j;
tbottom++;
}
//构建并查集
for(int k = 0; k < j; k++){
if ((x[j]-x[k])*(x[j]-x[k])+(y[j]-y[k])*(y[j]-y[k]) > 4*r*r) {
continue;
}
if (((x[k]-x[j])*(x[k]-x[j])+(y[k]-y[j])*(y[k]-y[j])+(z[k] - z[j]) * (z[k] - z[j])) <= 4*r*r){
fa[find(k)] = find(j);
}
}
}
int p;
for(p = 0; p < ttop; p++){
int q;
for(q = 0; q < tbottom; q++){
if(find(top[p]) == find(bottom[q])){
cout << "Yes"<<endl;
break;
}
}
if(q < tbottom){
break;
}
}
if(p == ttop){
cout<<"No"<<endl;
}
}
return 0;
}
4、搭配购买
题目描述
明天就是母亲节了,电脑组的小朋友们在忙碌的课业之余挖空心思想着该送什么礼物来表达自己的心意呢?听说在某个网站上有卖云朵的,小朋友们决定一同前往去看看这种神奇的商品,这个店里有 nn 朵云,云朵已经被老板编号为 1,2,3,…,n1,2,3,…,n,并且每朵云都有一个价值,但是商店的老板是个很奇怪的人,他会告诉你一些云朵要搭配起来买才卖,也就是说买一朵云则与这朵云有搭配的云都要买,电脑组的你觉得这礼物实在是太新奇了,但是你的钱是有限的,所以你肯定是想用现有的钱买到尽量多价值的云。
输入格式
第一行输入三个整数,n,m,w,表示有 n 朵云,m 个搭配和你现有的钱的数目。
第二行至 n+1 行,每行有两个整数, ci*,*di,表示第 i 朵云的价钱和价值。
第 n+2 至 n+1+m 行 ,每行有两个整数 ui,vi。表示买第 ui 朵云就必须买第 vi 朵云,同理,如果买第 vi 朵就必须买第 ui 朵。
输出格式
一行,表示可以获得的最大价值。
输入输出样例
输入 #1
5 3 10
3 10
3 10
3 10
5 100
10 1
1 3
3 2
4 2
输出 #1
1
说明/提示
- 对于 30%30% 的数据,满足 1≤n≤100;
- 对于 50%50% 的数据,满足 1≤n,w≤10^3,1≤m≤100;
- 对于 100%100% 的数据,满足 1≤n≤10^4 ,0≤m≤5×10^3。
这道题的并查集部分是直接给出了关系,我们直接使用并查集模板即可,关键是这道题还涉及到并查集的应用,我们需要统计出来每个集合的价钱和价值,然后利用动态规划中的01背包问题模板来解决,背包问题有机会我们再展开来了解。
AC code
#include <iostream>
#include <bits/stdc++.h>
#pragma GCC optimize(2)
#define ll long long
using namespace std;
ll m,n,w,res = 0;
ll mon[30007];
ll evl[30007];
ll count1[30007];//统计价钱
ll count2[30007];//统计价值
ll tot = 1;//统计有多少个组合
ll t[300007];//记录每个组合的老大
ll f[300007];
ll find(int k){
if(f[k] == k){
return k;
}
return f[k] = find(f[k]);
}
int main()
{
ios::sync_with_stdio(false);
cin >> n >> m >> w;
for(int i = 1; i <= n; i++){
f[i] = i;
}
for(int i = 1 ; i <= n; i++){
cin >> mon[i] >> evl[i];
}
for(int i = 1 ;i <= m; i++){
int m,n;
cin >> m >> n;
f[find(m)] = find(n);
}
t[1] = find(1);
count1[1] = mon[1];
count2[1] = evl[1];
for(int i = 2 ; i <= n; i++){
int j = 1;
for(j = 1; j <= tot; j++){
if(t[j] == find(i)){
break;
}
}
if(j > tot){
tot++;
t[tot] = find(i);
count1[tot] = mon[i];
count2[tot] = evl[i];
}
else{
count1[j] += mon[i];
count2[j] += evl[i];
}
}
ll dp[10003] = {0};
for(int i = 1; i <= tot; i++){
for(int j = w; j >= count1[i]; j--)
dp[j] = max(dp[j], dp[j - count1[i]] + count2[i]);
}
cout<<dp[w]<<endl;
return 0;
}
5、朋友
题目背景
小明在A公司工作,小红在B公司工作。
题目描述
这两个公司的员工有一个特点:一个公司的员工都是同性。
A公司有N名员工,其中有P对朋友关系。B公司有M名员工,其中有Q对朋友关系。朋友的朋友一定还是朋友。
每对朋友关系用两个整数(Xi,Yi)组成,表示朋友的编号分别为Xi,Yi。男人的编号是正数,女人的编号是负数。小明的编号是1,小红的编号是-1.
大家都知道,小明和小红是朋友,那么,请你写一个程序求出两公司之间,通过小明和小红认识的人最多一共能配成多少对情侣。(包括他们自己)
输入格式
第1行,4个空格隔开的正整数N,M,P,Q。
之后P行,每行两个正整数Xi,Yi。
之后Q行,每行两个负整数Xi,Yi。
输出格式
一行,一个正整数,表示通过小明和小红认识的人最多一共能配成多少对情侣。(包括他们自己)
输入输出样例
输入 #1
4 3 4 2
1 1
1 2
2 3
1 3
-1 -2
-3 -3
输出 #1
2
说明/提示
对于30%数据,N,M<=100,P,Q<=200
对于80%数据,N,M<=4000,P,Q<=10000.
对于全部数据,N,M<=10000,P,Q<=20000。
这道题我们也很容易看出来是两个并查集,然后再搜出和1老大相同的元素个数,然后再输出小的那个。就是负数可能不太好处理,*=-1变正数就好了
AC code
#include <iostream>
#include <bits/stdc++.h>
#pragma GCC optimize(2)
using namespace std;
int f1[20000];
int f2[20000];
int m,n,p,q;
int find1(int k){
if(f1[k] == k){
return k;
}
return f1[k] = find1(f1[k]);
}
int find2(int k){
if(f2[k] == k){
return k;
}
return f2[k] = find2(f2[k]);
}
int main()
{
ios::sync_with_stdio(false);
cin >> m>> n >> p >> q;
for(int i = 1; i <= m; i++){
f1[i] = i;
}
for(int i = 1 ; i <= n; i++){
f2[i] = i;
}
for(int i = 1; i <= p; i++){
int x,y;
cin >> x >> y;
f1[find1(x)] = find1(y);
}
for(int i = 1; i <= q; i++){
int x,y;
cin >> x >> y;
x *= -1;
y *= -1;
f2[find2(x)] = find2(y);
}
int a1 = 0,a2 = 0;
int t1 = find1(1);
int t2 = find2(1);
for(int i = 1; i <= m; i++){
if(find1(i) == t1){
a1++;
}
}
for(int i = 1; i <= n; i++){
if(find2(i) == t2){
a2++;
}
}
if(a1 > a2){
cout << a2;
}
else{
cout << a1;
}
return 0;
}
6、修复公路
题目背景
A地区在地震过后,连接所有村庄的公路都造成了损坏而无法通车。政府派人修复这些公路。
题目描述
给出A地区的村庄数N,和公路数M,公路是双向的。并告诉你每条公路的连着哪两个村庄,并告诉你什么时候能修完这条公路。问最早什么时候任意两个村庄能够通车,即最早什么时候任意两条村庄都存在至少一条修复完成的道路(可以由多条公路连成一条道路)
输入格式
第11行两个正整数N,M
下面M行,每行3个正整数x,y,t,告诉你这条公路连着x,y两个村庄,在时间t时能修复完成这条公路。
输出格式
如果全部公路修复完毕仍然存在两个村庄无法通车,则输出−1,否则输出最早什么时候任意两个村庄能够通车。
输入输出样例
输入 #1
4 4
1 2 6
1 3 4
1 4 5
4 2 3
输出 #1
5
说明/提示
N≤1000,M≤100000
x≤N,y≤N,t≤100000
这道题我的想法是尽可能用较少的时间来尝试通路,所以我们需要将数据排序,然后用贪心算法,从时间最短的一个一个试,看什么时候能通路,所以这里我们还需要用到结构体,用结构体来存储每条路的情况。
AC code
#include <iostream>
#include <bits/stdc++.h>
#pragma GCC optimize(2)
using namespace std;
typedef struct road{
int x, y;
int t;
}road;
road roads[100009];
bool cmp(road r1, road r2){
return r1.t < r2.t;
}
int f[1009];
int find(int k){
if(f[k] == k){
return k;
}
else{
return f[k] = find(f[k]);
}
}
int main()
{
ios::sync_with_stdio(false);
int n,m;
cin >> n >> m;
for(int i = 1; i <= n; i++){
f[i] = i;
}
for(int i = 1; i <= m; i++){
cin >> roads[i].x >> roads[i].y >> roads[i].t;
}
sort(roads+1,roads+m+1,cmp);
int i;
for(i = 1;i <= m; i++){
f[find(roads[i].x)] = find(roads[i].y);
int j;
int a = find(1);
for(j = 1; j <= n; j++){
if(find(j) != a){
break;
}
}
if(j > n){
break;
}
}
if(i > m){
cout<<-1<<endl;
return 0;
}
cout<<roads[i].t;
return 0;
}
7、村村通
题目描述
某市调查城镇交通状况,得到现有城镇道路统计表。表中列出了每条道路直接连通的城镇。市政府 “村村通工程” 的目标是使全市任何两个城镇间都可以实现交通(但不一定有直接的道路相连,只要相互之间可达即可)。请你计算出最少还需要建设多少条道路?
输入格式
输入包含若干组测试测试数据,每组测试数据的第一行给出两个用空格隔开的正整数,分别是城镇数目 n 和道路数目 m ;随后的 m 行对应 m 条道路,每行给出一对用空格隔开的正整数,分别是该条道路直接相连的两个城镇的编号。简单起见,城镇从 1 到 n 编号。
注意:两个城市间可以有多条道路相通。
输出格式
对于每组数据,对应一行一个整数。表示最少还需要建设的道路数目。
输入输出样例
输入 #1
4 2
1 3
4 3
3 3
1 2
1 3
2 3
5 2
1 2
3 5
999 0
0
输出 #1
1
0
2
998
说明/提示
数据规模与约定
对于 100% 的数据,保证 1≤n<1000 。
这道题是考察并查集合并之后的集合数量,我们只需要找出老大的总数即可。
AC code
#include <iostream>
#include <bits/stdc++.h>
#pragma GCC optimize(2)
using namespace std;
int f[1009];
int find(int k){
if(f[k] == k){
return k;
}
return f[k] = find(f[k]);
}
int book[1009];
int main()
{
ios::sync_with_stdio(false);
int n, m;
for(;cin >> n>> m;){
int tot = 0;
for(int i = 1 ; i <= n; i++){
f[i] = i;
}
for(int i = 0; i < m; i++){
int x,y;
cin >> x >> y;
f[find(x)] = find(y);
}
for(int i = 1 ; i <= n; i++){
int a = find(i);
int j;
for(j = 1; j <= tot; j++){
if(book[j] == a){
break;
}
}
if(j > tot){
tot++;
book[tot] = a;
}
}
cout<<tot - 1<<endl;;
}
return 0;
}
8、一中运动会之百米跑
题目背景
在一大堆秀恩爱的**之中,来不及秀恩爱的苏大学神踏着坚定(?)的步伐走向了100米跑的起点。这时苏大学神发现,百米赛跑的参赛同学实在是太多了,连体育老师也忙不过来。这时体育老师发现了身为体育委员的苏大学神,便来找他帮忙。可是苏大学神需要热身,不然跑到一半就会抽(筋)、于是他就找到了你。。。如果你帮助体育老师解决了问题,老师就会给你5个积分。
题目描述
假设一共有N(2<=N<=20000)个参赛选手。(尼玛全校学生都没这么多吧)
老师会告诉你这N个选手的名字。
接着会告诉你M(1<=M<=1000000)句话,即告诉你学生A与学生B在同一个组里。
如果学生A与学生B在同一组里,学生B与学生C也在同一组里,就说明学生A与学生C在同一组。
然后老师会问你K(1<=K<=1000000)句话,即学生X和学生Y是否在同一组里。
若是则输出"Yes.",否则输出"No."
输入格式
第一行输入N和M。
接下来N行输入每一个同学的名字。
再往下M行每行输入两个名字,且保证这两个名字都在上面的N行中出现过,表示这两个参赛选手在同一个组里。
再来输入K。
接下来输入K个体育老师的询问。
输出格式
对于每一个体育老师的询问,输出"Yes.“或"No.”。
输入输出样例
输入 #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.
并查集和字符串的结合,我们可以声明一个结构体,也可以分别声明两个数组来处理。
#include <iostream>
#include <bits/stdc++.h>
#pragma GCC optimize(2)
using namespace std;
string name[20009];
int f[20009];
int find(int k){
if(f[k] == k){
return k;
}
return f[k] = find(f[k]);
}
int main()
{
ios::sync_with_stdio(false);
int N,M,K;
cin >> N >> M;
for(int i = 1; i <= N; i++){
f[i] = i;
}
for(int i = 1; i <= N; i++){
cin >> name[i];
}
for(int i = 0; i < M; i++){
string n1,n2;
int x,y,t = 2;
cin >> n1 >> n2;
for(int i = 1; i <= N; i++){
if(name[i] == n1){
x = i;
t--;
}
if(name[i] == n2){
y = i;
t--;
}
if(t == 0){
break;
}
}
f[find(x)] = find(y);
}
cin >> K;
for(int i = 1; i <= K; i++){
string n1,n2;
int x,y,t = 2;
cin >> n1 >> n2;
for(int i = 1; i <= N; i++){
if(name[i] == n1){
x = i;
t--;
}
if(name[i] == n2){
y = i;
t--;
}
if(t == 0){
break;
}
}
if(find(x) == find(y)){
cout<<"Yes."<<endl;
}
else{
cout<<"No."<<endl;
}
}
return 0;
}
9、团伙
题目描述
给定 n 个人,他们之间有两个种关系,朋友与敌对。可以肯定的是:
- 与我的朋友是朋友的人是我的朋友
- 与我敌对的人有敌对关系的人是我的朋友
现在这 n 个人进行组团,两个人在一个团队内当且仅当他们是朋友。
求最多的团体数。
输入格式
第一行一个整数 n 代表人数。
第二行一个整数 m 代表每个人之间的关系。
接下来 m 行每行一个字符 opt 与两个整数 p,q
- 如果 opt 为
F
代表 p 与 q 为朋友。 - 如果 opt 为
E
代表 p 与 q 为敌人。
输出格式
一行一个整数代表最多的团体数。
输入输出样例
输入 #1
6
4
E 1 4
F 3 5
F 4 6
E 1 2
输出 #1
3
说明/提示
对于 100% 的数据,2≤n≤1000,1≤m≤5000,1≤p,q≤n。
w(゚Д゚)w没想到居然碰到了我进工作室时的笔试题,这道题朋友部分就是普通的并查集,但是敌人部分还是需要思考一下的。敌人的敌人是朋友,当一个人不止一个敌人的时候,我们就把他的敌人合并,最后查看集合的数量,之前也遇到过类似的,难度也不大。
AC code
#include <iostream>
#include <bits/stdc++.h>
//#pragma GCC optimize(2)
using namespace std;
int f[1009];
int find(int k){
if(f[k] == k){
return k;
}
else{
return f[k] = find(f[k]);
}
}
int haters[1009];
int res[1009];
int main()
{
ios::sync_with_stdio(false);
int n,m;
cin >> n >> m;
for(int i = 1 ; i <= n; i++){
f[i] = i;
}
for(int i = 0; i < m; i++){
string s;
int x,y;
cin >> s >> x >> y;
if(s == "F"){
f[find(x)] = find(y);
}
if(s == "E"){
if(haters[x] == 0){
haters[x] = find(y);
}
else{
f[find(y)] = find(haters[x]);
}
if(haters[y] == 0){
haters[y] = find(x);
}
else{
f[find(x)] = find(haters[y]);
}
}
}
int tot = 1;
res[1] = find(1);
for(int i = 2; i <= n; i++){
int j;
int a = find(i);
for(j = 1; j <= tot; j++){
if(a == res[j]){
break;
}
}
if(j > tot){
tot++;
res[tot] = find(i);
}
}
cout << tot;
return 0;
}
10、无线通讯网
题目描述
国防部计划用无线网络连接若干个边防哨所。2 种不同的通讯技术用来搭建无线网络;
每个边防哨所都要配备无线电收发器;有一些哨所还可以增配卫星电话。
任意两个配备了一条卫星电话线路的哨所(两边都ᤕ有卫星电话)均可以通话,无论他们相距多远。而只通过无线电收发器通话的哨所之间的距离不能超过 D,这是受收发器的功率限制。收发器的功率越高,通话距离 D 会更远,但同时价格也会更贵。
收发器需要统一购买和安装,所以全部哨所只能选择安装一种型号的收发器。换句话说,每一对哨所之间的通话距离都是同一个 D。你的任务是确定收发器必须的最小通话距离 D,使得每一对哨所之间至少有一条通话路径(直接的或者间接的)。
输入格式
从 wireless.in 中输入数据第 1 行,2 个整数 S 和 P,S 表示可安装的卫星电话的哨所数,P 表示边防哨所的数量。接下里 P 行,每行两个整数 x,y 描述一个哨所的平面坐标(x, y),以 km 为单位。
输出格式
输出 wireless.out 中
第 1 行,1 个实数 D,表示无线电收发器的最小传输距离,精确到小数点后两位。
输入输出样例
输入 #1
2 4
0 100
0 300
0 600
150 750
输出 #1
212.13
说明/提示
对于 20% 的数据:P = 2,S = 1
对于另外 20% 的数据:P = 4,S = 2
对于 100% 的数据保证:1 ≤ S ≤ 100,S < P ≤ 500,0 ≤ x,y ≤ 10000。
这道题有点像第六题,同样的贪心算法,结构体,排序等等
AC code
#include <iostream>
#include <bits/stdc++.h>
#pragma GCC optimize(2)
using namespace std;
typedef struct edge{
int u,v;//连接的两边
double dis;//距离,用来排序
}edge;
edge edges[300000];
int x[600];
int y[600];
int P,S;
int f[600];
int find(int k){
if(f[k] == k){
return k;
}
else{
return f[k] = find(f[k]);
}
}
int t = 0;
bool cmp(edge e1, edge e2){
return e1.dis < e2.dis;
}
int main()
{
ios::sync_with_stdio(false);
cin >> S >> P;
for(int i = 1; i <= P; i++){
f[i] = i;
}
for(int i = 1; i<= P; i++){
cin >> x[i] >> y[i];
}
for(int i = 1; i <= P; i++){
for(int j = 1; j <= P; j++){
edges[t].u = i;
edges[t].v = j;
edges[t].dis = sqrt((x[i] - x[j])*(x[i]-x[j]) + (y[i] - y[j]) * (y[i] - y[j]));
t++;
}
}
sort(edges,edges+t,cmp);
int i;
for(i = 0; i < t; i++){
if(P <= S){
break;
}
int a = edges[i].u;
int b = edges[i].v;
if(find(a) != find(b)){
f[find(a)] = find(b);
P--;
}
}
printf("%.2lf",edges[i].dis);
return 0;
}
11、刻录光盘
题目描述
在JSOI2005夏令营快要结束的时候,很多营员提出来要把整个夏令营期间的资料刻录成一张光盘给大家,以便大家回去后继续学习。组委会觉得这个主意不错!可是组委会一时没有足够的空光盘,没法保证每个人都能拿到刻录上资料的光盘,又来不及去买了,怎么办呢?!
组委会把这个难题交给了LHC,LHC分析了一下所有营员的地域关系,发现有些营员是一个城市的,其实他们只需要一张就可以了,因为一个人拿到光盘后,其他人可以带着U盘之类的东西去拷贝啊!
可是,LHC调查后发现,由于种种原因,有些营员并不是那么的合作,他们愿意某一些人到他那儿拷贝资料,当然也可能不愿意让另外一些人到他那儿拷贝资料,这与我们JSOI宣扬的团队合作精神格格不入!!!
现在假设总共有N个营员(2<=N<=200),每个营员的编号为1~N。LHC给每个人发了一张调查表,让每个营员填上自己愿意让哪些人到他那儿拷贝资料。当然,如果A愿意把资料拷贝给B,而B又愿意把资料拷贝给C,则一旦A获得了资料,则B,C都会获得资料。
现在,请你编写一个程序,根据回收上来的调查表,帮助LHC计算出组委会至少要刻录多少张光盘,才能保证所有营员回去后都能得到夏令营资料?
输入格式
先是一个数N,接下来的N行,分别表示各个营员愿意把自己获得的资料拷贝给其他哪些营员。即输入数据的第i+1行表示第i个营员愿意把资料拷贝给那些营员的编号,以一个0结束。如果一个营员不愿意拷贝资料给任何人,则相应的行只有1个0,一行中的若干数之间用一个空格隔开。
输出格式
一个正整数,表示最少要刻录的光盘数。
输入输出样例
输入 #1
5
2 3 4 0
4 5 0
0
0
1 0
输出 #1
1
这道题第一反应果断并查集ヾ(・ω・`。)但慢着,仔细看会发现,这道题的联系是单向的ค(TㅅT)而一般的并查集是双向的。所以并不是那么简单的。所以首先我们要判断这道题我们输出的特点,其实就是集合的数量加上老大是自己的人数,仔细想一下就发现是这样,所以我们要记录每个元素是否存在老大,然后再按并查集算出集合个数,相加即可。另外,我看到题解有人提到Floyd算法(二叉树当中提到过),所以我就试着写了一下,大概原理就是,算出每个点之间的距离,只要是通路就视为一个集合,其实就是合并方法不一样,其他和并查集差不多。
AC code(并查集)
#include <iostream>
#include <bits/stdc++.h>
#pragma GCC optimize(2)
using namespace std;
int f[300];
int find(int k){
if(f[k] == k){
return k;
}
return f[k] = find(f[k]);
}
int q[300];
int main()
{
ios::sync_with_stdio(false);
int T;
cin >> T;
int tot = 0;
for(int i = 1; i <= T; i++){
f[i] = i;
}
for(int i = 1 ; i <= T; i++){
for(;;){
int x;
cin >> x;
if(x == 0){
break;
}
if(x != i){
int a = find(x);
int b = find(i);
q[x] = 1;
if(a != b){
f[a] = b;
}
}
}
}
for(int i = 1 ;i <= T; i++){
if(find(i) == i || q[i] == 0){
tot++;
}
}
cout<< tot;
return 0;
}
AC code(Floyd算法)
#include <iostream>
#include <bits/stdc++.h>
#pragma GCC optimize(2)
using namespace std;
int dis[202][202];
int f[202];
int main()
{
ios::sync_with_stdio(false);
int N;
cin >>N;
int tot = 0;
for(int i = 1 ;i <= N; i++){
f[i] = i;
}
for(int i = 1; i <= N; i++){
for(;;){
int x;
cin >> x;
if(x == 0){
break;
}
dis[i][x] = 1;
}
}
for(int i = 1; i <= N; i++){
for(int k = 1; k<=N; k++){
for(int j = 1; j<= N; j++){
if(dis[i][k] != 0 && dis[k][j] != 0){
dis[i][j] = 1;
}
}
}
}
for(int i = 1; i<=N; i++){
int j = 1;
for(j = 1; j<= N; j++){
if(dis[i][j]){
f[j] = f[i];
}
}
}
for(int i = 1; i <= N; i++){
if(f[i] == i){
tot++;
}
}
cout<<tot<<endl;
return 0;
}
12、家谱
题目背景
现代的人对于本家族血统越来越感兴趣。
题目描述
给出充足的父子关系,请你编写程序找到某个人的最早的祖先。
输入格式
输入由多行组成,首先是一系列有关父子关系的描述,其中每一组父子关系中父亲只有一行,儿子可能有若干行,用 #name
的形式描写一组父子关系中的父亲的名字,用 +name
的形式描写一组父子关系中的儿子的名字;接下来用 ?name
的形式表示要求该人的最早的祖先;最后用单独的一个 $
表示文件结束。
输出格式
按照输入文件的要求顺序,求出每一个要找祖先的人的祖先,格式为:本人的名字 ++ 一个空格 ++ 祖先的名字 ++ 回车。
输入输出样例
输入 #1
#George
+Rodney
#Arthur
+Gareth
+Walter
#Gareth
+Edward
?Edward
?Walter
?Rodney
?Arthur
$
输出 #1
Edward Arthur
Walter Arthur
Rodney George
Arthur Arthur
说明/提示
规定每个人的名字都有且只有 6 个字符,而且首字母大写,且没有任意两个人的名字相同。最多可能有 10^3 组父子关系,总人数最多可能达到 5×10^4 人,家谱中的记载不超过 30 代。
还是和之前一样,给名字编号,然后并查集。还有就是注意查重,出现过的名字都不要再定义。
AC code
#include <iostream>
#include <bits/stdc++.h>
#pragma GCC optimize(2)
using namespace std;
string names[50009];
int f[50009];
int find(int k){
if(f[k] == k){
return k;
}
return f[k] = find(f[k]);
}
int main()
{
ios::sync_with_stdio(false);
int fa = 1;
int tot = 0;
for(;;){
char op;
cin >> op;
if(op == '#'){
string s;
cin >> s;
int i;
for(i = 1; i <= tot; i++){
if(names[i] == s){
fa = i;
break;
}
}
if(i > tot){
tot++;
fa = tot;
f[tot] = tot;
names[tot] = s;
}
}
if(op == '+'){
int i;
string s;
cin >> s;
for( i =1 ;i <= tot; i++){
if(names[i] == s){
break;
}
}
if(i > tot){
tot++;
f[tot] = find(fa);
names[tot] = s;
}
else{
f[i] = find(fa);
}
}
if(op == '?'){
string s;
cin >> s;
int i;
for(i = 1; i <= tot; i++){
if(names[i] == s){
break;
}
}
cout<<names[i]<<" "<<names[find(i)]<<endl;
}
if(op == '$'){
break;
}
}
return 0;
}
四、小结
并查集算法本身难度不大,相信大家都可能轻松理解,但当并查集和一些其他的概念结合的时候难度就会上升,这周的题目大多不是很难,向大家展示了并查集和其他概念的简单结合,应该会比上周的二叉树容易理解,掌握得也更好一些