小学生都能看懂的并查集

普通并查集

以洛谷 P3367 【模板】并查集 为例

题目描述
如题,现在有一个并查集,你需要完成合并和查询操作。
题目
我们可以想到这样,对于每一次的合并操作,我们得到的结果可以用一棵树来表示,那么整个并查集的过程实际上是对森林进行操作,而我们判断两个元素是否在一个集合中,只需要判断其对应的森林的根节点是否相等就可以了

于是我们有了最初的想法:

fa[i]=i;

for_each_merge : fa[x] = y;
for_each_question : check(root_x,root_y)

但是,这样的时间复杂度肯定是不够优秀的,那么我们考虑一下几点:
对于每一个点 x 来说:

  1. 确定 x 所在的集合,只需确定 x 所在的森林的根
  2. x 所在的森林的根与 x 的父亲节点无关

那么知道了以上的一点性质,我们就可以很方便的去优化我们的代码
比如对于下面的一棵树(森林的一部分)

在这里插入图片描述
其实我们要仅仅需要当前节点对应的根即可

在这里插入图片描述
于是,我们可以将我们本来用来记录父亲节点的数组直接用来记录根节点,于是上面的图树退化为这样
在这里插入图片描述
在明确我们思路之后,就可以来构建代码了

在代码中,包含两个函数,一个是 find() 函数,起的作用是去寻找我们当前节点所在的树的根节点,并且在查找过程中更新路径上的节点的根节点,使其父亲节点全部都指向根
另一个函数是 merge() ,起到合并两个集合(森林)的作用

int Find(int x)
{
	if(fa[x]==x)	//当前点的父亲节点就是自己,那么该点就是当前树的根节点
		return fa[x];	//返回跟节点,以便更新下方节点所指向的根
	return fa[x]=Find(fa[x]);	//否则就继续向上找根,并且更新当前点的父亲节点
}
void Merge(int x, int y)
{
    f[Find(x)] = Find(y);
    //这里只需要将x所在树的根的根修改为y所在的树的根,那么就完成了对x,y两棵树的合并
}

全部代码:

#include <bits/stdc++.h>
using namespace std;
int f[100010];
int n, m;

int Find(int x)
{
    return ((f[x] == x) ? (x) : (f[x] = Find(f[x])));
}

void Merge(int x, int y)
{
    f[Find(x)] = Find(y);
}

int main()
{
    scanf("%d%d", &n, &m);
    for (int i = 1; i <= n; i++)
        f[i] = i;	//在未构建的时候,每个点单独成树,根即自身
    for (int i = 1, flag, x, y; i <= m; i++)
    {
        scanf("%d%d%d", &flag, &x, &y);
        if (flag == 1)
        {
            Merge(x, y);
        }
        else
        {
            if (Find(x) == Find(y))
                printf("Y\n");
            else
                printf("N\n");
        }
    }
    return 0;
}

权值并查集1(Find them, Catch them为例)

在这里插入图片描述
题目大意,有 n 个罪犯,2个犯罪团伙,每一个罪犯只属于其中某一个,并给出了 m 次操作
操作A是询问这两个罪犯是否是一个团伙
操作D是给出这两个罪犯不在一个团伙

同样我们分析,这与上面的并查集来说,又多了一层条件:是否属于同一种类别。对于这种并查集,我们称为权值并查集或种类并查集,此时我们不但要记录当前点所在树的根节点,同时,我们应该来记录当前点与其根节点的对应关系,并且我们能从这种对应关系中来推导出它们原来的关系

那么我们就要在这个程序中引入一个新的数组 en (enemy)
我们考虑如下关系,我们用0来代表是同一个帮派,1代表不同帮派,那么对于任意两个点它们分别和根的关系:
同一帮派和同一帮派:这两个点为同一帮派
同一帮派和不同帮派:这两个点为不同帮派
不同帮派和不同帮派:这两个点为同一帮派

