首页 > 其他分享 >并查集

并查集

时间:2024-03-17 11:13:16浏览次数:17  
标签:int 查集 合并 fa maxn find

并查集

并查集是一种可以动态维护若干个不重叠集合,并且支持合并与查询的数据结构,主要用于处理不相交集合的的合并关系。

为了具体实现并查集这种数据结构,首先我们需要定义集合的表示方法。在并查集中,我们采用"代表元"法,即为每一个集合选择一个固定的元素,作为整个集合的代表。

其次,我们需要定义归属关系的表示方法。

第一种思路:维护一个数组 \(f\) 用 \(f[x]\) 来表示元素 \(x\) 所在集合的“代表”。这可以快速的查询元素 \(x\) 的所属集合,但在修改时需修改大量的 \(f\) 值,效率极低。

第二种思路:使用一个树形结构存储存储每个集合,树上的每个点都是一个元素,树根是集合的代表元素。 整个并查集实际上是一个森林。我们仍可维护一个数组 \(fa[]\) 来模拟这个森林,用 \(fa[x]\) 表示 \(x\) 的父节点。特别的,令树根的 \(fa\) 值为它本身。

如此一来,在合并两个集合时,只需令其中一个数根成为另一个树的子节点。不过在查询时,需从该元素开始不断递归的寻找集合的代表元素。为了提高查询的效率,并查集引入了路径压缩与按秩合并两种思想,下文中将会提到。

本文将包括:

目录

1.1 并查集的基础操作

主要包括1.初始化 2.查找 3.合并 4.统计

