【刷题日记】并查集基础练习题
亲戚(洛谷P1551)
题目背景
若某个家族人员过于庞大,要判断两个是否是亲戚,确实还很不容易,现在给出某个亲戚关系图,求任意给出的两个人是否具有亲戚关系。
题目描述
规定:\(x\) 和 \(y\) 是亲戚,\(y\) 和 \(z\) 是亲戚,那么 \(x\) 和 \(z\) 也是亲戚。如果 \(x\),\(y\) 是亲戚,那么 \(x\) 的亲戚都是 \(y\) 的亲戚,\(y\) 的亲戚也都是 \(x\) 的亲戚。
输入格式
第一行:三个整数 \(n,m,p\),(\(n,m,p \le 5000\)),分别表示有 \(n\) 个人,\(m\) 个亲戚关系,询问 \(p\) 对亲戚关系。
以下 \(m\) 行:每行两个数 \(M_i\),\(M_j\),\(1 \le M_i,~M_j\le n\),表示 \(M_i\) 和 \(M_j\) 具有亲戚关系。
接下来 \(p\) 行:每行两个数 \(P_i,P_j\),询问 \(P_i\) 和 \(P_j\) 是否具有亲戚关系。
输出格式
\(p\) 行,每行一个 Yes 或 No。表示第 \(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
题解
用并查集直接解决。
AC代码
#include <bits/stdc++.h>
using namespace std;
long long n,m,p,x,y;
long long sett[5050];
//查找函数
int find_set(int x){
return x==sett[x]? x : find_set(sett[x]);
}
//合并函数
void merge_set(int x,int y){
x = find_set(x);
y = find_set(y);
if(x != y) sett[x] = sett[y];
return;
}
int main(){
scanf("%lld %lld %lld",&n,&m,&p);
//初始化并查集
for(long long i = 1;i<=n;i++) sett[i] = i;
//输入+合并
while(m--){
scanf("%lld %lld",&x,&y);
merge_set(x,y);
}
while(p--){
scanf("%lld %lld",&x,&y);
if(find_set(x) == find_set(y)) cout<<"Yes"<<endl;
else cout<<"No"<<endl;
}
}
或用路径压缩法:
把查找函数改为
点击查看代码
//查找函数
int find_set(int x){
if(x != sett[x]) sett[x] = find_set(sett[x]);
return sett[x];
}
其他不变即可。
修复公路(洛谷P1111)
题目背景
A 地区在地震过后,连接所有村庄的公路都造成了损坏而无法通车。政府派人修复这些公路。
题目描述
给出 A 地区的村庄数 \(N\),和公路数 \(M\),公路是双向的。并告诉你每条公路的连着哪两个村庄,并告诉你什么时候能修完这条公路。问最早什么时候任意两个村庄能够通车,即最早什么时候任意两条村庄都存在至少一条修复完成的道路(可以由多条公路连成一条道路)。
输入格式
第 \(1\) 行两个正整数 \(N,M\)。
下面 \(M\) 行,每行 \(3\) 个正整数 \(x,y,t\),告诉你这条公路连着 \(x,y\) 两个村庄,在时间 \(t\) 时能修复完成这条公路。
输出格式
如果全部公路修复完毕仍然存在两个村庄无法通车,则输出 \(-1\),否则输出最早什么时候任意两个村庄能够通车。
样例 #1
样例输入 #1
4 4
1 2 6
1 3 4
1 4 5
4 2 3
样例输出 #1
5
提示
\(1\leq x, y\leq N \le 10 ^ 3\),\(1\leq M, t \le 10 ^ 5\)。
题解
先按照时间顺序排序(这里为了省事直接用优先队列嘻嘻),然后一条一条修好并检查。
AC代码
#include <bits/stdc++.h>
using namespace std;
long long s[1005];
priority_queue<tuple<long long,long long,long long>,vector<tuple<long long,long long,long long>>,greater<tuple<long long,long long,long long>>>pq;
long long n,m,x,y,t;
long long find_set(long long x){
if(x != s[x]) s[x] = find_set(s[x]);
return s[x];
}
void merge_set(long long x,long long y){
x = find_set(x);
y = find_set(y);
if(x != y) s[x] = s[y];
return;
}
bool check(){
long long num = 0;
for(long long i = 1;i<=n;i++)
if(i == s[i]) num++;
return (num == 1);
}
int main(){
scanf("%lld %lld",&n,&m);
for(long long i = 1;i<=n;i++) s[i] = i;
for(long long i = 0;i<m;i++){
scanf("%lld %lld %lld",&x,&y,&t);
pq.push({t,x,y});
}
while(!pq.empty()){
auto it = pq.top();
pq.pop();
merge_set(get<1>(it),get<2>(it));
if(check()){
printf("%lld",get<0>(it));
return 0;
}
}
printf("-1");
return 0;
}
修改数组(蓝桥185)
题目
题解
如果暴力遍历,只能通过30%的测试用例。
如果用hash的方法存储数的存在情况,则可通过80%的测试用例。
正解是用并查集的方法,每个数的帮主都是再输入这个数将输出的数。帮主的查找与更新是难点。
AC代码
#include <bits/stdc++.h>
using namespace std;
const long long N = 1100002;
long long s[N];
long long n,scan;
long long find_set(long long x){
if(x != s[x]) s[x] = find_set(s[x]);
return s[x];
}
int main(){
scanf("%lld",&n);
for(long long i = 1;i<N;i++) s[i] = i;
for(long long i = 0;i<n;i++){
scanf("%lld",&scan);
printf("%lld ",find_set(scan));
s[find_set(scan)] = find_set(find_set(scan)+1);
}
return 0;
}
蓝桥幼儿园(蓝桥1135)
题目
题解
AC代码
#include <bits/stdc++.h>
using namespace std;
const long long N = 1100002;
long long n,m,op,x,y;
long long s[200005];
long long find_set(long long x){
if(x != s[x]) s[x] = find_set(s[x]);
return s[x];
}
void m_set(){
x = find_set(x);
y = find_set(y);
if(x != y) s[x] = s[y];
return;
}
int main(){
scanf("%lld %lld",&n,&m);
for(long long i = 1;i<=n;i++) s[i] = i;
while(m--){
scanf("%lld %lld %lld",&op,&x,&y);
op--;
if(!op){
m_set();
}
else{
if(find_set(x) == find_set(y)) printf("YES\n");
else printf("NO\n");
}
}
return 0;
}
简单的集合合并(蓝桥3959)
题目
题解
注意修改次数很多,所以千万不能在l到r的区间内使用find_set(),会超时,并且记得用root保存find_set(l);
AC代码
#include <bits/stdc++.h>
using namespace std;
long long n,q,l,r,ans,x,root;
long long s[1000100];
long long find_set(long long x){
if(x != s[x]) s[x] = find_set(s[x]);
return s[x];
}
int main(){
scanf("%lld %lld",&n,&q);
for(long long i = 1;i<=n;i++) s[i] = i;
for(long long i = 0;i<n;i++){
scanf("%lld",&x);
if(x) s[find_set(i+1)] = s[find_set(x)];
}
while(q--){
scanf("%lld %lld",&l,&r);
root = find_set(l);
for(long long i = l+1;i<=r;i++){
s[s[i]] = root;
}
}
ans = 0;
for(long long i = 1;i<=n;i++){
if(s[i] == i) ans++;
}
printf("%lld",ans);
return 0;
}
[USACO3.1] 最短网络 Agri-Net(洛谷P1546)
题目背景
Farmer John 被选为他们镇的镇长!他其中一个竞选承诺就是在镇上建立起互联网,并连接到所有的农场。当然,他需要你的帮助。
题目描述
FJ 已经给他的农场安排了一条高速的网络线路,他想把这条线路共享给其他农场。为了用最小的消费,他想铺设最短的光纤去连接所有的农场。
你将得到一份各农场之间连接费用的列表,你必须找出能连接所有农场并所用光纤最短的方案。每两个农场间的距离不会超过 \(10^5\)。
输入格式
第一行农场的个数 \(N\)(\(3 \leq N \leq 100\))。
接下来是一个 \(N \times N\) 的矩阵,表示每个农场之间的距离。理论上,他们是 \(N\) 行,每行由 \(N\) 个用空格分隔的数组成,实际上,由于每行 \(80\) 个字符的限制,因此,某些行会紧接着另一些行。当然,对角线将会是 \(0\),因为不会有线路从第 \(i\) 个农场到它本身。
输出格式
只有一个输出,其中包含连接到每个农场的光纤的最小长度。
样例 #1
样例输入 #1
4
0 4 9 21
4 0 8 17
9 8 0 16
21 17 16 0
样例输出 #1
28
提示
题目翻译来自NOCOW。
USACO Training Section 3.1
题解
贪心算法,每次找出最短路径去连接即可。
AC代码
#include <bits/stdc++.h>
using namespace std;
long long s[110];
long long n,scan,now_min,ans=0,num1,num2,root1,root2;
priority_queue<tuple<long long,long long,long long>,vector<tuple<long long,long long,long long>>,greater<tuple<long long,long long,long long>>>pq;
long long find_set(long long x){
if(x != s[x]) s[x] = find_set(s[x]);
return s[x];
}
void merge_set(long long x,long long y){
x = find_set(x);
y = find_set(y);
if(x != y) s[x] = s[y];
return;
}
bool check(){
long long num = 0;
for(long long i = 1;i<=n;i++) if(s[i] == i) num++;
return (num == 1);
}
int main(){
ios::sync_with_stdio(0);
cin.tie(0);
cin>>n;
for(long long i = 0;i<n;i++){
s[i+1] = i+1;
for(long long j = 0;j<i+1;j++) cin>>scan;
for(long long j = 0;j<n-i-1;j++){
cin>>scan;
pq.push({scan,i+1,i+2+j});
}
}
while(!check()){
auto it = pq.top();
pq.pop();
num1 = get<1>(it);
num2 = get<2>(it);
root1 = find_set(num1);
root2 = find_set(num2);
if(root1 == root2) continue;
//printf("connect %lld %lld sum %lld\n",num1,num2,get<0>(it));
merge_set(num1,num2);
ans += get<0>(it);
}
cout<<ans;
return 0;
}
家谱(洛谷P2814)
题目背景
现代的人对于本家族血统越来越感兴趣。
题目描述
给出充足的父子关系,请你编写程序找到某个人的最早的祖先。
输入格式
输入由多行组成,首先是一系列有关父子关系的描述,其中每一组父子关系中父亲只有一行,儿子可能有若干行,用 #name 的形式描写一组父子关系中的父亲的名字,用 +name 的形式描写一组父子关系中的儿子的名字;接下来用 ?name 的形式表示要求该人的最早的祖先;最后用单独的一个 $ 表示文件结束。
输出格式
按照输入文件的要求顺序,求出每一个要找祖先的人的祖先,格式为:本人的名字 \(+\) 一个空格 \(+\) 祖先的名字 \(+\) 回车。
样例 #1
样例输入 #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 \times 10^4\) 人,家谱中的记载不超过 \(30\) 代。
题解
融合了字符串处理,那需要对每一个人给一个编号。
AC代码
#include <bits/stdc++.h>
using namespace std;
using i64 = long long;
i64 s[50010];
char c = '\0';
string str1,str2;
map<string,i64>m;
string sss[50010];
i64 cnt = 1;
i64 find_set(i64 x){
if(x != s[x]) s[x] = find_set(s[x]);
return s[x];
}
void merge_set(i64 x,i64 y){
x = find_set(x);
y = find_set(y);
if(x != y) s[x] = s[y];
return;
}
int main(){
for(i64 i = 0;i<50010;i++) s[i] = i;
while(1){
cin>>c;
if(c == '$') break;
switch(c){
case '#': //输入父亲
cin>>str1;
if(!m.count(str1)){
m[str1] = cnt;
sss[cnt++] = str1;
}
break;
case '+': //输入儿子
cin>>str2;
if(!m.count(str2)){
m[str2] = cnt;
sss[cnt++] = str2;
}
merge_set(m[str2],m[str1]);
break;
case '?': //输入查询
cin>>str1;
cout<<str1<<" "<<sss[find_set(m[str1])]<<"\n";
break;
}
}
return 0;
}
选择题(洛谷P6691)
题目背景
小 L 喜欢逻辑推理。
一天,他在一本由英国哲士沃·协德编写的《我也不知道为什么要叫这个名字的一本有关逻辑学的书》中翻到了一道奇特的问题,但他并不会做。他知道你善于用程序解决问题,于是决定让你来帮助他完成这些问题。
题目描述
这是一道有 \(n\) 个选项的选择题,每个选项的内容都很独特。第 \(i\) 个选项的内容的形式如下:
- 第 \(a_i\) 个选项是正确/错误的
小 L 认为这种题目的答案不一定是唯一的,所以他想问题这道题有多少种合法的答案(可以全部正确或全部错误)。他还想问你这么多答案中,正确选项最多和最少的答案分别有多少个正确选项。
当然,如果这道题不存在合法的答案,你可以直接回答小 L No answer。
输入格式
第一行有一个正整数 \(n\),表示选项个数。
接下来 \(n\) 行,每行有两个整数 \(a_i,opt_i\),描述一个选项。其中当 \(opt_i=1\) 时,表示这个选项的内容为 第 \(a_i\) 个选项是正确的;当 \(opt_i=0\) 时,表示这个选项的内容为 第 \(a_i\) 个选项是错误的。
输出格式
如果没有答案满足这道选择题,输出No answer。
否则输出三行,每行一个正整数,分别为合法答案数及正确选项最多和最少的答案分别有多少个正确选项。其中合法答案数要对 \(998244353\) 取模。
样例 #1
样例输入 #1
4
2 1
4 0
1 1
2 0
样例输出 #1
2
3
1
样例 #2
样例输入 #2
10
4 1
7 0
2 0
3 1
7 1
5 0
9 1
10 1
8 0
1 1
样例输出 #2
No answer
提示
对于样例一,一共有下面 \(2\) 种正确答案:
- 第 \(1,2,3\) 个选项是正确的。
- 第 \(4\) 个选项是正确的。
其中正确选项最多的答案有 \(3\) 个选项正确,正确选项最少的答案有 \(1\) 个选项正确。
数据范围
对于 \(10\%\) 的数据,\(n\leq 10\)。
对于 \(30\%\) 的数据,\(n\leq 100\)。
对于 \(60\%\) 的数据,\(n\leq 10^3\)。
对于 \(100\%\) 的数据,\(n\leq 10^6,1\leq a_i\leq n,i\neq a_i,opt_i\in\{0,1\}\)。
题解
带权并查集。用权表示两个选项的相斥或相同关系。融合时用异或算法处理权值。
AC代码
#include <bits/stdc++.h>
using namespace std;
using i64 = long long;
const i64 MOD = 998244353;
i64 s[1001000],n,a,opt,num = 0,max_num = 0,min_num = 0;
bool quan[1001000],flag = 0;
i64 num_t[1001000][2];
i64 pow_mod(i64 a,i64 x){
i64 result = 1;
while(x){
if(x%2){
result = result * a % MOD;
}a = a * a % MOD;
x /= 2;
}
return result;
}
i64 find_set(i64 x){
if(x != s[x]){
i64 px = s[x];
s[x] = find_set(s[x]);
quan[x] = (quan[x] + quan[px]) % 2;
}
return s[x];
}
void merge_set(i64 x,i64 y,bool o){
i64 px = find_set(x);
i64 py = find_set(y);
bool quan_px = quan[x];
if(px != py){
s[px] = py;
quan[px] = (quan_px + quan[y] + o + 1) % 2;
}
if(px == py && (quan[x] + quan[y])%2 != (o + 1)%2){
printf("No answer"); flag = 1;
}
return;
}
int main(){
scanf("%lld",&n);
for(i64 i = 1;i<=n;i++) s[i] = i;
for(i64 i = 1;i<=n;i++){
scanf("%lld %lld",&a,&opt);
merge_set(i,a,opt);
if(flag) return 0;
}
for(i64 i = 1;i<=n;i++){
if(s[i] == i){
num++;
num_t[i][0]++;
}
else{
num_t[find_set(i)][quan[i]]++;
}
}
printf("%lld\n",pow_mod(2,num));
for(i64 i = 1;i<=n;i++){
if(s[i] == i){
max_num += max(num_t[i][0], num_t[i][1]);
min_num += min(num_t[i][0], num_t[i][1]);
}
}
printf("%lld\n%lld",max_num,min_num);
return 0;
}

浙公网安备 33010602011771号