并查集系列题解~

关于并查集

并查集是一个主要用来处理元素分组问题的十分简洁有效的数据结构。它管理一系列不相交的集合,每个集合中的元素都存在一定的关系。
它主要支持合并查询两种操作:

  1. 合并:把两个不相交的集合合并为一个集合;
  2. 查询:判断给定的两个元素是否在同一个集合中;

一个经典的应用:亲戚问题
大意就是在给定一些人的父子关系之后,让你判断两个人是不是有同一个祖先。

让我们换成一个更好理解的说法:
在一场很多学校一起举办的联欢会上,有很多来自不同学校的同学,现在告诉你XX和YY来自同一个学校,YY和ZZ来自同一个学校。。。然后让你判断XX和ZZ是否来自同一个学校。
用什么来判定呢?这就涉及到并查集的一个重要思想:用一个代表元素来代表整个集合

并查集的运作流程

图片来自知乎@Pecco

  1. 一开始,元素间的关系还没有给定,每个元素都是一个集合,代表元素就是他自己:
    引自知乎
  2. 假如1和3之间有关系,就把1和3弄到一起,代表元素为1(代表元素是谁并不重要):
    在这里插入图片描述
  3. 假如2和3又有关系,那么就要把2和3弄到一起,而此时3已经是1的人了,那么2也就成了1的人(~当然也可以让1成为2的人 ,代表是谁并不重要):
    在这里插入图片描述
  4. 假如4、5、6间也经过一番折腾形成了一个集合,代表元素为4:
    在这里插入图片描述
  5. 假如此时2和6有了关系,2是1的人,6是4的人,那么就要让1和4间产生关系:
    在这里插入图片描述
    至此,所有的元素都在一个集合里面;
    这种层层的父子关系不禁让我们想起了树,所以我们可以把图改成下面的样子。
    在这里插入图片描述

要想找到一个元素所在的集合的代表元素,就要一层层向上访问父节点,直至他的父节点为他自己。
这样我们可以写出一个简单的查询函数:

int Find(int x){
	if(x==fa[x]) return fa[x];
	return Find(fa[x]);
}

所谓的合并就是把一个元素的代表元素的父亲设成另一个元素的代表元素:

void Hebing(int x,int y){
	fa[Find(x)]=Find(y);
}

路径压缩并查集

普通的并查集查询的过程可以如图所示:
在这里插入图片描述
普通的并查集因为要一层层递归,每次查询时间效率都是O(n),如果数据不友好的话很容易GG。那我们能不能让递归的次数减少呢?
答案是肯定的:在第一次查询的时候就把直接这个元素的父亲设为他的代表元素。 一步到位,岂不美哉。
在这里插入图片描述
实现只需要在原来的基础上加一步:

int Find(int x){
	if(x!=fa[x]){
		fa[x]=Find(fa[x]);	//此时fa[x]的父节点已经经过递归设成了祖先,即代表元素
	}
	return fa[x];
}

还可以再简洁一点:

int Find(int x){
	return x==fa[x] ? x:(fa[x]=Find(fa[x])); //这里的括号不可少,因为赋值符号=的优先级没有三目?:高
}

完整的并查集代码(只要看查询和合并函数就行了):

#include<cstdio>
#include<iostream>
#include<algorithm>
using namespace std;
const int maxn=1e6;
int fa[maxn];
void Set(int n){    //fa数组初始化
    for(int i=1;i<=n;i++) fa[i]=i;
}
int Find(int x){
    return x==fa[x] ? x:(fa[x]=Find(fa[x]));
}
void Hebing(int x,int y){
    fa[Find(x)]=Find(y);
}
int main(){
    int n,m;scanf("%d%d",&n,&m);
    Set(n);
    for(int i=1;i<=m;i++){
        int a,b;scanf("%d%d",&a,&b);
        Hebing(a,b);
    }
    int t;scanf("%d",&t);
    for(int i=1;i<=t;i++){
        int a,b;scanf("%d%d",&a,&b);
        int pa=Find(a),pb=Find(b);
        if(pa==pb) printf("Yes\n");
        else printf("No\n");
    }
    return 0;
}

几道例题

1.亲戚问题

或许你并不知道,你的某个朋友是你的亲戚。他可能是你的曾祖父的外公的女婿的外甥女的表姐的孙子。如果能得到完整的家谱,判断两个人是否亲戚应该是可行的,但如果两个人的最近公共祖先与他们相隔好几代,使得家谱十分庞大,那么检验亲戚关系实非人力所能及。在这种情况下,最好的帮手就是计算机。为了将问题简化,你将得到一些亲戚关系的信息,如Marry和Tom是亲戚,Tom和Ben是亲戚,等等。从这些信息中,你可以推出Marry和Ben是亲戚。请写一个程序,对于我们的关于亲戚关系的提问,以最快的速度给出答案。

