10分钟带你学会并查集

引言

无所不在的宗教

​ 世界上宗教何其多。假设你对自己学校的同学总共有多少种宗教信仰很感兴趣。已知学校有n个学生,但是建议你不要直接问大家的宗教信仰,这不是一种礼貌的做法。还有另外一个方法是问m对同学,问他们是否信仰同一宗教。根据这些信息,聪明的你如何计算出学校最多有多少种宗教信仰?
在这里插入图片描述

概述

Union-Find Set

一种树型数据结构,用于处理一些不相交集合(Disjoint Sets)的合并及查询问题

在使用中通常以森林来表示

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AkiExxqi-1588904773651)(C:\Users\ASUS\AppData\Roaming\Typora\typora-user-images\image-20200508094015318.png)]

初始化:将编号分别为1…N的N个元素划分为N个不相交集合,在每个集合中,选择其中某个元素代表所在集合(通常用根结点来作为代表)

常见操作:

​ •**合并**两个集合

​ •**查找**某元素属于哪个集合

​ •判断两个元素是否属于同一个集合

基本操作

判断两个元素是否属于同一集合,只要判断它们所在集合的根结点是否相同即可

合并两个集合,将一个集合的根结点作为另一个集合的根结点

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tsm8jg3M-1588904773652)(C:\Users\ASUS\AppData\Roaming\Typora\typora-user-images\image-20200508094227642.png)]

基本函数

make_set(x):把每一个元素初始化为一个集合【将每个元素的父结点初始化为自己】

find_set(x):查找一个元素所在的集合。在执行查找操作时,要沿着父结点一直找下去,直到找到根结点为止

union_set(x, y):利用find_set()找到两个集合的根结点,将一个集合的根结点指向另一个集合的根结点

启发式策略优化

按秩合并

•秩(Rank):结点所在树的高度,只有一个根结点的树的秩为0

union_set(x,y)时按秩合并合并时将秩相对较小的树合并到秩相对较大的树中,这样合并之后树的高度会相对较小

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aLXsKL5A-1588904773653)(C:\Users\ASUS\AppData\Roaming\Typora\typora-user-images\image-20200508094604382.png)]

路径压缩

•find_set(x)时路径压缩,使用“递推”找到根结点后,在“回溯”时将路径上所有结点的父结点都直接指向根结点,以后再次调用find_set(x)时其时间复杂度为O(1)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jXZUepGi-1588904773655)(C:\Users\ASUS\AppData\Roaming\Typora\typora-user-images\image-20200508094640148.png)]

#include <stdio.h>

const int MAXN = 100; /*结点数目*/
int pa[MAXN];    /*pa[x]表示x的父结点*/
int rank[MAXN];    /*rank[x]是x的高度的一个上界*/

/*创建一个单元集*/
void make_set(int x)
{
    pa[x] = x;//自己为自己的父节点,
    rank[x] = 0;//初始只有自己一个节点,秩为0
}

/*带路径压缩的查找*/
int find_set(int x)
{
    if(x != pa[x])//自己的父节点不是自己,就继续找,知道找到自己是自己父亲
        pa[x] = find_set(p[x]); //将所有结点的父结点回溯为根结点
    return pa[x];
}
/*按秩合并x,y所在的集合*/
void union_set(int x, int y)
{
    x = find_set(x); //返回x的根结点
    y = find_set(y); //返回y的根结点
    if(rank[x] > rank[y])/*让rank比较高的作为父结点*/
        pa[y] = x;//x的秩高,x做父亲,y做x的儿子
    else 
    {
        pa[x] = y;//否则,x的父亲就是y
        
        //秩相同,其实谁做父亲都一样,但是这里已经先让y父亲了,y的秩就会加1
        if(rank[x] == rank[y])
            rank[y]++;
    }
}

实例:PTA 朋友圈(25 分)

朋友圈(25 分)
某学校有N个学生,形成M个俱乐部。每个俱乐部里的学生有着一定相似的兴趣爱好,形成一个朋友圈。一个学生可以同时属于若干个不同的俱乐部。根据“我的朋友的朋友也是我的朋友”这个推论可以得出,如果A和B是朋友,且B和C是朋友,则A和C也是朋友。请编写程序计算最大朋友圈中有多少人。

输入格式:
输入的第一行包含两个正整数N(≤30000)和M(≤1000),分别代表学校的学生总数和俱乐部的个数。后面的M行每行按以下格式给出1个俱乐部的信息,其中学生从1~N编号:

第i个俱乐部的人数Mi(空格)学生1(空格)学生2 … 学生Mi

输出格式:
输出给出一个整数,表示在最大朋友圈中有多少人。

输入样例:

7 4
3 1 2 3
2 1 4
3 5 6 7
1 6

输出样例:

4

思路:一看题目就发现是个并查集的板子,有着稍微的改动,在合并完所有的节点后,遍历所有节点的父节点,并记录该父节点当前做父亲数,并比较其当前是不是儿子最多的父亲,把最大的儿子数记录下来,输出即可。

上代码

#include "bits/stdc++.h"
using namespace std;
const int maxx = 30005;
int father[maxx];
int ran[maxx];
int isRoot[maxx];

void make_set(int x)
{
    father[x] = x;
    ran[x] = 0;
}
int find(int x)
{ //压缩路径的查找
    if (father[x] == x)
    { //如果自己的父节点是自己,那么x 结点就是根结点
        return x;
    }
    return find(father[x]); // 返回父结点的根结点
}
void merge(int x, int y)
{
    x = find(x);
    y = find(y);
    if (x == y)
        return;
    if (ran[x] > ran[y])
    {
        father[y] = x;
    }
    else
    {
        father[x] = y;
        if (ran[x] == ran[y])
        {
            ran[y]++;
        }
    }
}

int main()
{
    int n, m;
    cin >> n >> m;
    for (int i = 0; i <= n; i++)
    {
        make_set(i);
    }
    int x, y;
    int k;
    int s = 1;

    for (int i = 0; i < m; i++)
    {
        cin >> k >> x;
        for (int j = 0; j < k - 1; j++)
        {
            cin >> y;
            merge(x, y);
        }
    }
    int ans = 0;
    for (int i = 1; i <= n; i++)
    {
        int f = find(i);
        isRoot[f]++;
        ans = max(ans, isRoot[f]);
    }
    cout << ans << endl;
}
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

°PJ想做前端攻城狮

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值