并查集入门(持续更新!)

嘿大家,我又回来了,今天我们来介绍一下并查集,它是一种很高效的算法,值得学习


现在,我们先看看一道题,简单思考一下。

引入

洛谷P3367

如题,现在有一个并查集,你需要完成合并和查询操作。

输入输出格式

输入格式:
第一行包含两个整数N、M,表示共有N个元素和M个操作。

接下来M行,每行包含三个整数Zi、Xi、Yi

当Zi=1时,将Xi与Yi所在的集合合并

当Zi=2时,输出Xi与Yi是否在同一集合内,是的话输出Y;否则话输出N

输出格式:
如上,对于每一个Zi=2的操作,都有一行输出,每行包含一个大写字母,为Y或者N

输入输出样例

输入样例#1:
4 7
2 1 2
1 1 2
2 1 2
1 3 4
2 1 4
1 2 3
2 1 4
输出样例#1:
N
Y
N
Y
说明

时空限制:1000ms,128M

数据规模:

对于30%的数据,N<=10,M<=20;

对于70%的数据,N<=100,M<=1000;

对于100%的数据,N<=10000,M<=200000。


一、思考

看完题目,你可能一头雾水,也可能灵光乍现,我们就先来了解一下什么是并查集:

并查集被很多OIer认为是最简洁而优雅的数据结构之一,主要用于解决一些元素分组的问题。它管理一系列不相交的集合,并支持两种操作:

合并(Union):把两个不相交的集合合并为一个集合。
查询(Find):查询两个元素是否在同一个集合中。

是的,我们可以把并查集理解成是一种集合数据库,里面记录了集合之间的关系。这样说未免让人摸不着头,我们就先来引入一个实际问题

现在有5个人,名叫1,2,3,4,5,他们有各自的老大(老大可以是自己),现在规定,1,2,3共用一个老大,4,5共用一个老大(或者说是共用一个组),我们可以如何表示呢?

是的,我们当然可以把1,2,3存入一个数组叫a,把4,5存入一个数组叫b,但这样未免慢了些,有什么快捷的方法吗?有的,并查集

我们先设置一个数组名叫p,如果要实现用一条数组存储这样的关系,我们可以规定1,2,3的老大是1,4和5的老大是4,那么如果将p的下标定义为人员,内部存的数据定为它的老大,是不是就可以得到这个关系了呢?没错,得到的p就是[1,1,1,4,4](下标从1到5)。

我们执行查询的时候,比如说我要知道3的老大是谁,我只需要调用p[3],里面存的就是集合头子。

好的,现在我们已经介绍了查询了,如何合并呢?聪明的你一定想到了,如果我要合并上述第一个和第二个集合,我只需要把第二个集合的内容都改成第一个的头子即可。

二、具体实现

这里find()函数我们要单独拎出来讲讲:

int f[10005];
int find(int k){
    
    
    return f[k]==k?k:f[k] = find(f[k]);
}

我们注意到find是这样运作的:
1.如果找到的f数组内容就是本身查询的k值(自己就是自己的头子,相当于上述例子p[1]=1),则直接返回k值
2.如果不是,就要去更深层挖掘,也就是我们写的递归程序,令f[k] = find(f[k]),一方面在寻找k的总头子(头子也可能有头子),一方面也直接把找到的值赋给了f[k],我们管这叫路径压缩
(路径压缩优化后,并查集的时间复杂度已经比较低了,绝大多数不相交集合的合并查询问题都能够解决)

代码如下(示例):

#include<bits/stdc++.h>
using namespace std;
int f[10005];
int find(int k){
    
    
    if(f[k]==k)return k;
    else return f[k] = find(f[k]);
    
}
int main(){
    
    
     int n,m;
    cin >> n >> m;
    for(int i=1;i<=n;i++)f[i] = i;
    int type,a,b;
    while(m--)
    {
    
    
        scanf("%d%d%d",&type,&a,&b);
        if(type==1){
    
    
            f[find(b)] = find(a);
        }else
        {
    
    
                if(find(b)==find(a))
                cout << "Y" << endl;
            else
                cout << "N" << endl;
        }
        
    }
    
    
    
    return 0;
}

三、拓展题

ZCMU-1435
盟国
Time Limit: 3 Sec Memory Limit: 128 MB
Submit: 592 Solved: 143
世界上存在着N个国家,简单起见,编号从0~N-1,假如a国和b国是盟国,b国和c国是盟国,那么a国和c国也是盟国。另外每个国家都有权宣布退盟(注意,退盟后还可以再结盟)。

定义下面两个操作:

