并查集 笔记

本文详细介绍了并查集的概念、基本操作以及两种常见的优化方法——路径压缩和按秩合并。通过实例展示了如何使用并查集解决实际问题,如洛谷2024 [NOI2001]食物链问题,解释了如何处理不同种类的关系,以及在处理过程中如何判断和处理矛盾情况。

before 正片

讲之前说一下,并查集可有用了!什么题都能用并查集乱搞!

(最基础的)正片

并查集是啥?

  1. 定义:并查集(disjoint-set data structure)分为3个部分,并,查,集。然后我们讲的顺序是集,查,并。

2.珂以干嘛:

2.1 初级:珂以维护在一块的关系。打个比方,我们珂以快速求两个人是不是在同一个班里面。同时,我们也珂以记录集合里面的信息(比如有多少元素,最大最小的元素,等)。

2.2 升级:珂以维护种类的关系(后面会讲)。举个栗子,就是敌人和朋友的关系,并且敌人的敌人是朋友。

那说完这些,就要讲实现了。

首先是“集”的实现。通常,我们把它用一个森林(也就是很多个树结构)来表示。但是,我们维护的很简单,只要知道每个节点的父亲是谁就珂以了。如果一个节点的父亲是自己,说明它是祖先。初始状态每个节点都是祖先(这个不加就炸了)。
然后是这一步的代码实现(我喜欢面向对象):

class DSU
//DSU是并查集的英文名简写
{
    public:
        #define N 1001000
        int Father[N];
        void Init()
        {
            for(int i=0;i<N;i++)
            {
                Father[i]=i;
            }
        }
};

然后并查集就学了13\frac{1}{3}31了。

接下来是如何“查”。

这个很显然,写一个递归就珂以了。不断的找,如果是祖先,就返回自己,否则就去问自己的爸爸祖先是谁,然后爸爸再问爸爸的爸爸,爸爸的爸爸再问爸爸的爸爸的爸爸的爸爸…直到没有爸爸(自己是祖先)。
代码:

//写在类里面
int Find(int x)
{
    if (x==Father[x])
    {
        return x;
    }
    else
    {
        return Find(Father[x]);
    }
}

然后并查集就学了23\frac{2}{3}32了。

接下来是“并”。这也同样很简单,如果我们要并xxxyyy,只要把xxx的祖先的爸爸设置成yyy的祖先,xxx就并到yyy上了。代码:

//这个也写在类里面
void Merge(int x,int y)
{
    int ax=Find(x),ay=Find(y);
    Father[ax]=ay;
}

这样并查集就学了33\frac{3}{3}33了。

是不是很水?但这还不够,后面还有优化(依然很水)。

正片的优化

优化1. 路径压缩

我们会发现,在维护并查集的过程中,其实中间经过哪些父亲,并不是很重要,只要知道谁是祖先就珂以了,所以在找的过程中,直接把爸爸的爸爸设置成祖先即可(这样快了好多,理论上只要有了这个优化,不加优化2很多题都珂以过了。)

代码:

int Find(int x)
{
    if (x==Father[x])
    {
        return x;
    }
    else
    {
        Father[x]=Find(Father[x]);
        //直接接到祖先上
        return Father[x];
    }
}

优化2. 按秩合并

【注释】
秩:集合大小

也就是说我们把集合从小的接到大的。这样就避免了结构十分不平衡。(如果出题人故意卡你的话,结构不平衡就算路径压缩也没有用)。这就涉及到记录集合大小的问题。容易想到,只要记录一个CntCntCnt数组,记录以iii为根的子树的集合大小。而iii所在的集合大小,就是Cnt[Find(i)]Cnt[Find(i)]Cnt[Find(i)]

显然,初始的时候,由于每个点都单独在一个只有自己的集合,所以Cnt[i]=1Cnt[i]=1Cnt[i]=1
代码(由于改动较大,直接贴完整代码):

class DSU
{
    public:
        #define N 1001000
        int Father[N];
        int Cnt[N];
        void Init()
        {
            for(int i=0;i<N;i++)
            {
                Father[i]=i;
                Cnt[i]=1;//注意初始化!
            }
        }
        int Find(int x)
        {
            if (x==Father[x])
            {
                return x;
            }
            else
            {
                Father[x]=Find(Father[x]);
                return Father[x];
            }
        }
        void Merge(int x,int y)
        {
            int ax=Find(x),ay=Find(y);
            if (ax==ay) return; //2020.02.11 update: 这句不加出大问题!
            if (Cnt[ax]<Cnt[ay])//小的接到大的上
            {
                Cnt[ay]+=Cnt[ax];
                Father[ax]=ay;
            }
            else
            {
                Cnt[ax]+=Cnt[ay];
                Father[ay]=ax;
            }
        }
};

种类并查集

