并查集 笔记

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;
            }
        }
};

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

接下来是如何“查”。

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

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

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

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

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

这样并查集就学了 3 3 \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. 按秩合并

【注释】
秩:集合大小

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

显然,初始的时候,由于每个点都单独在一个只有自己的集合,所以 C n t [ i ] = 1 Cnt[i]=1 Cnt[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]食物链

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

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

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

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

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

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

对于 1   x   y 1\ x\ y 1 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)。解释的具体点,就是:
x x x属于 A A A时,则 y y y属于 A A A
x x x属于 B B B时,则 y y y属于 B B B
x x x属于 C C C时,则 y y y属于 C C C
比如说我们令 1 1 1 2 2 2为同类,就这样连边:

对于 2   x   y 2\ x\ y 2 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)。同理,它相当于:
x x x属于 A A A时,则 y y y属于 B B B
…(不赘述了)
比如我们又令 3 3 3 4 4 4,画出来图就是这样的:
在这里插入图片描述
那么,什么情况是矛盾的情况呢?
我们要在一遍处理的时候一遍统计。
首先,“越界”的情况很好判断,统计答案然后 c o n t i n u e continue continue掉即珂;
关于“自己吃自己”的情况,在操作 2   x   y 2\ x\ y 2 x y的时候,如果 x , y x,y x,y在同一个集合,就是不合法的。
还有其他的矛盾的情况

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

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

例题代码
#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;
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值