食物链--

欢迎大家访问博客:博客传送门

食物链

题目描述

image-20210715205849019


核心思路

这题刚开始看的时候,都想不到会用到并查集。那么,我们就来分析一下,为什么需要用到并查集呢?

题目中提到,只有三类动物,但是每类动物中可能有很多个,每个动物都会有各自的编号。并且 A A A B B B B B B C C C C C C A A A,这三类动物的食物链构成了有趣的环形。我们设如果 x x x吃了 y y y,则记录表示为 y → x y\to x yx。那么 A A A B B B B B B C C C C C C A A A,可以表达为 B → A , C → B , A → C B\to A,C\to B,A\to C BA,CB,AC,如下图所示:

image-20210715230906789

题目会给出一些信息,那么我们如何根据这些信息来判断各个动物之间的关系呢?是同类呢还是天敌关系呢?

我们把这些动物看成是军人,假设有 100 100 100个军人,我们想知道每个军人是什么等级。

  • 如果我们是两两比较军人的关系,那么需要 O ( n 2 ) O(n^2) O(n2)才能知道每个军人是什么等级
  • 但是如果我们知道编号为 1 1 1的军人它是司令,然后知道了剩下的 99 99 99个军人与司令的关系,那么就可以在 O ( n ) O(n) O(n)内知道每个军人是什么等级了。比如 2 2 2号说:我比司令低一级,那么 2 2 2号军人的等级就是军长, 5 5 5号军人说:我比司令低两级,那么 5 5 5号军人的等级就是师长。如果 90 90 90号军人说:我比司令低一级,那么 90 90 90号军人的等级就是军长。那么 2 2 2号和 90 90 90就是同类,处于同一个等级。不要被编号迷惑了,可以理解为 2 2 2号成为军长的时间较早, 90 90 90号成为军长较晚,但是他俩都是军长,是同类,处于同一个等级。

因此,我们可以把司令看作是一个根节点,那么我们怎么知道每个点与司令这个根节点的关系呢?这其实就需要用到并查集呢!想要知道每个点与司令这个根节点的关系,其实就类似于并查集中的路径压缩。因此,我们可以使用并查集来查询和合并食物链中动物之间的关系。

这里给出d[]数组的解释:

d[i]的含义:表示第 i i i个动物在食物链中的深度,其实也就是第 i i i个动物到它父节点的距离。

设根节点的深度为0,我们有以下定义:

  • 如果某类动物,它到根节点距离为0,则表明该类动物与根节点这类动物是同一类动物
  • 如果某类动物,它到根节点距离为1,则表明根节点被该类动物吃
  • 如果某类动物,它到根节点距离为2,则表明该类动物可以吃上一种情况的动物,而且该类动物被根节点吃(因为三类动物形成环)

在本题中,我们可以用深度来表达动物在食物链中的关系。由于本题只有三种类型的动物,这三类动物的食物链构成了有趣的环形。 A A A B B B B B B C C C C C C A A A,那么深度也只有 0 , 1 , 2 0,1,2 0,1,2,因此当深度 ≥ 3 \geq3 3时,则可以通过模 3 3 3运算,将其转换成 0 , 1 , 2 0,1,2 0,1,2中的某一个。

现在来思考一个问题,我们 在查找时如何更新深度

首先,通过并查集的查询操作,找到祖宗节点,当集合号等于自身时回溯,在回溯过程中需要更新集合号为祖宗的集合号,并且要更新当前节点的深度累加其父节点的深度。当深度 ≥ 3 \geq3 3时,则可以通过模 3 3 3运算即可。即 d [ x ] = ( d [ x ] + d [ f x ] ) % 3 d[x]=(d[x]+d[f_x])\%3 d[x]=(d[x]+d[fx])%3

如何理解 d [ x ] = ( d [ x ] + d [ f x ] ) % 3 d[x]=(d[x]+d[f_x])\%3 d[x]=(d[x]+d[fx])%3这个式子呢?

如图:

image-20210718000753778

