欢迎大家访问博客:博客传送门
食物链
题目描述
核心思路
这题刚开始看的时候,都想不到会用到并查集。那么,我们就来分析一下,为什么需要用到并查集呢?
题目中提到,只有三类动物,但是每类动物中可能有很多个,每个动物都会有各自的编号。并且 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 y→x。那么 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 B→A,C→B,A→C,如下图所示:
题目会给出一些信息,那么我们如何根据这些信息来判断各个动物之间的关系呢?是同类呢还是天敌关系呢?
我们把这些动物看成是军人,假设有 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这个式子呢?
如图:
当输入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
再来考虑一个问题:合并时如何更新深度呢?
假设节点 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]+c−1)%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 ?=dy−dx+0 ⟺ \iff ⟺
? = ( d y − d x + 3 + 0 ) % 3 ?=(dy-dx+3+0)\%3 ?=(dy−dx+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 c−1=1−1=0。因此用 c − 1 c-1 c−1代替0即可
⟺ ? = ( d y − d x + 3 + c − 1 ) % 3 \iff ?=(dy-dx+3+c-1)\%3 ⟺?=(dy−dx+3+c−1)%3 这里 d y − d x + 3 dy-dx+3 dy−dx+3之所以要加3是因为有可能 d y − d x dy-dx dy−dx是负数,导致最终结果是负数,负数不能取模,所以需要转换为正数。模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) ?=(dy−dx+1) ⟺ \iff ⟺
? = ( d y − d x + 3 + 1 ) % 3 ?=(dy-dx+3+1)\%3 ?=(dy−dx+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 c−1=2−1=1。因此用 c − 1 c-1 c−1代替1即可
⟺ ? = ( d y − d x + 3 + c − 1 ) % 3 \iff ?=(dy-dx+3+c-1)\%3 ⟺?=(dy−dx+3+c−1)%3 这里 d y − d x + 3 dy-dx+3 dy−dx+3之所以要加3是因为有可能 d y − d x dy-dx dy−dx是负数,导致最终结果是负数,负数不能取模,所以需要转换为正数。模3是因为该食物链为环形
举个栗子:
输入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+2−1)%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,查询更新图如下:
因此,我们来总结以下就是,如果 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 ?=(dy−dx+3+c−1)%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 dx−dy=0⟺
( d x − d y + 3 ) % 3 = 0 (dx-dy+3)\%3=0 (dx−dy+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 c−1=1−1=0。因此用 c − 1 c-1 c−1代替0即可
⟺ ( d x − d y + 3 ) % 3 = c − 1 \iff (dx-dy+3)\%3=c-1 ⟺(dx−dy+3)%3=c−1 也就是说当 x x x和 y y y在同一个集合,然后 x x x和 y y y是同类时,如果满足 ( d x − d y + 3 ) % 3 (dx-dy+3)\%3 (dx−dy+3)%3与 c − 1 c-1 c−1相等,则说明是真话,否则就是假话
-
如果 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]=0−2=−2;2吃3,那么深度差为 d [ 2 ] − d [ 3 ] = 2 − 1 = 1 d[2]-d[3]=2-1=1 d[2]−d[3]=2−1=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 dx−dy=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 c−1=2−1=1。因此用 c − 1 c-1 c−1代替1即可
( d x − d y + 3 ) % 3 = c − 1 (dx-dy+3)\%3=c-1 (dx−dy+3)%3=c−1 也就是说当 x x x和 y y y在同一个集合,然后 x x x和 y y y是异类时,如果满足 ( d x − d y + 3 ) % 3 (dx-dy+3)\%3 (dx−dy+3)%3与 c − 1 c-1 c−1相等,则说明是真话,否则就是假话
因此,从上面分析可知,在同一个集合中,不论是同类还是被吃关系,公式统一为 ( d x − d y + 3 ) % 3 = c − 1 (dx-dy+3)\%3=c-1 (dx−dy+3)%3=c−1,如果不满足此等式,则为假话
算法设计:
- 若 x x x或 y y y大于 n n n,或者 c = 2 c=2 c=2并且 x = = y x==y x==y,则为假话
- 执行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+c−1)%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!=c−1,则为假话
算法实现:
(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+c−1)%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!=c−1,则为假话, 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;
}