食物链NOI2001/POJ1182(并查集边带权或扩展域)

问题描述:
动物王国中有三类动物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];
 }
}
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;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值