也就是说:朋友的朋友是朋友,朋友的敌人是敌人,敌人的敌人是朋友
那么我们可以很方便的知道,对于帮派的更新,我们只需要对其值异或或求和模 2 即可

而对一条链上的各个点,我们不妨来画一个图辅助分析
在这里插入图片描述
[相同的颜色表示相同的帮派,边上的值代表当前点和其父亲节点的关系]

那么我们更新过程如下
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
也就是说,我们需要递归寻找当前点的根节点,并且在回溯的过程中去更新当前点和根节点的关系
然后就可以写出这样的一个Find函数

int Find(int x)
{
    if (fa[x] != x)
    {
        fa[x] = Find(fa[x]);
        en[x] = (en[x] + en[fa[x]]) % 2;
    }
    return fa[x];
}

然而不要高兴的太早,这必然是一个错误的代码,原因很简单,我们这里的更新方式是先对当前节点的父亲节点更新了,然后再对于当前节点的权值更新
但是仔细看上图,你会发现,我们对于每一次的关系更新,都是基于以前的父亲节点而不是经过我们Find函数处理过后的根节点
在这里插入图片描述
我们以上图为例,对于6号节点,我们应该通过5号节点去更新权值,但是在执行完Find操作后,我们6号节点的父亲已经变成1号节点了,自然我们再去更新就会出问题

所以以上的代码应该做出如下修改

int Find(int x)
{
    if (fa[x] != x)
    {
        int t = fa[x];	//在这里记录一下之前的父亲节点
        fa[x] = Find(fa[x]);
        en[x] = (en[x] + en[t]) % 2;
    }
    return fa[x];
}

而对于查找函数,我们就要用到上面朋友的朋友是朋友,朋友的敌人是敌人,敌人的敌人是朋友的思想了
如下图:
在这里插入图片描述
对于上面的一棵树,我们要查找任意两点的关系,其实就是找这两点所在链上的权值异或和
而对于任意一点,我们执行完一次Find操作后,其父亲一定直接指向根,有且仅有0和1两种状态

比如节点3和6
我们对3和6分别做Find操作,那么3与根的关系是“敌人”,6与根的关系是“敌人”,所以3和6的关系是朋友

再比如6和7
一样做Find操作,我们可以知道,6与根的关系是“敌人”,7与根的关系是“朋友”,所以6和7的关系是敌人

Find操作到此结束
接下来就是Merge操作

虽然题目给的是x和y的关系,但是实际上我们可以将其转化为 Root_x 和 Root_y 的关系
在这里插入图片描述
有应为题目给出的是x和y为敌对关系,所以 Root_x 和 Root_y 通过(Root_x,x)与(Root_y,y)来更新时,还需要异或 1
证明如下:

我们定义有序点对 [A,B] = 0 or 1
分别表示A指向B时,其关系状态
那么我们可以得到如下性质
性质1:
:对于任意 [A,B] ≡ [B,A]
性质2:
对于 [A,B] = a [B,C] = b
恒有 [A,C] ≡ [A,B] XOR [B,C]
由性质2,我们可以得到:
所以对于[Root_x,Root_y] ≡ [Root_x,x] XOR [x,y] XOR [y,Root_y]
再由性质1,我们可以得到:
[Root_x,Root_y] ≡ [x,Root_x] XOR [x,y] XOR [y,Root_y]

于是我们的Merge函数就可以愉快的写了

void Merge(int x, int y)
{
    int fx = Find(x);
    int fy = Find(y);
    if (fx != fy)
    {
        fa[fy] = fx;
        en[fy] = (en[x] + en[y] + 1) % 2;
    }
}

最后附上全部代码

#include <cstdio>
using namespace std;
int fa[100010]; //自己所属的祖先
int en[100010]; //自己和祖先的关系,1表示不同,0表示相同
int n, m;

int Find(int x)
{
    if (fa[x] != x)
    {
        int t = fa[x];
        fa[x] = Find(fa[x]);
        en[x] = (en[x] + en[t]) % 2;
    }
    return fa[x];
}

