题目描述
动物王国中有三类动物 A,B,C,这三类动物的食物链构成了有趣的环形。A 吃 B,B
吃 C,C 吃 A。
现有 N 个动物,以 1 - N 编号。每个动物都是 A,B,C 中的一种,但是我们并不知道
它到底是哪一种。
有人用两种说法对这 N 个动物所构成的食物链关系进行描述:
第一种说法是“1 X Y”,表示 X 和 Y 是同类。
第二种说法是“2 X Y”,表示 X 吃 Y 。
此人对 N 个动物,用上述两种说法,一句接一句地说出 K 句话,这 K 句话有的是真
的,有的是假的。当一句话满足下列三条之一时,这句话就是假话,否则就是真话。
• 当前的话与前面的某些真的话冲突,就是假话
• 当前的话中 X 或 Y 比 N 大,就是假话
• 当前的话表示 X 吃 X,就是假话
你的任务是根据给定的 N 和 K 句话,输出假话的总数。
输入格式
从 eat.in 中输入数据
第一行两个整数,N,K,表示有 N 个动物,K 句话。
第二行开始每行一句话(按照题目要求,见样例)
输出格式
输出到 eat.out 中
一行,一个整数,表示假话的总数。
输入输出样例
100 7 1 101 1 2 1 2 2 2 3 2 3 3 1 1 3 2 3 1 1 5 5
3
说明/提示
1 ≤ N ≤ 5 ∗ 10^4
1 ≤ K ≤ 10^5
多种解法,就我所知:
①多集(开三个并查集)
②取模
①
最开始我开了三个并查集水果,分别代表自己、自己的猎物、自己的天敌(吃自己的)
在这个过程中不仅感叹:思维差异影响代码复杂度啊!(哭死在RE的血泪中)
看了代码估计就都明白了
#include<bits/stdc++.h> using namespace std; const int N = 100005; int n,m,fa[N*3],ans,s,a,b; int find(int x) { if(fa[x]==x)return fa[x]; return find(fa[x]); } int main() { cin>>n>>m; for(int i=1;i<=n*3;i++) fa[i]=i; for(int i=1;i<=m;i++){ cin>>s>>a>>b; if(a>n||b>n){ ans++; continue; } int t1=find(a),t2=find(b),t3=find(a+n),t4=find(b+n),t5=find(a+n+n),t6=find(b+n+n); if(s==1){ //声明他们是同类 if(t1==t4||t2==t3)ans++; //互为食物 else { fa[t1]=t2; //合并 fa[t3]=t4; fa[t5]=t6; } } if(s==2){ //声明a吃b if(t1==t2||t1==t4)ans++; //ab同类或b吃a else fa[t3]=t2,fa[t5]=t4,fa[t1]=t6; //合并 } } cout << ans; return 0; }
②带权并查集
以下转自luogu天泽龟大佬的题解↓
(先%%一下题解大佬orz)
带权并查集的诠释是这样的:
在对并查集进行路径压缩和合并操作时,这些权值具有一定属性,即可将他们与父节点的关系,变化为与所在树的根结点关系。
也就是说,权值代表着当前节点与父节点的某种关系(即使路径压缩了也是这样),通过两者关系,也可以将同一棵树下两个节点的关系表示出来。
而P2024《食物链》这道题,又属于加权并查集下的分支:
种类并查集
由题意得,动物一共只有A,B,C三种,也就是说只要确定了一种动物的种类和他们的关系(即权值),其他的动物的种类也就知道了。
我们用re数组表示编号i与父亲节点的权值关系,由于只有三种动物,所以权值也只有三种:0-->同种动物,1-->捕食关系,2-->被捕食关系,转移时便可以采用对3取模来实现。(初始化为0,即自己与自己为同种动物)
那么第一个问题,就是如何在查找与合并时转移这种权值?
1.合并:并查集合并的本质就是一棵树认另一棵树做父亲,把树根相连即可,但是能否也把权值直接赋值呢(比如1操作就直接赋值为1)? 当然不行,因为给你的a,b是树下节点,还有考虑各自与树根的关系。 也就说,推出A,B各自与根的关系,就可以实现树根权值的连接了。
设F1与F2分别为A,B的根,两者权值关系为re[F1],A与F1的权值关系是re[A],B与F2的权值关系是re[B],A与B的权值关系为x。
由图得,re[f1]=x+re[b]-re[a]
由于可能会造成re[b]-re[a]<0的情况,所以加3再对三取模。又因为x已知为0或1(要么是同种动物,要么是捕食关系),所以最终结果为:
re[f1]=(re[b]-re[a]+3)%3或re[f1]=(1+re[b]-re[a]+3)%3;
2.查找(路径压缩):路径压缩就是在搜索的时候找到最远的祖先,然后将父亲节点赋值,对于权值而言,就是找出权值与最远祖先之前所有边权传递的过程,找出节点与父亲节点的关系,依次传递即可。
设在同一树内,3号节点父亲是2号,2号父亲是根1号。与父亲的关系依次为re[3],re[2],路径压缩后权值为re[3]撇。
显然,re[3]撇=(re[3]+re[2])%3,别忘了取模。
当然不会数学推得话打表也是好方法,本蒟蒻就是打完表水过题再数学证明的(:з」∠)。
这两式子一出来,题目就好做多了(我还是因为板子打错了改个近一小时)。
根据题目我们还可以确定:判断两点的关系是否正确必须要在同一棵树下,反之则一定正确。(因为如果是两棵树,两点的关系就不能确定了。)
然后一些小问题又没啥好说了,贴上丑陋的代码:
#include <iostream> using namespace std; int f[100000],re[100000]; //0-->同种动物,1-->捕食关系,2-->被捕食关系。 int n,m,a,b,p,ans=0; int find(int a) { int fa = f[a]; if (a != fa) { f[a] = find(fa); re[a] = (re[a] + re[fa]) % 3; //路径压缩后的权值 return f[a]; } else return fa; } int main() { cin >> n >> m; for (int i = 1; i <= n; i++) f[i] = i, re[i] = 0; //初始化 for (int i=1;i <= m;i++){ cin>>p>>a>>b; if ((a>n||b>n)||(p==2&&a==b)) { ans++; continue; } int f1 = find(a),f2 = find(b); if (p == 1) { //1xy表示两者是同类 if (f1 == f2 && re[a] != re[b]) { //f判断是否在同一棵树 //re(权值)两者是否为同种动物。 ans++; continue; } else if(f1!=f2){ f[f1] = f2; re[f1] = (3 - re[a] + re[b]) % 3; //合并权值,归入同一棵树下 } } if (p == 2) { //2xy x吃y if (f1 == f2) { int rela = (re[a] -re[b] + 3) % 3; //用两个节点与父亲的关系推出两者关系 if (rela != 1) { ans++; continue; } } else { f[f1]=f2; re[f1]=(3-re[a]+re[b]+1)%3; } } } cout<<ans; return 0; }