“M X Y” :X国和Y国结盟 (如果X与Z结盟,Y与Z结盟,那么X与Y也自动结盟).

“S X” :X国宣布退盟 (如果X与Z结盟,Y与Z结盟,Z退盟,那么X与Y还是联盟).

Input
多组case。

每组case输入一个N和M (1 ≤ N ≤ 100000 , 1 ≤ M ≤ 1000000),N是国家数,M是操作数。

接下来输入M行操作

当N=0,M=0时,结束输入

Output
对每组case输出最终有多少个联盟(如果一个国家不与任何国家联盟,它也算一个独立的联盟),格式见样例。

Sample Input

5 6
M 0 1
M 1 2
M 1 3
S 1
M 1 2
S 3
3 1
M 1 2
0 0

Sample Output

Case #1: 3
Case #2: 2

拓展题AC代码

在这里我就先上代码了,有能力的同学可以先看,看不懂的话下面我会解释

#include<bits/stdc++.h>
using namespace std;
const int maxn = 2e6+5;
int f[maxn],ff[maxn],vis[10000];
int find(int k){
    
    
    if(f[k]==k)return k;
    else return f[k] = find(f[k]);

}
int main(){
    
    
       int m,n,ca = 1;
    while(~scanf("%d%d",&n,&m))
    {
    
    
        int pos = n;
        if(m==0&&n==0)break;
        for(int i=0;i<maxn;i++)f[i] = ff[i] = i;
        while(m--){
    
    
            char str[2];
            int a,b;
            scanf("%s %d",str,&a);
            if(str[0]=='M'){
    
    
                scanf("%d",&b);
                int fa = find(ff[a]);
                int fb = find(ff[b]);
                if(fa!=fb)f[fa] = fb;
             }else if(str[0]=='S')
             {
    
    
                 ff[a] = pos;
                 f[pos] = pos;
                 pos++;
             }
        }
        set<int>s;
        for(int i=0;i<n;i++)
        s.insert(find(ff[i]));
        
        printf("Case #%d: %d\n",ca++,s.size());
        
    }
    return 0;
    }

思路讲解

在这里插入图片描述

我们可以看到,现在这是一个关于数集合的树,可以得到一个关系,2,3,4的老大是1,5和6的老大是4,如果我们现在要实现删除的操作,我们该怎么做呢?

删除操作比合并操作稍微复杂一点。
首先,我们已经用f数组保存了原来结点间的关系(2,3,4的老大是1,5和6的老大是4),现在我们想要让4从这个关系图中剔除,我们就需要新建一个数组ff[],里面存储的就是最新的关系

也就是说,删除操作要这样执行

 ff[a] = pos;
 f[pos] = pos;
 pos++;
 //pos就是从n开始增大,因为题目数据范围是0 to n-1

我们首先让ff内的a指向pos,(pos已经在原有数据范围之外了),我们可以理解为a这个数据被流放了,同时在f数组执行f[pos] = pos,使得让pos的老大就是本身,这样在find运行时可以作为一个独立的个体

我们用一个形象的比喻:
澳大利亚是英国殖民时期的罪犯流放地,但刚开始并没有罪犯对吧。现在,英国本来有6个小混混,他们之间的关系如上图,现在4号小混混(小头子)放下了杀人罪,被法律规定要流放到澳大利亚,成为那里第一个犯人,于是,执行官(也就是ff数组把ff[4]=7标记了)把4送到了7这个地方流放了,也就是新开辟的澳大利亚区域,但是,尽管4号小头子被流放了,它两个手下5和6的总头子仍然是1,为了以后警察能通过f数组仍然找到最大的头子1,f数组原来数据范围内的数据是不会有变化的,只是会添加一条 f[pos] = pos,意味着如果澳大利亚警察找4的头子,那还是会找到4本身的。

接下来讲讲稍微简单的合并操作

int fa = find(ff[a]);
int fb = find(ff[b]);
if(fa!=fb)f[fa] = fb;

我们可以注意到,fa是在ff数组(最新的犯罪记录,用了上面的例子了)里用find函数查找a的头子(注意:find函数用的还是f数组,原因上面已经讲过了),fb同理,如果头子相同,那么不必执行操作,如果不同,就要在f数组里面记录a的头子就是b的头子(其实反过来也是对的)

总结

今天介绍了了并查集的简单入门,查询,合并和删改,并做到了一些应用,后面那个例子讲的可能不是非常恰当,但希望能帮助你理解。
如有出错,希望在评论区告知,希望能给你们派上用场。

猜你喜欢

转载自blog.csdn.net/DAVID3A/article/details/114274389

相关文章