【题解】食物链(Poj 1182)·两种方法的并查集(超级简单,而且非常容易理解)·C/C++

hi,这里是大千小熊,一只很かわい的小熊。欢迎您的关注。(小声:如果您乐意欢迎关注我的B站账号


原题目链接: http://poj.org/problem?id=1182

题目描述:

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

输入:

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

输出:

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

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

Output:

3

第一种题解:

本题目中,实际上到底谁是A,B,C这种问题是无关紧要的。

为什么呢,我们假设这个世界有很多的平行世界,那么如果说 1 吃 2,那么 1 如果是 A ,那么 2 就应该是 B。如果 1 是B,那么 2 就是C。如果 1 是C,那么 2 就是A。

现在我们用1_A,表示1是A。那么1_B表示1是B。

按照平行世界的原理,我们可以有这样的三个集合:

(1)1_A 2_B

(2)1_B 2_C

(3)1_C 2_A

上面的每一个集合,都意味着这些假设是一定会成立的。比如集合(1)就意味着,1是A的话,那么2一定是B。

现在让我们想想怎么去维护这一个并查集。

D==1 合并关系,那么他们就不能是敌对的关系,x和y不能是互相吃对方。比如x_A和y_B是不能在一个集合的。

D==2 x吃y的关系,那么首先x_A和y_A不能在一个集合。其次x_B和y_A也不能同时在一个集合之中。

那么就是在每次的操作之前,想想这种操作可不可以发生,如果不能发生,那么ans++。如果可以发生,就继续维护这种动物之间的关系。

开辟一个大小为3*N的数组,那么这个数组前1-N的意思是:1_A 2_A 3_A … N_A

然后N+1到2*N的意思是1_B 2_B … N_B

最后的2*N+1到3*N的意思是1_C 2_C … N_C

参考代码(第一种):

//并查集练习题
#include <algorithm>#include<cmath>#include<cstdio>#include<cstdlib>#include<iostream>using namespace std;
unsigned int N = 0, M = 0, ans = 0;
int par[150015] = {0};
//设计了一个数据结构表示1_A=1 1_B=1+1*N 1_C=1+2*N
//用一个集合表示情况的发生,比如假设1是A,那么2一定是B,就把1_A和2_B放在一个集合里面s
int find(int x) {
    if (x == par[x]) {
        return x;
    } else {
        return par[x] = find(par[x]);
    }
}
void Join(int x, int y) {
    int fx = find(x), fy = find(y);
    int ft = min(fx, fy);
    par[fx] = ft;
    par[fy] = ft;
}
bool Same(int x, int y) { return (find(x) == find(y)); }
int main() {
    scanf("%d%d", &N, &M);
    for (int i = 1; i <= 3 * N; i++)
        par[i] = i;
    for (int i = 1; i <= M; i++) {
        int D = 0, X = 0, Y = 0;
        scanf("%d %d %d", &D, &X, &Y);
        if (X > N || X < 1 || Y > N || X < 1) {
            ans++;
            continue;
        }
        if (D == 1) {
            if (Same(X, Y + N) || Same(Y, X + N)) {
                ans++;
            } else {
                Join(X, Y);
                Join(X + N, Y + N);
                Join(X + 2 * N, Y + 2 * N);
            }
        } else if (D == 2) {
            if (Same(Y, X + N) || Same(X, Y)) {
                ans++;
            } else {
                Join(X, Y + N);
                Join(X + N, Y + 2 * N);
                Join(X + 2 * N, Y);
            }
        }
    }
    printf("%d\n", ans);
    return 0;
}

第二种题解:

(这种解法在B站没更新,因为那个时候还不理解这个想法,捂脸)
本题目您一定注意到了,其实还有其他的想法,而且也同样的简单。
现在让我们想一想,到底什么才是“并查集”。

并查集的本质是“将一定有关系的元素合并在一起”。

那么本题目中什么才算有关系呢?
我们现在来思考一下。
首先,每次说的第一句话,如果不是边界错误,那么它一定是正确的。
我想您一定是同意的,因为初始的时候,大家(元素)之间互相都没有关系。只有您曾经说过他们之间的关系(比如是同类,X吃Y),他们才会扯上关系。
在这里插入图片描述
那么好,我来指定一个关系,1吃2
在这里插入图片描述
现在1和2指定上了关系(1吃2的关系)。
那么现在我一次指定1和2之间的关系,才有可能存在假话。

第一步的程序设计:

判断X和Y是不是之前存在关系,如果之前不存在关系,那么他(出题人)说X和Y是什么关系都是对的
(反对者的声音:为什么X和Y一定是对的呢?解答者:因为并查集是一类关系的集合,我们可以查询X和Y节点的根,如果X和Y不在一个同样的根,那么他们之间本来就不存在一个确切的关系,当然出题者说什么都是对的。)

第二步的程序设计:

我们如果去维护一个1吃2的关系呢?也就是,我们在这个并查集上面如何去知道2(叶子节点)这个节点对于1(根节点)的关系呢?

2.1首先,无论x和y是A,B,C的一种,他们之间一定存在吃被吃相同这3种的一种。
C(3,2)=3。排除相同,那么一定是AB,AC,BC。
现在我们假设A对应0,B对应1,C对应2。
即:A,B,C三个节点被命名为0,1,2
重点:
并且这个关系中小的值吃大的值。

那么如果我们现在创建一种关系1吃2 2吃3 3吃4 4吃5 5吃6
现在问:1和6是什么关系。
打住!我们不写代码,我们手算。
假设1的动物是0(就是A类 A->0)那么既然1吃2,那么2肯定是1(B类 B->1)
如果2(B类 B->1)吃3,那么3肯定是(C类 C->2)
那么3吃4,4是…
D类?????????????当然不是啦,根本没有D类,根据我们所知道的,他们所有的关系只能在0,1,2
实际上我们知道的D应该就是A。D是3,可是A_0 B_1 C_2
怎么更正D的值呢????????
聪明的你一定看出来了。0,1,2是一个循环,我们不可能超过这个循环。
所以3虽然不是1,2,3但是3%3==0,不就是A类吗??
所以我们这样设计一个并查集节点的结构。
这个并查集里面存在两个int,一个是指向前驱节点,一个是指向前驱节点的值(相当于树的边的权值)(怎么有点图论的味道 )。
在这里插入图片描述
这里话,也就是说6号节点是C,1号节点是A那么6号节点吃一号节点。
所以我们这样设计:

如果现在x和y是同类:
在这里插入图片描述
如果x吃y:
在这里插入图片描述
(比如现在x是1,那么y就是2,1是吃2的(B吃C),如果x是2,那么y就是0((2+1)MOD3=0)也就是x是C,y是A,C是吃A的。)

如果x被y吃:
您当然可以-1。但是,实际上+2也是一样的,为了保证统一性(通常MOD运算是很怕负数的),所以我们+2和-1效果一样。您可以想象一个手表,无论往前拨动还是往后拨动都是可以到达同一位置。
在这里插入图片描述
现在我们可以很荣耀的说出,如果存在这样一个树,那么任何一个节点相对于根节点的关系我们都可以求出。
在这里插入图片描述
比如这个图,我们可以这样:我们就认为1是A类(因为实际上A,B,C是无所谓的,题目本身不要求知道一个节点的确切种类,也没指定一个节点的确切种类。)
那么3号节点是在1号节点上面加2出来,所以3号节点是2(C类,因为我指定1号节点是C类)
那么4号节点,如果说1是A类,那么4号节点(1+2)MOD3=0(A类)。
所以1和4是同类。
这就告诉我们,查询一个节点的关系,就是把这天路径上面的权值全部加起来。
在这里插入图片描述
比如4号节点,您应该加上这些。

第三步程序设计:

我该如何合并这些关系呢???
(留个读者自行思考)
骗你的啦~
在这里插入图片描述
现在我想干一件事情将y合并到x上面。
按照3和6是同类的关系合并。
那么现在您要不然承认X的根节点是A,要不然您承认Y的根节点是A。
不过现在我们把Y合并到X上面所以我们承认X的根节点是A。
在这里插入图片描述
那么5号节点的PreSum(权值)现在需要更正。
更正一个数字使得,在以1为A的这个根节点。相对于根节点3号和6号是2(C类)
很显然?=1
现在全部更正为:
在这里插入图片描述
我们说好的:在第一种解法中我们就明白A,B,C这种关系不是绝对的,而是相对的。
不信您看,y这颗树依旧是成立的。
现在我们的焦点就是合并的时候这个?该是多少呢。
其实很简单。

手表拨动原理:

在这里插入图片描述

我们想像一个手表有0,1,2这3个刻度。
我们现在停在a,我们想把手表的指针指向b。
我们要拨动几个刻度呢?
我们规定我们只能正的拨动(顺时针)不能反过来。
如果b>=a,我们正着波动,顺时针拨动b-a。
如果b<a,我们这样拨动,先顺时针拨动3下,然后偷偷回拨(b-a)个。只要速度够快,您看不出来我是偷偷往回拨动的。(笑)
公式:?=(b-a+3)MOD 3
很明显a是Y树上面y的对应根的关系的权值。

现在我们的问题是b是几呢?

如果我们合并的是同类关系,那么在X树上我们想要合并的x里面所存放的值就是b。
如果是x吃y,那么我们知道小的吃大的。
那么就是X树上x节点对应RootX的值要再加上一,也就是b+1。这样看起来x就小了,y就比x大了一(b是目标,目标比x的值还要大了一,确定了x和y之间的关系)。

如果您有不懂的地方,请您反复阅读,下面的代码是详细注释的。

参考代码(第二种):

#include <iostream>
using namespace std;
int N, M, RootSum = 0, Tie = 0;
#define MAX_N 50002
struct Node {//设计一个结构,不仅有前驱节点的信息,还有权值。
    int Pre, PreSum;
} Tree[MAX_N];
int FindR(int x) {//查找根节点,并把路径上面的值加上。
    if (x == Tree[x].Pre) {
        return x;
    } else {
        RootSum = (RootSum + Tree[x].PreSum) % 3;
        return FindR(Tree[x].Pre);
    }
}
int FindRoot(int x) {
    RootSum = 0;//怕自己忘记更新RootSum=0,所有写了一个FindRoot函数,这样避免使用FindR忘记更新。
    Tree[x].Pre = FindR(x);//更新前驱节点(父节点)
    Tree[x].PreSum = RootSum;//查找根节点顺便把这条路上面的所有的权值加起来。
    return Tree[x].Pre;
}
bool XEatY(int x, int y) {//判断X是不是吃Y,如果吃就return 1。
    x = Tree[x].PreSum;
    y = Tree[y].PreSum; 
    return ((x == 0 && y == 1) || (x == 1 && y == 2) || (x == 2 && y == 0));
}
int main() {
    scanf("%d %d", &N, &M);
    for (int i = 1; i <= N; i++) Tree[i].Pre = i, Tree[i].PreSum = 0;
    for (int i = 1; i <= M; i++) {
        int D = 0, X = 0, Y = 0;
        scanf("%d %d %d", &D, &X, &Y);
        if (X < 1 || X > N || Y < 1 || Y > N) {
            Tie++;//说谎的次数。
            continue;
        }
        int RootX = FindRoot(X), RootY = FindRoot(Y);//找一下他们的集合,并且在这个过程也把他们挂在了根节点上面
        if (RootX == RootY) {
            if (D == 1) {
                if (Tree[X].PreSum != Tree[Y].PreSum) Tie++;
            } else if (D == 2) {
                if (!XEatY(X, Y)) Tie++;
            }
        } else {  //两个节点之前没有关系,他说什么都是对的。
            if (D == 1) {
                //把y的根节点挂在x的根节点上
                Tree[RootY].Pre = RootX;
                Tree[RootY].PreSum = (Tree[X].PreSum - Tree[Y].PreSum + 3) % 3;//合并的时候重新算一下,他们之间的关系。我这里默认把Y的根节点挂在X的根节点上面。无所谓。您想反过来也可以。您如果看不懂公式是怎么推到的,您可以查阅:“手表原理”。
            } else if (D == 2) {
                //维护关系X,吃Y,还是按照把y的根节点挂在x的根节点,Y的PreSum应该比X的PreSum大一
                Tree[RootY].Pre = RootX;
                Tree[RootY].PreSum = (Tree[X].PreSum + 1 - Tree[Y].PreSum + 3) % 3;
            }
        }
    }
    printf("%d", Tie);
    return 0;
}
  • 3
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值