种类并查集珂以记录“分组”的关系。闲话少说,来道例题。

例题:洛谷2024 [NOI2001]食物链

(一道经典题)有nnn个生物,每个生物可能是A,B,CA,B,CA,B,C种类中的一种。其中,AAABBBBBBCCCCCCAAA
给你kkk句话,格式为:

  1. x yx\ yx y 表示x,yx,yx,y同类。
  2. x yx\ yx y 表示xxxyyy

请你求出有多少句话是假的。假如两句话矛盾了,那靠前的那句话是真的。假的话有几种可能:

  1. xxxy>ny>ny>n (越界了,自然是假的)
  2. 吃自己的情况显然是假的
  3. 和前面的某句话矛盾了,则前面的是真的,这句是假的
解法

普通的并查集只能维护“同类”的关系。我们要维护三个种类,怎么办呢?

拆点。我们把每个点复制三遍,分别表示这个点属于AAA的情况,属于BBB的情况,属于CCC的情况。我们记点uuu属于种类XXX的情况为X(u)X(u)X(u)。像这样:

对于1 x y1\ x\ y1 x y的操作,我们合并A(x)↔A(y)A(x)\leftrightarrow A(y)A(x)A(y),B(x)↔B(y)B(x)\leftrightarrow B(y)B(x)B(y),C(x)↔C(y)C(x)\leftrightarrow C(y)C(x)C(y)。解释的具体点,就是:
xxx属于AAA时,则yyy属于AAA
xxx属于BBB时,则yyy属于BBB
xxx属于CCC时,则yyy属于CCC
比如说我们令111222为同类,就这样连边:

对于2 x y2\ x\ y2 x y的操作,我们合并A(x)↔B(y)A(x)\leftrightarrow B(y)A(x)B(y)B(x)↔C(y)B(x)\leftrightarrow C(y)B(x)C(y)C(x)↔A(y)C(x)\leftrightarrow A(y)C(x)A(y)。同理,它相当于:
xxx属于AAA时,则yyy属于BBB
…(不赘述了)
比如我们又令333444,画出来图就是这样的:
在这里插入图片描述
那么,什么情况是矛盾的情况呢?
我们要在一遍处理的时候一遍统计。
首先,“越界”的情况很好判断,统计答案然后continuecontinuecontinue掉即珂;
关于“自己吃自己”的情况,在操作2 x y2\ x\ y2 x y的时候,如果x,yx,yx,y在同一个集合,就是不合法的。
还有其他的矛盾的情况

  1. 操作2 x y2\ x\ y2 x y的时候,如果这个时候yyyxxx,那就是矛盾的
  2. 操作1 x y1\ x\ y1 x y的时候,如果这个时候xxxyyy或者yyyxxx,也是矛盾的。

就这些了。注意代码实现。

例题代码
#include<bits/stdc++.h>
#define N 1001000 //其实15000+1就够了
using namespace std;

class DSU
{
    private:
        int Father[N]; //懒得写按秩合并了(千万不要学我)
    public:
        void Init()
        {
            for(int i=0;i<N;i++)
            {
                Father[i]=i;
            }
        }
        int Find(int x)
        {
            return x==Father[x]?x:Father[x]=Find(Father[x]);
        }
        void Merge(int x,int y)
        {
            Father[Find(x)]=Find(y);
        }
}D;
int n,m;

#define A(x) x
#define B(x) x+n
#define C(x) x+2*n
//开三倍空间
//1~n表示A种类
//n+1~2n表示B种类
//2n+1~3n表示C种类
void Eat(int x,int y) //x吃y的关系
{
    D.Merge(A(x),B(y));
    D.Merge(B(x),C(y));
    D.Merge(C(x),A(y));
}
void Same(int x,int y) //x和y同类
{
    D.Merge(A(x),A(y));
    D.Merge(B(x),B(y));
    D.Merge(C(x),C(y));
}
bool Query(int x,int y)
{
    return D.Find(x)==D.Find(y);
}

int cnt=0;
void Input()
{
    scanf("%d%d",&n,&m);
    for(int i=1;i<=m;i++)
    {
        int o,u,v;
        cin>>o>>u>>v;
        if (u>n or v>n)
        {
            cnt++;
            continue;
        }

        if (o==1) //同类的情况
        {
            if (Query(u+n,v) or Query(u,v+n)) //u,v有吃的关系
            {
                cnt++;
                continue;
            }
            else
            {
                Same(u,v);
            }
        }
        else //u吃v
        {
            if (Query(u,v) or Query(u+n,v)) //u,v同类,或者v吃u
            {
                cnt++;
                continue;
            }
            else
            {
                Eat(u,v);
            }
        }
    }
    printf("%d\n",cnt);
}

main()
{
    D.Init();
    Input();
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值