当输入1吃2、2吃3、3吃4时,并查集如下左图所示。当查询1的集合号时,首先找到祖宗节点4,回溯时更新3号节点的深度为1,集合号为4;更新2号节点的深度为2,集合号为4;更新1号节点的深度为0,集合号为4,如下右图所示:

  • 对于3号节点来说,路径压缩前,它的父节点是4,距离为1,所以 d [ x ] = d [ 3 ] = 1 d[x]=d[3]=1 d[x]=d[3]=1;路径压缩后,找到集合的根节点是4号节点,那么3号节点的父节点就是4,距离为1,所以 d [ x ] = d [ 3 ] = 1 d[x]=d[3]=1 d[x]=d[3]=1,其父节点的深度其实就是4号节点到4号节点的深度,所以 d [ f x ] = 0 d[f_x]=0 d[fx]=0;所以路径压缩后,3号节点的深度为 d [ 3 ] = ( d [ x ] + d [ f x ] ) % 3 = ( 1 + 0 ) % 3 = 1 d[3]=(d[x]+d[f_x])\%3=(1+0)\%3=1 d[3]=(d[x]+d[fx])%3=(1+0)%3=1
  • 对于2号节点来说,路径压缩前,它的父节点是3,距离为1,所以 d [ x ] = d [ 2 ] = 1 d[x]=d[2]=1 d[x]=d[2]=1;路径压缩后,找到集合的根节点是4号节点,那么路径压缩后2号节点的父节点就是4,其父节点的深度其实就是3号节点到4号节点的深度,所以 d [ f x ] = 1 d[f_x]=1 d[fx]=1;所以路径压缩后,2号节点的深度为 d [ 2 ] = ( d [ x ] + d [ f x ] ) % 3 = ( 1 + 1 ) % 3 = 2 d[2]=(d[x]+d[f_x])\%3=(1+1)\%3=2 d[2]=(d[x]+d[fx])%3=(1+1)%3=2
  • 对于1号节点来说,路径压缩前,它的父节点是2,距离为1,所以 d [ x ] = d [ 2 ] = 1 d[x]=d[2]=1 d[x]=d[2]=1;路径压缩后,找到集合的根节点是4号节点,那么路径压缩后1号节点的父节点就是4,其父节点的深度其实就是2号节点到4号节点的深度,所以 d [ f x ] = 2 d[f_x]=2 d[fx]=2;所以路径压缩后,1号节点的深度为 d [ 1 ] = ( d [ x ] + d [ f x ] ) % 3 = ( 1 + 2 ) % 3 = 0 d[1]=(d[x]+d[f_x])\%3=(1+2)\%3=0 d[1]=(d[x]+d[fx])%3=(1+2)%3=0

image-20210718002227975

再来考虑一个问题:合并时如何更新深度呢

假设节点 x x x的集合号为 a a a,节点 y y y的集合号为 b b b,如果 a ≠ b a\neq b a=b,则合并集合号 p [ a ] = b p[a]=b p[a]=b,更新 a a a的深度为 d [ a ] = ( d [ y ] − d [ x ] + c − 1 ) % 3 d[a]=(d[y]-d[x]+c-1)\%3 d[a]=(d[y]d[x]+c1)%3。如何理解这个式子呢?

路径压缩后,节点 x x x到祖宗节点 a a a的距离为 d [ x ] d[x] d[x],节点 y y y到祖宗节点 b b b的距离为 d [ y ] d[y] d[y],那么如果合并集合号 p [ a ] = b p[a]=b p[a]=b后?那么如何求节点 a a a到它的祖宗节点 b b b的距离呢?