void Merge(int x, int y)
{
    int fx = Find(x);
    int fy = Find(y);
    if (fx != fy)
    {
        fa[fy] = fx;
        en[fy] = (en[x] + en[y] + 1) % 2;
    }
}

int main()
{
    int T;
    scanf("%d", &T);
    while (T--)
    {
        scanf("%d%d", &n, &m);
        char s[5];
        for (int i = 1; i <= n; i++)
            fa[i] = i, en[i] = 0;
        for (int i = 1, x, y; i <= m; i++)
        {
            scanf("%s%d%d", s, &x, &y);
            if (s[0] == 'A')
            {
                if (Find(x) == Find(y))
                {
                    if (en[x] == en[y])
                        puts("In the same gang.");
                    else
                        puts("In different gangs.");
                }
                else
                    puts("Not sure yet.");
            }
            else
                Merge(x, y);
        }
    }
    return 0;
}

权值并查集2(食物链)

在这里插入图片描述

这是一道相对具有技术含量的题,但是基本思路和上面差不多,都是多记录一下当前节点和根节点的关系

因为我们这里有吃与被吃两种关系,所以我们不妨假设用0,1,2来储存这种关系状态,x能吃x-1,且能被x+1吃(当然要对3取模)

那么我们对于上面的Find和Merge函数稍加改造就能得到我们这一题可用的函数

int find(int x)
{
    if (fa[x] != x)
    {
        int f = fa[x];
        //因为在下方的更新后,当前的fa已经改变,而对dist的更新在fa后,所以要先行记录
        fa[x] = find(fa[x]);
        dist[x] = (dist[x] + dist[f]) % 3; //更新关系
    }
    return fa[x];
}


Merge:
//合并同类
if (find(x) != find(y))
{
	//改变x的根节点的指向
	dist[fa[x]] = (dist[y] - dist[x] + 3) % 3;
	fa[fa[x]] = fa[y];
}

//合并不同类(y被x吃)
if (find(x) != find(y))
{
	dist[fa[x]] = (dist[y] - dist[x] + 4) % 3;
	fa[fa[x]] = fa[y];
}

我们应当特别注意这一句:

dist[fa[x]] = (dist[y] - dist[x] + 4) % 3;

这里实际可以被拆分为这样:

dist[fa[x]] = ((dist[y] - dist[x] + 3) + 1) % 3;

这里的 +1 实际就反映了 x 和 y 的关系,而Root_x和Root_y同样可以由Dist_x , Dist_y , xy 来表示

至此,改题目的讲解已经完成,下面放上代码:

#include <bits/stdc++.h>
using namespace std;
int fa[100010], dist[100010];
int ans, n, k;

int find(int x)
{
    if (fa[x] != x)
    {
        int f = fa[x];
        //因为在下方的更新后,当前的fa已经改变,而对dist的更新在fa后,所以要先行记录
        fa[x] = find(fa[x]);
        dist[x] = (dist[x] + dist[f]) % 3; //更新关系
    }
    return fa[x];
}

void setup()
{
    for (int i = 1; i <= n; i++)
        fa[i] = i, dist[i] = 0;
}

int main()
{
    int flag, x, y;
    scanf("%d%d", &n, &k);
    setup();
    while (k--)
    {
        scanf("%d%d%d", &flag, &x, &y);
        if (x > n || y > n)
        {
            ans++;
            continue;
        }
        if (flag == 1)
        {
            if (find(x) != find(y))
            {
                //改变x的根节点的指向
                dist[fa[x]] = (dist[y] - dist[x] + 3) % 3;
                fa[fa[x]] = fa[y];
            }
            else if (dist[x] != dist[y])
                ans++;
        }
        if (flag == 2)
        {
            if (find(x) != find(y))
            {
                dist[fa[x]] = (dist[y] - dist[x] + 4) % 3;
                fa[fa[x]] = fa[y];
            }
            else if (dist[x] != ((dist[y] + 1) % 3))
                ans++;
        }
    }
    printf("%d", ans);
}
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值