输入由两部分组成。

第一部分以N , M开始。N 为问题涉及的人的个数(1≤N≤20000)。这些人的编号为1,2,3,…, N。下面有M行(1≤M≤1 000 000),每行有两个数,表示已知和是亲戚。

第二部分以Q开始。以下Q行有Q个询问(1≤Q≤1 000 000),每行为,表示询问和是否为亲戚。

输出格式

对于每个询问,输出一行:若和为亲戚,则输出Yes,否则输出No。

样例:
10 7
2 4
5 7
1 3
8 9
1 2
5 6
2 3
3
3 4
7 10
8 9

Yes
No
Yes

一道裸的不能再裸的裸题,直接用我上边那个完整的代码就可以,就不侮辱大家的智商了。

2.奶酪

传送门:https://www.luogu.com.cn/problem/P3958

题目大意:
在一个立体空间内,给你一个底层和一个顶层,给你若干个球的半径及空间坐标,让你判断能否从最低面上的某一个点通过各个球到达顶层。

思路:
因为所谓的路径就是一个个空间中的球,我们很容易就能想到把一个个能相连的球合并到一个集合,形成一个总的路径。

那么如何判断能不能从底层到达顶层呢?直接记录所有球中最高和最低的显然不行,维护每个集合中最高和最低的那两个又太过麻烦。

所以我们要用两个额外的点来代表底层和顶层,这样我们在读进每个球的坐标时只需要判断一下它能否与底或顶相连就好了,如果能相连,那么就把这个球和底(或顶)并到一个集合。然后再判断与其他球的关系。

在所有的判断结束后,直接判断这个顶和底在不在一个集合当中就好了!

代码:

#include<cstdio>
#include<iostream>
#include<algorithm>
#include<cmath>
#include<cstring>
using namespace std;
const int maxn=1005;
int fa[maxn];
void Set(int n){
    for(int i=1;i<=n+2;i++)	//这里额外多初始化出两个点来代表底和顶
        fa[i]=i;
}
int Find(int x){
    return x==fa[x] ? x : (fa[x]=Find(fa[x]));
}
void Hebing(int a,int b){
    fa[Find(a)]=Find(b);
}
struct Dian{
    int x,y,z;
}a[maxn];
double Dist(Dian a,Dian b){
    return sqrt((long long)(b.x-a.x)*(b.x-a.x)+(long long)(b.y-a.y)*(b.y-a.y)+(long long)(b.z-a.z)*(b.z-a.z));
}
int main(){
    int T;scanf("%d",&T);
    for(int i=1;i<=T;i++){
        int n,h,r;scanf("%d%d%d",&n,&h,&r);
        Set(n);
        for(int i=1;i<=n;i++){
            scanf("%d%d%d",&a[i].x,&a[i].y,&a[i].z);
            //判断能否与底层或顶层相连
            if(a[i].z>=r*-1.0&&a[i].z<=r*1.0) Hebing(n+1,i);
            if(a[i].z<=h+r*1.0&&a[i].z>=h-r*1.0) Hebing(n+2,i);
        }
        //判断能否与其他球相连
        for(int i=1;i<=n;i++)
            for(int j=i+1;j<=n;j++){
                if(Dist(a[i],a[j])<=r*2.0) Hebing(i,j);
            }
        if(Find(n+1)==Find(n+2)) printf("Yes\n");
        else
            printf("No\n");
        memset(fa,0,sizeof(fa));
    }
}

3.团伙

传送门:https://www.luogu.com.cn/problem/P1892

思路:
题目大意应该就不用我说了,这道题用到的思想其实和上边的奶酪差不多,同样可以通过设虚点来很简单地解决。

通过题目我们知道一共存在两种关系:互为敌人;互为朋友。
麻烦之处就在于敌人的敌人就是我的朋友,我们可能很容易就想到把敌人和朋友两种关系分开来维护,可这样最后判断就很麻烦。

我们不妨想一下,自己和自己总不可能是朋友或敌人吧,如果我们把x+n看作x对应的一个虚点,那么这个虚点x+n一定不会与x产生关系,所以我们可以把这个x+n看作x永远的敌人,这样我们把互为敌人的两个点与对方对应的虚点放到一个集合里就可以了

代码:

#include<algorithm>
#include<cstdio>
#include<iostream>
#include<cstring>
using namespace std;
int fa[2010],m,n;
void Set(int n){
    for(int i=1;i<=2*n;i++)	//每个点都有自己的一个虚点,所以为2n
        fa[i]=i;
}
int Find(int x){
    return x==fa[x] ? x : (fa[x]=Find(fa[x]));
}
void Hebing(int x,int y){
    fa[Find(x)]=Find(y);
}
int main(){
    scanf("%d%d",&n,&m);
    Set(n);
    for(int i=1;i<=m;i++){
        char ch;
        int b,c;scanf(" %c %d%d",&ch,&b,&c);
        if(ch=='F') Hebing(b,c);
        else{
            Hebing(b+n,c);	//注意是把虚点的祖先设为实点,不要反了
            Hebing(c+n,b);	//否则可能会导致x与x+n产生联系
        }
    }
    int ans=0;
    for(int i=1;i<=n;i++)	//在实点中找出有多少个集合就好
        if(fa[i]==i) ans++;
    printf("%d\n",ans);
    return 0;
}