由于 a ≠ b a\neq b a=b,说明 x x x y y y不在同一个集合中,所以才需要用到合并操作。

  • x x x y y y是同类时,根据d[]的定义可知,同类的深度差为0。即 d [ x ] + ? d[x]+? d[x]+? d y dy dy是相等的。因此有如下式子推导:

    d x + ? = d y dx+?=dy dx+?=dy    ⟺    \iff

    d x + ? = d y + 0 dx+?=dy+0 dx+?=dy+0    ⟺    \iff

    ? = d y − d x + 0 ?=dy-dx+0 ?=dydx+0    ⟺    \iff

    ? = ( d y − d x + 3 + 0 ) % 3 ?=(dy-dx+3+0)\%3 ?=(dydx+3+0)%3 由于 x x x y y y是同类, 根据题意,此时 c = 1 c=1 c=1,我们可以发现式子中的0其实就是 c − 1 = 1 − 1 = 0 c-1=1-1=0 c1=11=0。因此用 c − 1 c-1 c1代替0即可

       ⟺    ? = ( d y − d x + 3 + c − 1 ) % 3 \iff ?=(dy-dx+3+c-1)\%3 ?=(dydx+3+c1)%3 这里 d y − d x + 3 dy-dx+3 dydx+3之所以要加3是因为有可能 d y − d x dy-dx dydx是负数,导致最终结果是负数,负数不能取模,所以需要转换为正数。模3是因为该食物链为环形

  • x x x y y y是异类时,不妨假设 x x x y y y,根据d[]的定义可知,合并集合后, x x x到祖宗节点的距离 比 y y y到祖宗节点的距离 多1。即 d [ x ] + ? d[x]+? d[x]+? d y + 1 dy+1 dy+1是相等的。因此有如下式子推导:

    d x + ? = d y + 1 dx+?=dy+1 dx+?=dy+1    ⟺    \iff

    ? = ( d y − d x + 1 ) ?=(dy-dx+1) ?=(dydx+1)    ⟺    \iff

    ? = ( d y − d x + 3 + 1 ) % 3 ?=(dy-dx+3+1)\%3 ?=(dydx+3+1)%3 由于 x x x y y y是异类, x x x y y y, 根据题意,此时 c = 2 c=2 c=2,我们可以发现式子中的1其实就是 c − 1 = 2 − 1 = 1 c-1=2-1=1 c1=21=1。因此用 c − 1 c-1 c1代替1即可

       ⟺    ? = ( d y − d x + 3 + c − 1 ) % 3 \iff ?=(dy-dx+3+c-1)\%3 ?=(dydx+3+c1)%3 这里 d y − d x + 3 dy-dx+3 dydx+3之所以要加3是因为有可能 d y − d x dy-dx dydx是负数,导致最终结果是负数,负数不能取模,所以需要转换为正数。模3是因为该食物链为环形

    image-20210718094601633

举个栗子:

