食物链 - 并查集的两种解法(详解)

动物王国中有三类动物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),输出假话的总数。 

Input

第一行是两个整数N和K,以一个空格分隔。 
以下K行每行是三个正整数 D,X,Y,两数之间用一个空格隔开,其中D表示说法的种类。 
若D=1,则表示X和Y是同类。 
若D=2,则表示X吃Y。

Output

只有一个整数,表示假话的数目。

Sample Input

100 7
1 101 1 
2 1 2
2 2 3 
2 3 3 
1 1 3 
2 3 1 
1 5 5

Sample Output

3

分析:

解法一:

和Find them,Catch them思路一样,题目连接:点击打开链接

但是这题有3种关系,所以要开3倍大的数组,设:x+2n吃x+n,x+n吃x,x吃x+2n;y+2n吃y+n,y+n吃y,y吃y+2n

 

如果x与y同类则

link(x,y);

link(x+n,y+n);

link(x+2*n,y+2*n);

 

如果x吃y,则

link(x+2*n,y);

link(x+n,y+2*n);

link(x,y+n);

每次输入后要先判断,再操作:

1 x y:判断x是否吃y,y是否吃x,都不满足的话,说明不是错误答案,则进行上述x与y同类的三步操作。

2 x y:判断x是否与y同类,y是否吃x,都不满足的话,说明不是错误答案,则进行上述x吃y的三步操作。

代码如下:

#include<iostream>
#include<cstdio>

using namespace std;
int pre[150003];

int f(int x){
    if(x==pre[x])return x;
    pre[x]=f(pre[x]);
    return pre[x];
}

void link(int a,int b){
    int root1=f(a),root2=f(b);
    pre[root1]=root2;
}

int main(){
    int n,k,flag,x,y;
    scanf("%d%d",&n,&k);
    int ans=0;
    for(int i=1;i<=3*n;i++){
        pre[i]=i;
    }
    while(k--){
        scanf("%d%d%d",&flag,&x,&y);
        if(x>n||y>n){ans++;continue;}
        if(flag==2&&x==y){ans++;continue;}

        if(flag==1){//x,y同类
            if(f(y)==f(x+2*n)||f(x)==f(y+2*n)){ans++;continue;}
            link(x,y);
            link(x+n,y+n);
            link(x+2*n,y+2*n);
        }
        else{
            if(f(x)==f(y)||f(x)==f(y+2*n)){ans++;continue;}
            link(x+2*n,y);
            link(x+n,y+2*n);
            link(x,y+n);
        }
    }
    printf("%d\n",ans);
}

解法二:

第一步:定义rel[i]为i和父亲的关系

rel[i]=x1---------i与父亲同级

rel[i]=x2---------i被父亲吃

rel[i]=x3---------i吃父亲

这样定义是有科学道理的,我们要把每一个正确的值与父结点串在一起,(无论什么关系),再用rel数组保存子与父的关系

题中有几种状态就考虑模几,这里假设模三。我们设的三种状态必须是从子->父,父->爷能推出来子->爷的关系。

枚举:子吃父,父被爷吃,则子爷同类-----------x3+x2=x1;

          子吃父,父吃爷,则子被爷吃-------------2*x3=x2;

          子被父吃,父被爷吃,则子吃爷-----------2*x2=x3;

第2,3个式子显然矛盾,所以要加上取模的思想,式子两边取模(x1,x2,x3要从0,1,2里选,因为再大的数模3后也是这三个数)

发现x1=0,x2与x3分别等于1,2都可。发现输入“2 x y”表示x吃y“1 x y”表示xy同类,把1,2都减一,恰好符合:

(这里我们是pre[y]=x; 把y归到x,rel[y]表示y与父结点x的关系,即1,则1表示y被x吃,即被父结点吃)

rel[i]=0---------i与父亲同级

rel[i]=1---------i被父亲吃

rel[i]=2---------i吃父亲

 

公式:rel[x](子到爷的关系)=(rel[x](子到父的关系)+rel[y](父到爷的关系))%3;

初始化时rel[i]=0; 表示自己和自己同类,并不影响以后的计算

第二步:路径压缩

我们说了我们要把每一个正确的值与父结点串在一起,(无论什么关系)用rel保存关系,那么问题来了:

我们要把3和1连到一起,那么可以在路径压缩的时候,更新rel[3]表示3到根节点1的关系(用步骤一枚举得到的公式:

rel[x](3到1的关系)=(rel[x](3到2的关系)+rel[y](2到1的关系))%3;)

第三步:对于输入数据的处理

判断真假话 
1) 当前的话与前面的某些真的话冲突,就是假话; 
2) 当前的话中X或Y比N大,就是假话; 

3) 当前的话表示X吃X,就是假话。 

2与3三直接判断,是假话continue;ans++;

对于1:

当我们输入x,y时,先求根节点

1)若两者根节点相同,说明我们已经赋予x,y关系了,只需要验证题中给的关系符合不符合我们已有的关系即可

此时知道rel[x]与rel[y],则x到y的关系是:(3+rel[x]+(-rel[y]))%3(根据向量的关系,也可以枚举一下)即判断(3+rel[x]-rel[y])%3与flag-1是否相等。加3是因为减的过程可能会出现负数。

2)若两者根节点不相同,说明我们还没有赋予x,y关系,令root1=f(x),root2=f(y);pre[root2]=root1,关键就是怎么求rel[root2]

这时我们换种角度思考,之前一直都是rel[x]表示x与x父的关系,现在我们把它想成x父与x的关系,向量的方向要变了!

如图,此时求的是e1,  d1=a1+b1 ;  e1=d1-c1 ; e1=(a1+b1-c1);

a1=rel[x] ; c1=rel[y] ; b1=flag-1;(题上给的x,y的关系)

现在就是要求-c1,已知子与根节点的关系,求根节点与子的关系

 子与根节点的关系      根节点与子的关系

        0                                0

        1                                2

        2                                1

得-c1=(3-rel[y]) %3;

综上:e1=( (rel[x]+flag-1) %3+(3-rel[y]) %3 )%3;

即rel[root2]=(rel[x]+flag-1+3-rel[y])%3;

好了,到这结束了,看下代码:

#include<iostream>
#include<cstdio>

using namespace std;
int pre[50003],rel[50003];

int f(int x){
    if(x==pre[x])return x;
    int tmp=pre[x];
    pre[x]=f(pre[x]);
    rel[x]=(rel[x]+rel[tmp])%3;
    return pre[x];
}

int link(int x,int y,int flag){
    int root1=f(x),root2=f(y);
    if(root1==root2){//表示已经合并啦
        if(flag!=(3-rel[x]+rel[y])%3)return 0;
        else return 1;
    }
    pre[root2]=root1;
    rel[root2]=(rel[x]+flag+3-rel[y])%3;
    return 1;
}

int main(){
    int n,k,flag,x,y;
    scanf("%d%d",&n,&k);
    int ans=0;
    for(int i=1;i<=n;i++){
        pre[i]=i;
        rel[i]=0;
    }
    while(k--){
        scanf("%d%d%d",&flag,&x,&y);
        if(x>n||y>n){ans++;continue;}
        if(flag==2&&x==y){ans++;continue;}
        if(!link(x,y,flag-1))ans++;
    }
    printf("%d\n",ans);
}

ps:感谢dalao给我讲解orz

  • 21
    点赞
  • 54
    收藏
    觉得还不错? 一键收藏
  • 10
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值