此题有个差不多的兄弟:关押罪犯https://www.luogu.com.cn/problem/P1525
思路是一样的,我把代码放这,有想法的可以看下:

#include<cstdio>
#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
const int maxn=20005,maxm=100005;
int fa[maxn*2];
struct Mad{
    int he,she,dis;
    bool operator <(const Mad a)const {
        return dis>a.dis;
    }
}mad[maxm];
void init(int n){
    for(int i=1;i<=2*n;i++)
        fa[i]=i;
}
int Find(int x){
    return x==fa[x] ? x : (fa[x]=Find(fa[x]));
}
void Hebing(int x,int y){
    fa[Find(x)]=Find(y);
}
int main(){
    int n,m;scanf("%d%d",&n,&m);
    init(n);
    for(int i=1;i<=m;i++){
        scanf("%d%d%d",&mad[i].he,&mad[i].she,&mad[i].dis);
    }
    sort(mad+1,mad+1+m);
    for(int i=1;i<=m;i++){
        int a=mad[i].he,b=mad[i].she;
        if(Find(a)!=Find(b)){
            Hebing(a+n,b);
            Hebing(b+n,a);
        }
        else{
            printf("%d\n",mad[i].dis);
            return 0;
        }     
    }
    printf("0\n");
    return 0;
}

4.打击罪犯

题目:
某个地区有n(n<=1000)个犯罪团伙,当地警方按照他们的危险程度由高到低给他们编号为1-n,他们有些团伙之间有直接联系,但是任意两个团伙都可以通过直接或间接的方式联系,这样这里就形成了一个庞大的犯罪集团。

犯罪集团的危险程度唯一由集团内的犯罪团伙数量确定,而与单个犯罪团伙的危险程度无关(该犯罪集团的危险程度为n)。

现在当地警方希望花尽量少的时间(即打击掉尽量少的团伙),使得庞大的犯罪集团分离成若干个较小的集团,并且他们中最大的一个的危险程度不超过n/2。为达到最好的效果,他们将按顺序打击掉编号1到k的犯罪团伙,请编程求出k的最小值。

输入格式:
第一行一个正整数n。

接下来的n行每行有若干个正整数,第一个整数表示该行除第一个外还有多少个整数,若第i行存在正整数k,表示i , k两个团伙可以直接联系。

输出格式:
一个正整数,为k的最小值

样例:
7
2 2 5
3 1 3 4
2 2 4
2 2 3
3 1 6 7
2 5 7
2 5 6

1

思路:
倒序并查集
这道题要求按顺序删去节点,可删除操作并不适合并查集,所以我们不妨先把所有节点读进来,再按倒序进行添加,直到危险程度超过n/2,这样我们添加了k个点,就相当于删去了前面的n-k个点
代码:

#include<cstdio>
#include<iostream>
#include<algorithm>
#include<cstring>
#include<cmath>
using namespace std;
const int maxn=1005;
int fa[maxn],mapp[maxn][maxn],danger[maxn];
void Set(int n){
    for(int i=1;i<=n;i++)   {fa[i]=i;danger[i]=1;}
}
int Find(int x){
    return x==fa[x] ? x : (fa[x]=Find(fa[x]));
}
void Hebing(int x,int y){
    fa[Find(x)]=Find(y);
    danger[y]+=danger[x];
}
int Solve(int n){
    int ans=0;
    for(int i=n;i>=1;i--){
       for(int j=1;j<=mapp[i][0];j++)
        if(mapp[i][j]>i){
           int r1=Find(i),r2=Find(mapp[i][j]);
           if(r1!=r2){
               fa[r2]=r1;
               danger[r1]+=danger[r2];
               if(danger[i]>(n/2)){
                   return i;
                }
            }
        }
    }
    return 0;
}
int main(){
    int n;scanf("%d",&n);
    Set(n);
    memset(mapp,0,sizeof(mapp));
    for(int i=1;i<=n;i++){
        scanf("%d",&mapp[i][0]);
        for(int j=1;j<=mapp[i][0];j++){
            scanf("%d",&mapp[i][j]);
        }
    }
    int ans=Solve(n);
    printf("%d\n",ans);
    return 0;
}
posted @ 2020-03-06 22:38  zfio  阅读(229)  评论(0编辑  收藏  举报