输入6吃2,两个节点属于不同的集合,其中6号节点属于4号集合,6号节点属于7号集合,执行合并,那么 p [ 7 ] = 4 p[7]=4 p[7]=4,更新7号节点的深度为 d [ 7 ] = ( d [ 2 ] − d [ 6 ] + 3 + 2 − 1 ) % 3 = 2 d[7]=(d[2]-d[6]+3+2-1)\%3=2 d[7]=(d[2]d[6]+3+21)%3=2,合并更新图如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-J5VVPX49-1626617703857)(https://cdn.jsdelivr.net/gh//3CodeLove/Images@main/20210718210130.png)]

当下次查询6号节点的集合号时,找到它的祖宗节点4,回溯时同时更新6号节点的深度为 d [ 6 ] = ( d [ 6 ] + d [ 7 ] ) % 3 = 0 d[6]=(d[6]+d[7])\%3=0 d[6]=(d[6]+d[7])%3=0,查询更新图如下:

image-20210718210557278

因此,我们来总结以下就是,如果 x x x的集合号 a a a y y y的集合号 b b b不相同,则说明它俩不在同一个集合中,这是才需要用到并查集的合并操作。根据以上分析推导可知,最终合并时更新深度其实就只有一个式子: ? = ( d y − d x + 3 + c − 1 ) % 3 ?=(dy-dx+3+c-1)\%3 ?=(dydx+3+c1)%3

最后再来看一个问题,深度满足什么关系是真话

这里判断是否为真话或假话,是在 x x x y y y属于同一个集合中讨论的,因为属于同一个集合的话,则不需要合并操作,那么就没有未知变量 d [ a ] d[a] d[a],不需要求未知变量,而且在这里的全部变量都是已知的,因此我们可以用这些已知变量推导出一些式子,然后我们判断这些式子是否正确就可以判断是否为真话还是假话了。因此可以在这里讨论真假话:

  • 如果 x x x y y y是同类,那么深度差为0,那么有如下式子推导:

    d x = d y    ⟺    dx=dy\iff dx=dy

    d x = d y + 0    ⟺    dx=dy+0\iff dx=dy+0

    d x − d y = 0    ⟺    dx-dy=0\iff dxdy=0

    ( d x − d y + 3 ) % 3 = 0 (dx-dy+3)\%3=0 (dxdy+3)%3=0 由于 x x x y y y是同类, 根据题意,此时 c = 1 c=1 c=1,我们可以发现式子中的0其实就是 c − 1 = 1 − 1 = 0 c-1=1-1=0 c1=11=0。因此用 c − 1 c-1 c1代替0即可

       ⟺    ( d x − d y + 3 ) % 3 = c − 1 \iff (dx-dy+3)\%3=c-1 (dxdy+3)%3=c1 也就是说当 x x x y y y在同一个集合,然后 x x x y y y是同类时,如果满足 ( d x − d y + 3 ) % 3 (dx-dy+3)\%3 (dxdy+3)%3 c − 1 c-1 c1相等,则说明是真话,否则就是假话

  • 如果 x x x y y y是异类,假设 x x x y y y,那么深度差为 d [ x ] − d [ y ] = 1 d[x]-d[y]=1 d[x]d[y]=1或者 d [ x ] − d [ y ] = − 2 d[x]-d[y]=-2 d[x]d[y]=2。如上图所示,1吃2,那么深度差为 d [ 1 ] − d [ 2 ] = 0 − 2 = − 2 d[1]-d[2]=0-2=-2 d[1]d[2]=02=2;2吃3,那么深度差为 d [ 2 ] − d [ 3 ] = 2 − 1 = 1 d[2]-d[3]=2-1=1 d[2]d[3]=21=1,对于深度差为-2的话,我们可以先加上3,然后就会变为1了,接着在模3即可,对于深度差为1的话,我们先加上3,然后就会变为4,接着再模3即可。那么有如下式子推导:

    d x = d y + 1    ⟺    dx=dy+1\iff dx=dy+1

    d x − d y = 1    ⟺    dx-dy=1\iff dxdy=1

    $(dx-dy+3)%3=1\iff $ 由于 x x x y y y是异类, x x x y y y,根据题意,此时 c = 2 c=2 c=2,我们可以发现式子中的1其实就是 c − 1 = 2 − 1 = 1 c-1=2-1=1 c1=21=1。因此用 c − 1 c-1 c1代替1即可

    ( d x − d y + 3 ) % 3 = c − 1 (dx-dy+3)\%3=c-1 (dxdy+3)%3=c1 也就是说当 x x x y y y在同一个集合,然后 x x x y y y是异类时,如果满足 ( d x − d y + 3 ) % 3 (dx-dy+3)\%3 (dxdy+3)%3 c − 1 c-1 c1相等,则说明是真话,否则就是假话

    因此,从上面分析可知,在同一个集合中,不论是同类还是被吃关系,公式统一为 ( d x − d y + 3 ) % 3 = c − 1 (dx-dy+3)\%3=c-1 (dxdy+3)%3=c1,如果不满足此等式,则为假话

    算法设计

    1. x x x y y y大于 n n n,或者 c = 2 c=2 c=2并且 x = = y x==y x==y,则为假话
    2. 执行c x y指令时,首先查询 x x x y y y的集合号。查询集合号回归时,更新这条路径上每个节点的深度, d [ x ] = ( d [ x ] + d [ f x ] ) % 3 d[x]=(d[x]+d[f_x])\%3 d[x]=(d[x]+d[fx])%3。设 x x x的集合号为 a a a y y y的集合号为 b b b,则分以下两种情况讨论:
      • a ≠ b a\neq b a=b时,说明 x x x y y y不在同一个集合中,那么需要合并 p [ a ] = b p[a]=b p[a]=b,更新 a a a的深度为 d [ a ] = ( d [ y ] − d [ x ] + 3 + c − 1 ) % 3 d[a]=(d[y]-d[x]+3+c-1)\%3 d[a]=(d[y]d[x]+3+c1)%3
      • a = b a=b a=b时,说明 x x x y y y在同一个集合中,如果 ( d [ x ] − d [ y ] + 3 ) % 3 ! = c − 1 (d[x]-d[y]+3)\%3!=c-1 (d[x]d[y]+3)%3!=c1,则为假话

    算法实现:

    (1)初始化:

for(int i=1;i<=n;i++)
{
p[i]=i;
d[i]=0;
}


(2)查找集合号。查询$x,y$的集合号,在返回过程中,除了要统一路径上每个节点的集合号外,还要更新$d[x]$的值(将当前节点的$d$值($d[x]$)累加其父节点的$d$值$d[f_x]$模3)

```c++
int find(int x)
{
    if(x!=p[x])
    {
        int u=find(p[x]);
        d[x]=(d[x]+d[p[x]])%3;
        p[x]=u;
    }
    return p[x];
}

(3)判断假话数量。对输入的每一条指令,如果 x x x y y y大于 n n n,或者 c = 2 c=2 c=2并且 x = = y x==y x==y,则为假话, t o t a l total total++;否则查询集合号,设 x x x的集合号为 a a a y y y的集合号为 b b b,当 a ≠ b a\neq b a=b时,合并集合号 p [ a ] = b p[a]=b p[a]=b,更新 a a a的深度为 d [ a ] = ( d [ y ] − d [ x ] + 3 + c − 1 ) % 3 d[a]=(d[y]-d[x]+3+c-1)\%3 d[a]=(d[y]d[x]+3+c1)%3,当 a = b a=b a=b时,说明 x x x y y y在同一个集合中,如果 ( d [ x ] − d [ y ] + 3 ) % 3 ! = c − 1 (d[x]-d[y]+3)\%3!=c-1 (d[x]d[y]+3)%3!=c1,则为假话, t o t a l + + total++ total++

while(k--)
{
    scanf("%d%d%d",&c,&x,&y);
    if(x>n||y>n||(c==2&&x==y))
        total++;
    else
    {
        int a=find(x);
        int b=find(y);
        if(a==b)
        {
            if((dx-dy+3)%3!=c-1)
                total++;
        }
        else
        {
            p[a]=b;
            d[a]=(d[y]-d[x]+3+c-1)%3;
        }
    }
}

代码

#include<cstdio>
#include<cstring>
using namespace std;
#define N 50010
int n,k;
int total;
int p[N],d[N];
//初始化
void init()
{
	for(int i=1;i<=n;i++)
	{
        //每个点都是独立的集合  集合号为它自身
		p[i]=i;
        //每个节点到它自身的距离为0  即自身深度为0
		d[i]=0;
	}	
}
//在查询点x的祖宗节点过程中  更新d[x]的新值
int find(int x)
{
	if(x!=p[x])
	{
        //寻找节点x的父节点
		int u=find(p[x]);
        //更新d[x]的值
		d[x]=(d[x]+d[p[x]])%3;
        //回溯时进行了路径压缩,记录每个节点x的祖宗节点为u
        p[x]=u;
	} 	
    return p[x];
}
int main()
{
	scanf("%d%d",&n,&k);
    //先进行初始化操作
	init();
	while(k--)
	{
	    int c,x,y;
		scanf("%d%d%d",&c,&x,&y);
        //x或y大于n,或者是x吃y,并且x==y,即同类吃同类  则为假话
		if(x>n||y>n||(c==2&&x==y))
			total++;
		else
		{
			int a=find(x);  //查询节点x的集合号(祖宗节点)
			int b=find(y);  //查询节点y的集合号(祖宗节点)
            //如果集合号相同,说明x和y在同一个集合中,那么不需要合并
			if(a==b)
			{
                //如果d[x]-d[y]+3)%3不等于c-1,则为假话
				if((d[x]-d[y]+3)%3!=c-1)
					total++;
			}
            //否则说明集合号不同,说明x和y不在同一个集合中,那么就需要进行合并操作了
			else
			{
				p[a]=b; //a的父节点是b
                //更新节点a到父节点的距离d[a]
				d[a]=(d[y]-d[x]+3+c-1)%3;
			}
		}
	}
	printf("%d\n",total);
	return 0;
}

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

卷心菜不卷Iris

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值