一,亲戚
题目背景
若某个家族人员过于庞大,要判断两个是否是亲戚,确实还很不容易,现在给出某个亲戚关系图,求任意给出的两个人是否具有亲戚关系。
题目描述
规定:xx 和 yy 是亲戚,yy 和 zz 是亲戚,那么 xx 和 zz 也是亲戚。如果 xx,yy 是亲戚,那么 xx 的亲戚都是 yy 的亲戚,yy 的亲戚也都是 xx 的亲戚。
输入格式
第一行:三个整数 n,m,pn,m,p,(n,m,p \le 5000n,m,p≤5000),分别表示有 nn 个人,mm 个亲戚关系,询问 pp 对亲戚关系。
以下 mm 行:每行两个数 M_iMi,M_jMj,1 \le M_i,~M_j\le N1≤Mi, Mj≤N,表示 M_iMi 和 M_jMj 具有亲戚关系。
接下来 pp 行:每行两个数 P_i,P_jPi,Pj,询问 P_iPi 和 P_jPj 是否具有亲戚关系。
输出格式
pp 行,每行一个 Yes
或 No
。表示第 ii 个询问的答案为“具有”或“不具有”亲戚关系。
输入输出样例
输入 #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,这个题目就是并查集的运用,就是找根。就还是纯纯的套模板,也没有别的需要注意的地方。
代码如下:
c
/*
*/
#include<stdio.h>
#include<math.h>
#include<string.h>
int a[10000];
int f(int x)
{
if(a[x]!=0)
return a[x]=f(a[x]);
return x;
}
int main()
{
int n,m,p;
scanf("%d%d%d",&n,&m,&p);
for(int i=0;i<m;i++)
{
int l,r;
scanf("%d%d",&l,&r);
if(f(l)!=f(r))
a[f(r)]=f(l);
}
for(int i=0;i<p;i++)
{
int l,r;
scanf("%d%d",&l,&r);
if(f(l)==f(r))
printf("Yes\n");
else
printf("No\n");
}
}
c++
#include<bits/stdc++.h>
using namespace std;
int n,m,q,f[10010],c,d,a,b;
int fd(int x)//找出x家的大佬 也就是二叉树的祖先节点
{
if(f[x]==x)//x是x的爸爸,简单的来说就是x没爸爸了
//他是家里最大的大佬,所以返回的x就是我们所求的祖先节点
return 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子集最大祖先的爸爸
return ;
}
int main()
{
scanf("%d%d%d",&n,&m,&q);
for(int i=1;i<=n;i++)
f[i]=i;
for(int i=1;i<=m;i++)
{
scanf("%d%d",&c,&d);
hb(c,d);
}
for(int i=1;i<=q;i++)
{
scanf("%d%d",&a,&b);
if(fd(a)==fd(b))//如果a所在子集的大佬[前面已经解释过了]和b所在子集的大佬一样,即可知a和b在同一个集合
printf("Yes\n");
else
printf("No\n");
}
return 0;
}
二,修复公路
题目背景
AA地区在地震过后,连接所有村庄的公路都造成了损坏而无法通车。政府派人修复这些公路。
题目描述
给出A地区的村庄数NN,和公路数MM,公路是双向的。并告诉你每条公路的连着哪两个村庄,并告诉你什么时候能修完这条公路。问最早什么时候任意两个村庄能够通车,即最早什么时候任意两条村庄都存在至少一条修复完成的道路(可以由多条公路连成一条道路)
输入格式
第11行两个正整数N,MN,M
下面MM行,每行33个正整数x, y, tx,y,t,告诉你这条公路连着x,yx,y两个村庄,在时间t时能修复完成这条公路。
输出格式
如果全部公路修复完毕仍然存在两个村庄无法通车,则输出-1−1,否则输出最早什么时候任意两个村庄能够通车。
输入输出样例
输入 #1复制
4 4 1 2 6 1 3 4 1 4 5 4 2 3
输出 #1复制
5
说明/提示
N \le 1000,M \le 100000N≤1000,M≤100000
x \le N,y \le N,t \le 100000x≤N,y≤N,t≤100000
分析:
1,对于这样的找关系的题目,我们通常使用并查集,就基本上都会使用并查集,将多颗子树合并为一颗大子树。
2,但是在合并查询前,我们需要根据时间来排序,排序的关键是因为我们要找到修完路的最小值。
3,排序的时候我使用的是快速排序,因为排序的个数不是很小,使用快排的话就不会时间超限。
4,排完序之后,我们使用并查集,但是这个里面我们只有合并,没有查询,在合并的时候,我们找到那个修路的时间的最大值,每新修一条路我们就加1。
5,我们可以发现的是,新修的路达到村庄的总数减1的时候,我们就可以结束查询,直接输出,如果找不到那个值,也就是说有任意两个村庄不能通车,我们就输出-1。
代码如下:
c
#include<stdio.h>
#include<math.h>
#include<string.h>
int p[100005];
struct tk
{
int l;
int r;
int t;
}a[100005];
int f(int x)
{
if(p[x]!=0)
return p[x]=f(p[x]);
return x;
}
void kp( int begin, int end){
if(begin > end)
return;
struct tk tmp = a[begin];
int i = begin;
int j = end;
while(i != j){
while(a[j].t >= tmp.t && j > i)
j--;
while(a[i].t <= tmp.t && j > i)
i++;
if(j > i){
struct tk t = a[i];
a[i] = a[j];
a[j] = t;
}
}
a[begin] = a[i];
a[i] = tmp;
kp( begin, i-1);
kp( i+1, end);
}
int main()
{
int n,m,k=0;
int max=0;
scanf("%d%d",&n,&m);
for(int i=0;i<m;i++)
{
scanf("%d%d%d",&a[i].l,&a[i].r,&a[i].t);
}
kp(0,m-1);
for(int i=0;i<m;i++)
{
if(f(a[i].l)!=f(a[i].r))
{
p[f(a[i].r)]=f(a[i].l);
k++;
max=max>a[i].t?max:a[i].t;
}
if(k==(n-1))
break;
}
if(k==(n-1))
printf("%d",max);
else
printf("-1");
}
c++
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
struct hh
{
int x,y,t;
}a[200000];
int f[200000],n,m;
int cmp(const hh &a,const hh &b){return a.t<b.t;}
int find(int x){return f[x]==x?x:(f[x]=find(f[x]));}
int getin()
{
int x=0;char ch=getchar();
while(ch<'0'||ch>'9')ch=getchar();
while(ch>='0'&&ch<='9')x=x*10+ch-48,ch=getchar();
return x;
}
int main()
{
n=getin(),m=getin();
if(n==1){cout<<0;return 0;}//其实并没有什么用的特判
for(int i=1;i<=m;i++)a[i].x=getin(),a[i].y=getin(),a[i].t=getin();
sort(a+1,a+m+1,cmp);
for(int i=1;i<=n;i++)f[i]=i;
for(int i=1;i<=m;i++)
{
int fx=find(a[i].x),fy=find(a[i].y);
if(fx!=fy)f[fx]=fy,n--;
if(n==1){cout<<a[i].t;return 0;}
}
cout<<-1<<endl;
return 0;
}
三,朋友
题目背景
小明在 A 公司工作,小红在 B 公司工作。
题目描述
这两个公司的员工有一个特点:一个公司的员工都是同性。
A 公司有 NN 名员工,其中有 PP 对朋友关系。B 公司有 MM 名员工,其中有 QQ 对朋友关系。朋友的朋友一定还是朋友。
每对朋友关系用两个整数 (X_i,Y_i)(Xi,Yi) 组成,表示朋友的编号分别为 X_i,Y_iXi,Yi。男人的编号是正数,女人的编号是负数。小明的编号是 11,小红的编号是 -1−1。
大家都知道,小明和小红是朋友,那么,请你写一个程序求出两公司之间,通过小明和小红认识的人最多一共能配成多少对情侣(包括他们自己)。
输入格式
输入的第一行,包含 44 个空格隔开的正整数 N,M,P,QN,M,P,Q。
之后 PP 行,每行两个正整数 X_i,Y_iXi,Yi。
之后 QQ 行,每行两个负整数 X_i,Y_iXi,Yi。
输出格式
输出一行一个正整数,表示通过小明和小红认识的人最多一共能配成多少对情侣(包括他们自己)。
输入输出样例
输入 #1复制
4 3 4 2 1 1 1 2 2 3 1 3 -1 -2 -3 -3
输出 #1复制
2
说明/提示
对于 30 \%30% 的数据,N,M \le 100N,M≤100,P,Q \le 200P,Q≤200;
对于 80 \%80% 的数据,N,M \le 4 \times 10^3N,M≤4×103,P,Q \le 10^4P,Q≤104;
对于 100 \%100% 的数据,N,M \le 10^4N,M≤104,P,Q \le 2 \times 10^4P,Q≤2×104。
分析如下:
1,这还是一个找关系的题目,所以还是使用并查集。
2,但是这个里面女人是负数,所以在合并的时候我们要取反。
3,在查找的时候,我们要先找到小明和小红,也就是为1的节点。
4,然后找到一个就加1 ,要注意的是两个公司分开找。
5,最后一步就是比较两个当中最小的那个,因为最终能组成多少对情侣是看人更少的那个。
代码如下:
c
#include<stdio.h>
#include<math.h>
#include<string.h>
int a[20005]={0},b[20005]={0};
int f(int x)
{
if(a[x]!=0)
return a[x]=f(a[x]);
return x;
}
int f1(int x)
{
if(b[x]!=0)
return b[x]=f1(b[x]);
return x;
}
int main()
{
int n,m,p,q;
int k=0,k1=0;
scanf("%d%d%d%d",&n,&m,&p,&q);
for(int i=0;i<p;i++)
{
int l,r;
scanf("%d%d",&l,&r);
if(f(l)!=f(r))
{
a[f(r)]=f(l);
}
}
for(int i=0;i<q;i++)
{
int l,r;
scanf("%d%d",&l,&r);
l=-l;
r=-r;
if(f1(l)!=f1(r))
{
b[f1(r)]=f1(l);
}
}
int r1,r2;
r1=f(1);
r2=f1(1);
for(int i=1;i<=n;i++)
{
//printf("%d ",f(i));
if(f(i)==r1)
k++;
}
for(int i=1;i<=m;i++)
{
//printf("%d ",f1(i));
if(f1(i)==r2)
k1++;
}
printf("%d",k<k1?k:k1);
}
c++
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cmath>
#include<cstring>
using namespace std;
int father[20001],father2[20001];//father用来保存男生,father2保存女生
int min(int a,int b)//返回较小数
{
return a<b?a:b;
}
int find_num(int fa[],int x)//查
{
if(fa[x]!=x)
fa[x]=find_num(fa,fa[x]);
return fa[x];
}
void combine_boy(int x,int y)//并(男生)
{
int r1,r2;
r1=find_num(father,x);
r2=find_num(father,y);
if(r1!=r2)//不是一个祖先
father[r2]=r1;
}
void combine_girl(int x,int y)并(女生)
{
int r1,r2;
r1=find_num(father2,x);
r2=find_num(father2,y);
if(r1!=r2)//不是一个祖先
father2[r2]=r1;
}
int main()
{
int printnum,n,m,p,q,x,y,rootboy,rootgirl,i,boy,girl;
scanf("%d %d %d %d",&n,&m,&p,&q);
for(i=1;i<=n;i++)
father[i]=i;//初始化
for(i=1;i<=m;i++)
father2[i]=i;//初始化
for(i=1;i<=p;i++)
{
scanf("%d %d",&x,&y);
combine_boy(x,y);//并入男生 father 中
}
for(i=1;i<=q;i++)
{
scanf("%d %d",&x,&y);
x=-x;//取反,不然会炸
y=-y;//同上,取反
combine_girl(x,y);//并入女生 father2 中
}
rootboy=find_num(father,1);//男1号
rootgirl=find_num(father2,1);//女1号
boy=girl=0;//总量都初始化为0
for(i=1;i<=n;i++)
if(find_num(father,i)==rootboy)//男1号认识
boy++;//保存
for(i=1;i<=m;i++)
if(find_num(father2,i)==rootgirl)//女一号认识
girl++;//保存
printnum=min(boy,girl);//较小数
printf("%d\n",printnum);//输出
return 0;
}//完美的结束
总结:
1,如果是找关系的题目,基本上就是用的并查集,主要还是要记住模板。
2,今天听学长讲了课,就更加明白了并查集的时间复杂度。其实主要就是一个路径压缩的问题。
3,路径压缩就是径直地找到父节点,就很大程度上的节省了时间,但是学长还讲了另外一个知识点,不是很明白,每天找两个题目做一些应该能明白一些。