问题描述:
动物王国中有三类动物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句话有的是真的,有的是假的。当一句话满足下列三条之一时,这句话就是假话,否则就是真话。
1) 当前的话与前面的某些真的话冲突,就是假话;
2) 当前的话中X或Y比N大,就是假话;
3) 当前的话表示X吃X,就是假话。
你的任务是根据给定的N(1 <= N <= 50,000)和K句话(0 <= K <= 100,000),输出假话的总数。
样例输入:
100 7
1 101 1
2 1 2
2 2 3
2 3 3
1 1 3
2 3 1
1 5 5
样例输出
3
算法分析:
我们可以把所有元素都并到当前集合中,两个元素a,b而言,它们之间必定存在着某种联系,而这个联系是可以推算的。我们可以用动物之间“相对”的关系来确定一个并查集。
0 - 这个节点与它的父节点是同类,
1 - 这个节点被它的父节点吃,
2 - 这个节点吃它的父节点。
我们注意到,当 d = 1的时候,( d - 1 ) = 0,也就是我们制定的意义。
那么在find(x)路径压缩过程中,我们涉及到的当前节点的relation和当前节点父节点的relation推出当前节点与其父节点的父节点的relation。(也就是有儿子和父亲的关系,推出儿子和爷爷辈的关系)
我给出穷举过程
父i 子 j
爷爷 父亲 儿子 儿子与爷爷
0 0 (i + j)%3 = 0
0 1 (i + j)%3 = 1
0 2 (i + j)%3 = 2表示儿子吃爷爷
1 0 (i + j)%3 = 1
1 1 (i + j)%3 = 2
1 2 (i + j)%3 = 0
2 0 (i + j)%3 = 2
2 1 (i + j)%3 = 0
2 2 (i + j)%3 = 1
这样可以看到,( 儿子relation + 父亲relation ) % 3 = 儿子对爷爷的relation
所以有relation[now]=(relation[now]+relation[f[now]]) % 3
路径压缩就是在搜索的时候找到最远的祖先,然后将父亲节点赋值,对于权值而言,就是找出权值与最远祖先之前所有边权传递的过程,找出节点与父亲节点的关系,依次传递即可。
设在同一树内,3号节点父亲是2号,2号父亲是根1号。与父亲的关系依次为re[3],re[2],路径压缩后权值为re[3]撇。
那么在union(A, B)过程中,我们涉及到当前节点A和B的关系、当前节点A的relation和当前节点B的relation推出两个集合之后的新指向的relation。
1.合并:并查集合并的本质就是一棵树认另一棵树做父亲,把树根相连即可,但是能否也把权值直接赋值呢? 当然不行,因为A,B是树下节点,还有考虑各自与树根的关系。 也就说,推出A,B各自与根的关系,就可以实现树根权值的连接了。
设F1与F2分别为A,B的根,两者权值关系为re[F1],A与F1的权值关系是re[A],B与F2的权值关系是re[B],A与B的权值关系为x。
A B合并相当于,A的父是B,A到B的权是x,B到F2的权是re[B],那么A到F2的权就是x+re[B],但合并时是需要路径压缩,就是把A的根连到B的根F2,这样权值就是re[A]+fe[F1]
re[A]+fe[F1]=x+re[B]
re[f1]=x+re[b]-re[a]
由图得,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;
完整代码
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cmath>
using namespace std;
int fa[100002],re[100002];
int find(int x)
{
int f=fa[x];
if(x==f)
{
return x;
}
else
{
fa[x]=find(f);
re[x]=(re[x]+re[f])%3;
return fa[x];
}
}
/*void ad(int a,int b,int val)
{
int x=find(a);
int y=find(b);
fa[x]=y;
if(val==1) re[x]=(3+(re[a]-re[b]))%3;
}*/
int main()
{
int n,k;
scanf("%d%d",&n,&k);
for(int i=1;i<=n;i++)
{
fa[i]=i;
re[i]=0;
}
int p,a,b,ans=0;
for(int i=1;i<=k;i++)
{
scanf("%d%d%d",&p,&a,&b);
if((a>n||b>n)||(a==b&&p==2))
{
ans++;
continue;
}
if(p==1)
{
int f1=find(a),f2=find(b);
if(f1==f2&&re[a]!=re[b])
{
ans++;
continue;
}
else if(f1!=f2)
{//合并
fa[f1]=f2; re[f1]=(3-re[a]+re[b])%3;
}
}
if(p==2)
{
int f1=find(a),f2=find(b);
if(f1==f2)
{
int temp=((re[a]-re[b])+3)%3;
if(temp!=1)
{
ans++;
continue;
}
}
else
{
fa[f1]=f2; //合并
re[f1]=(3-re[a]+re[b]+1)%3;
}
}
}
printf("%d",ans);
return 0;
}
第二种方法用“扩展域”并查集来做
算法分析:
把每个动物x拆成三个节点,同类域xself、捕食域xeat和天敌域xenemy
若说“x和y是同类”,则说明"x的同类域与y的同类域一样",x的捕食域与y的捕食域一样,x的天敌与y的天敌一样。此时,我们合并xself和yself,xeat和yeat,xenemy和yenemy。
若说“x吃y”,说明“x捕食物种就是y的同类”,“x的同类就是y的天敌”,又因为题目中告诉我们食物链长度为3的环形,所以x的天敌就是y捕食的物种(x吃y,y吃z,z就吃x),此时,应合并xeat和yself,xself和yenemy,xenemy与yeat
。
在处理每个询问之前,都要检查这句话的真假。
有两种信息与“x与y是同类”矛盾:
1、xeat与yself在同一集合,说明x吃y
2、xself与yeat在同一集合,说明y吃x
有两种信息与“x吃y”矛盾:
1、xself与yself在同一集合,说明x与y是同类
2、xself与yeat在同一集合,说明y吃x。
完整代码
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
int fa[200000];
int n,m,k,x,y,ans;
int get(int x)
{
if(x==fa[x]) return x;
return fa[x]=get(fa[x]);
}
void ver(int x,int y)
{
fa[get(x)]=get(y);
}
int main()
{
cin>>n>>m;
for(int i=1;i<=3*n;i++) fa[i]=i;
for(int i=1;i<=m;i++)
{
scanf("%d%d%d",&k,&x,&y);
if(x>n||y>n) ans++;
else if(k==1)
{
if(get(x)==get(y+n)||get(x)==get(y+n+n)) ans++;
else{
ver(x,y);
ver(x+n,y+n);
ver(x+n+n,y+n+n);
}
}
else{
if(x==y||get(x)==get(y)||get(x)==get(y+n)) ans++;
else{
ver(x,y+n+n);
ver(x+n,y);
ver(x+n+n,y+n);
}
}
}
cout<<ans<<endl;
}
sunday_soft
发布了11 篇原创文章 · 获赞 0 · 访问量 388
私信
关注