2023.1.5(总结)

文章介绍了如何使用并查集解决三个不同的问题:判断亲戚关系、修复公路网络以实现任意两个村庄通车以及通过朋友关系配对情侣。每个问题都强调了并查集在找关系和路径压缩中的应用,并提供了C和C++的代码示例。同时,文章提到了路径压缩在提高并查集效率中的作用。
摘要由CSDN通过智能技术生成

一,亲戚

题目背景

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

题目描述

规定: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,路径压缩就是径直地找到父节点,就很大程度上的节省了时间,但是学长还讲了另外一个知识点,不是很明白,每天找两个题目做一些应该能明白一些。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值