(1.初始化

定义一个数组 \(fa[]\) , \(fa[i]\) 是 \(i\) 元素所指向的父节点,开始时每个元素都是一个集合(即都指向自己)

int N                               //N为元素个数
int init_set(int N){
    for(int i=1;i<=N;i++)
        fa[i]=i;                    //初始化指向自己 
}

(2.查找

查找元素的集合的“代表”,是一个递归的过程,直到元素的值与自己相同时(即当 \(fa[i]==i\) 时),这里给出初级的查找函数,后将用路径压缩优化。

int x;                         //将要查找的元素x
int find(int x){
    if(fa[x]==x) 
        return x;              //递归出口,即元素与其父节点相等
    return find(fa[x]);        //否则递归向上寻找根节点
}

(3.合并

合并两元素的集合,即将一棵树变为另一棵树的子树,这里给出初级的合并函数,后将用按秩合并优化。

int x,y;                         //将要合并的元素x,y
void join(int x,int y){          
    int f1=find(x),f2=find(y);   //取出两棵树的根节点
    if(f1!=f2)                   //若不在同一集合
        fa[f1]=f2;               //则将x所在的树变为y所在的树的子树
}

(4.统计

由于根节点的 \(fa\) 数组指向自己,故当有 \(n\) 个 \(i\) 满足 \(fa[i]==i\) 时,并查集便有 \(i\) 个。

int N;                       //共有N个元素
int sum(){
    int num;                 //共有num个集合
    for(int i=1;i<=N;i++)
         if(fa[i]==i)        //满足条件
             num++;
    return num;
}

1.2合并的优化(按秩合并)

首先,我们要了解秩的定义。

秩有两种定义方式,一种为树的高度,可用来优化合并(在未进行路径压缩时),另一种定义在路径压缩后给出。

为何以树的高度为秩进行合并优化,下面给出正确性证明:


在下图的两棵树中,选择将⑥树(即矮树)合并到①树(高树上)上,对于查询操作的优化是显然的(在查找⑥的根节点时,省去了递归长长的子树)

下面给出优化后的合并代码:

int h[N];                        //树的度数组
void init_set(){
    for(int i=1;i<=N;i++){
        fa[i]=i;
        h[i]=0;                  //初始化高度为零
    }
}
void join(int x,int y){
    int f1=find(x),f2=find(y);
    if(h[f1]==h[f2]){
        h[f1]++;                 //合并,树的高度加一
        fa[f2]=f1;
    }
    else{
        if(h[f1]<h[f2]){
            fa[f1]=f2;           //矮树合并到高树上,高树高度不变
        }
        else
            fa[f2]=f1;
    }
}

1.3 查询的优化(路径压缩)

实际上,在并查集的一棵树中,我们只关心其对应的根节点是什么,并不关心其具体形态,这意味着下图中两树是等价的。

因此,我们可以在每次执行 \(find\) 操作时把访问过的节点(即查询过的祖先元素)都直接指向树根,即把上图中左树变为右树 ,即为路径压缩优化,以下给出使用路径压缩的查询操作代码:

int x;
int find(int x){
    if(fa[x]==x) return x;
    return fa[x]=find(fa[x]);    //直接指向根节点
}

可以看出,路径压缩优化对并查集的速度优化是巨大的(因为合并操作也需要用到查询操作)其省去了巨量的重复递归,使得均摊复杂度来到了 \(O(\log n)\) 。

在一般题目中,只使用路径压缩优化的并查集就已经可以。

1.4 并查集的极限优化(个人认为)

在使用了路径压缩优化后,树的高度都应为2,那么使用以树的高度为秩的按秩合并便不在可行,于是在此给出秩的第二种定义,即为集合的大小,同时,当秩被定义为集合大小时,“按秩合并”也并被称作“启发式合并”。

此时按秩合并的可行性证明:

对于上图中的两棵树,我们可以明显看出当把右树(即小树)合并为左树(即大树)的子树时,复杂度更低(因为避免了 \(fa[]\) 的大量修改)。

下面给出模版:

int N,x,m,n;
int fa[maxn],sum[maxn];
void init_set(){
    for(int i=1;i<=maxn){
        fa[i]=i;
        sum[i]=1;
    }
}
int find(int x){
    if(fa[x]==x) return x;
    return fa[x]=find(fa[x]);
}
void join(int x,int y){
    x=find(x),y=find(y);
    if(sum[x]>=sum[y]){
        sum[x]+=sum[y];
        fa[y]=x;
    }
    else{
        fa[x]=y;
        sum[y]+=sum[x];
    }
}

由于前面已经解释过,同理可得,本处不再打注释。

对于此模版来说,复杂度可进一步降低到 \(O(a(N))\) ,其中 \(a(N)\) 可近似为常数。

1.5 “边带权”的并查集

普通的并查集应用中,元素之间只有归属关系,而没有权值。如果在元素之间加上权值,应用将会更广泛。

(1. 带权值的路径压缩优化

只需累加权值 \(d[]\) 即可(具体情况具体分析)。

int d[maxn];
int find(int x){
    if(x!=fa[x]){
        int f1=fa[x];          //记录父节点
        fa[x]=find(fa[x]);     //路径压缩,递归至根节点
        d[x]+=d[f1];           //权值更新为x至根节点的权值
    } 
    return fa[x];
}

(2. 带权值的合并

把 \(x\) 与 \(y\) 进行合并,即在 \(x\) 的根 \(fx\) 与 \(y\) 的根 \(fy\) 之间增加权值即可。

int num;
void join(int x,int y,int num){
    x=find(x),y=find(y);
    if(x!=y){
        fa[x]=y;                  //将x的树作为y的子树
        d[x]=num;                 //在fx与fy之间添加权值
    }
}

为什么不写按秩合并的优化版?因为要留给大家思考的空间 \(OwO\) 。好吧,我懒得写, \(QAQ\) 。

1.6 拓展域的并查集

对于复杂的并查集问题,单个的并查集无法解决,对于这样的问题,我们一般需要多个并查集互相推出,即将一个子节点拆为多个子节点,运用子节点之间的关系相互推出。

对于其具体实现方法,以下面这个题为例:

P2024 [NOI2001] 食物链

分析,对与本题,我们可以将动物 \(i\) 分为 \(i_{self}\) , \(i_{eat}\) 与 \(i_{enemy}\) 三个子节点,我们定义 \([1,maxn]\) 为 \(self\) 域, \((maxn,2*maxn]\) 为 \(eat\) 域, \((2*maxn,3*maxn]\) 为 \(enemy\) 域,即同时维护三个分别为 \(self\) , \(eat\) , \(enemy\) 的并查集,利用并查集之间的关系推出是否是假话。

在处理每句话之前,都要检查其真假性:

有两种信息与“ \(x\) 与 \(y\) 是同类”矛盾:

  1. \(x_{eat}\) 与 \(y_{self}\) 在同一集合 (即 \(x\) 吃 \(y\) )

  2. \(x_{self}\) 与 \(y_{eat}\) 在同一集合 (即 \(y\) 吃 \(x\) )

​ 若与上述任何一条都不矛盾,则:

join(x,y);
join(maxn+x,maxn+y);
join(2*maxn+x,2*maxn+y);   //注意域的范围 !!!

有两种信息与“ \(x\) 吃 \(y\) ”矛盾:

  1. \(x_{self}\) 与 \(y_{self}\) 在同一集合 (即 \(x\) 与 \(y\) 是同类)

  2. \(x_{self}\) 与 \(y_{eat}\) 在同一集合 (即 \(y\) 吃 \(x\) )

​ 若与上述任何一条都不矛盾,则:

join(maxn+x,y);
join(2*maxn+x,maxn+y);
join(x,2*maxn+y);

完整 ACcode :

#include <bits/stdc++.h>
#define seq(q, w, e) for (int q = w; q <= e; q++)  //个人陋习
using namespace std;
const int maxn = 50005;
int fa[3 * maxn+10];                               //记得比3*maxn大,否则会爆  
void init_set()
{
    seq(i, 1, 3 * maxn)
    {
        fa[i] = i;
    }
}
int find(int x)
{
    if (x == fa[x])
        return x;
    return fa[x] = find(fa[x]);
}
void join(int x, int y)
{
    int f1 = find(x), f2 = find(y);
    if (f1 != f2)
    {
        fa[f1] = f2;
    }
}
int n, k,ans;
signed main()
{
    scanf("%d %d", &n, &k);
    init_set();
    int op = 0,x=0,y=0;
    while (k--)
    {
        scanf("%d %d %d", &op,&x,&y);
        if(x>n||y>n){
            ans++;
            continue;
        }
        if(op==1){
            if(find(maxn+x)==find(y)||find(x)==find(maxn+y)){
                ans++;
                continue;
            }
            else{
                join( x, y);
                join(maxn + x, maxn+ y);
                join( 2*maxn + x, 2* maxn + y);
            }
        }
        else{
            if(find(x)==find(y)||find(x)==find(maxn+y)){
                ans++;
                continue;
            }
            else{
                join(maxn+x, y);
                join( x, 2 *maxn+ y);
                join(2 *maxn+ x,  maxn+y);
            }
        }
    }
    printf("%d", ans);
    return 0;
}

1.7 习题

模版题:

标签:int,查集,合并,fa,maxn,find
From: https://www.cnblogs.com/adsd45666/p/18078317

相关文章

  • 并查集
    模板题:https://www.luogu.com.cn/problem/P1551题解:#include<bits/stdc++.h>usingnamespacestd;intn,m,p;intfa[5050];intfind(intx){returnfa[x]==x?x:fa[x]=find(fa[x]);}intquery(intx,inty){intfx=find(x),fy=fi......
  • 并查集
    并查集是一种可以动态维护若干个不重叠的集合,并支持合并与查询的数据结构。例:P1551亲戚题目描述:如果\(x\)和\(y\)是亲戚,\(y\)和\(z\)是亲戚,那么\(x\)和\(z\)也是亲戚。如果\(x\)和\(y\)是亲戚,那么\(x\)的亲戚都是\(y\)的亲戚,\(y\)的亲戚也都是\(x\)的......
  • 并查集
    并查集目录并查集(1.概念:(2.详解Q1:如何表示不同的家族ans1:Q2:如何将两个人归到同一个家族中ans2:CODE:PS:(1.概念:处理不相交可合并的集合关系的数据结构叫做并查集;(2.详解例题:P1551亲戚一道并查集的板子题我们来详细解一下:Q1:如何表示不同的家族ans1:即如何表示不同的集合;再......
  • 并查集解mex_cf932_B. Informatics in MAC
    目录题目概述思路想法参考代码做题反思题目概述原题参考:B.InformaticsinMAC给出一个长度为n的数组,问是否可以将其分为k个(k>1)mex相同的数组,如果可以的话,作出一种划分思路想法假设一个数组可以被分为k(k>2)个区间,每个区间的mex相同,那么可以确定的是,该数组中一定不存在mex这......
  • 一类链式并查集问题
    链接:https://ac.nowcoder.com/acm/contest/69510/G来源:牛客网你在一个星球上,外星人amiloac想让你管理一条河流,该河流有\(x\)段,每两段之间有一个挡板隔开,每一段都有各自的颜色\(a\)。你需要管理\(q\)天,每一天你需要做以下的一种操作。\(1\l\r\)将第\(l\)至\(r\)段河流的所有未......
  • 带权并查集板子
    以一道区间和查询来说明板子如何使用1.merge的时候只需要维护两个根节点的距离,利用的是合并时题目给的信息2.find的时候更新维护是子节点到根的距离3.需要加一个查询函数,因为距离数组是开在结构体内部的。题目描述对于一个长度为\(N\)的整数数列\(A_{1},A_{2},\cdotsA_......
  • 程序自动分析—并查集
    Description在实现程序自动分析的过程中,常常需要判定一些约束条件是否能被同时满足。考虑一个约束满足问题的简化版本:假设x1,x2,x3,…代表程序中出现的变量,给定n个形如xi=xj或xi≠xj的变量相等/不等的约束条件,请判定是否可以分别为每一个变量赋予恰当的值,使得上述所有约束条件......
  • 信息传递(题解)[并查集]
    题目题目描述有n个同学(编号为1到n)正在玩一个信息传递的游戏。在游戏里每人都有一个固定的信息传递对象,其中,编号为i的同学的信息传递对象是编号为Ti同学。游戏开始时,每人都只知道自己的生日。之后每一轮中,所有人会同时将自己当前所知的生日信息告诉各自的信息传递对象(注意:可能有......
  • 并查集(模板介绍+路径压缩)
    并查集(模板介绍+路径压缩)题面P3367并查集题目描述如题,现在有一个并查集,你需要完成合并和查询操作。第一行包含两个整数N,M,表示共有N个元素和M个操作。接下来M行,每行包含三个整数Z,X,Y。当Z=1时,将X与Y所在的集合合并。当Z=2时,输出Z与Y是否在同一集合内,是的输出Y;否则输出N......
  • CF776D(并查集思想)
    难度1em还是一道比较套路的题目。观察发现,如果当\(r_{i}=1\)时他的两把钥匙状态是相同的,当\(r_{i}=0\)时他的两把钥匙状态是不同的,对于这种相同不同的问题可以考虑并差集,状态一样就一样的并在一起,否则就把不一样的并在一起所以以后看见这些问题(a=b+k,a=!b,a=b)都可以用带权并查......