【并查集】POJ 1182 食物链

POJ 1182 : 食物链

由于题目中的N个动物都是A,B,C三类中的其中一种。题目有两种操作方式,【1】”1 X Y”,表示X和Y是同类,这个操作就是并查集中的【并】操作,【2】”2 X Y”,表示X吃Y,这个操作实际上是维护了森林中树之间的关系(类似有向图,此时树类比成一个顶点)。题目求这两个操作不成功的个数,假设为ans。

问题拆分

【1】进行1 X Y表示X和Y是同类之前,首先得判断X是否能吃Y或者Y是否能吃X。如果可以则证明操作不成功,ans累加1,否则将X和Y合并。
【2】2 X Y表示X吃Y时,首先得判断Y是否能吃X或者X和Y是否是同类。如果可以则证明操作不成功,ans累加1,否则建立X和Y的关系。

方法一

通过数组建立分类(A,B,C),首先1~N是A类,(N+1)~ 2 * N是B类,(2 * N + 1)~3 * N是C类。N是动物总数。
上面的判断可以分解为:
【1】X是否能吃Y:X在A类,Y在B类;X在B类,Y是C类;X在C类,Y在A类。
【2】Y是否能吃X:Y在B类,X在A类;Y在C类,X是B类;Y在A类,X在C类。
【3】X和Y是否是同类:X,Y在A类 ;X,Y在B类 ; X,Y在C类。

假设输入例子如下

100 8
2 1 2
2 2 3
1 1 4
1 2 5
1 3 6
1 3 9
2 3 2
1 1 2
1 1 3

执行过程演示图如下:
这里写图片描述

代码

#include <iostream>
#include <cstdio>
#include <map>
using namespace std;

const int MAXNUM = 3* (50000 + 10);
int id[MAXNUM];
int Size[MAXNUM];

// 初始化
void make_set(int n){
    for(int i = 1 ; i <= n ; i++){
        id[i] = i;
        Size[i] = 1;
    }
}

// 查找父节点
int Find(int p) {
    while (p != id[p]) {
        // 路径压缩,会破坏掉当前节点的父节点的尺寸信息,因为压缩后,当前节点的父节点已经变了
        id[p] = id[id[p]];
        p = id[p];
    }
    return p;
}

// 合并 p ,q节点
void Union(int p, int q) {
    int pRoot = Find(p);
    int qRoot = Find(q);

    if (pRoot == qRoot) {
        return;
    }
    // 按秩进行合并
    if (Size[pRoot] > Size[qRoot]) {
        id[qRoot] = pRoot;
        Size[pRoot] += Size[qRoot];
    } else {
        id[pRoot] = qRoot;
        Size[qRoot] += Size[pRoot];
    }
}

//判断x和y是否属于同一个集合
bool same(int x,int y){
    return Find(x) == Find(y);
}
int main(){
    //freopen("input.txt","r",stdin);
    //freopen("output.txt","w",stdout);
    int N,K;
    int t,x,y;
    scanf("%d %d", &N,&K);
    make_set(N * 3);
    int ans = 0;

    for(int i = 0;i < K; i++){
        scanf("%d %d %d",&t,&x,&y);

        if(x <= 0|| N < x || y <= 0 || N < y){
            ans++;
            continue;
        }

        //分析x和y是同类是否正确
        if(t == 1){
            //如果x吃y || y吃x就不正确
            if(same(x, y + N) || same(x, y + 2 * N)){
                ans++;
            }else{
                //x和y是同类  正确
                Union(x, y);    //如果x属于A,y就属于A
                Union(x+N, y+N);  //如果x属于B,y就属于B
                Union(x+N*2, y+N*2); //如果x属于C,y就属于C
            }
        }else{
            //分析x吃y是否正确

            //如果x和y是同类 || y吃x就不正确
            if(same(x, y) || same(x, y + 2 * N)){
                ans++;
            }else{
                Union(x,y + N);   //如果x属于A,那么y就属于B
                Union(x + N,y + 2 * N);  //如果x属于B,那么y就属于C
                Union(x + 2 * N, y);   //如果x属于C,那么y就属于A
            }
        }
    }
    printf("%d\n",ans);

    return 0;
}

结合演示图和代码可知,
(1)合并操作将会在这三棵树中执行,最终维护的树主要是这三棵,剩余的都是结点树为1的树。
(2)same函数的比较,都是比较x和y + N,y + 2 * N的根节点是否相同。也就是说在same函数中在A类和B类,C类的根节点进行比较。
(3)在某棵树下的类型和在整个森林下的类型不一样,这个很关键,也是理解整个操作的关键。假设1号是A类,2号是B类,3号是C类,N=100,从而1吃2,2吃3,3吃1,这些在题意和例子中可以得到的假设。这三棵树从左到右编号分别是f1,f2,f3。

那么在f1中,1号是C类(因为201,204在2 * N + 1 ~ 3 * N之间),2号是A类(因为2,5在1 ~N之间),3号是B类(103,106,109在N + 1 ~ 2 * N之间)。
在f2中,1号是A类(因为1,4在1 ~N之间),2号是B类(因为102,105在N + 1 ~ 2 * N之间),3号是C类(因为203,206,209在2 * N + 1 ~ 3 * N之间)。
在f3中,1号是B类(因为101,104在N + 1 ~ 2 * N之间),2号是C类(因为202,205在2 * N + 1 ~ 3 * N之间),3号是A类(因为3,6,9在1 ~N之间)。

(4)由(3)可以得知(x, y + N)就是假设在某个树下,x是A类,y是B类,如果根节点相同则证明在同一颗树,同时证明含有x吃y的关系。而same(x, y + 2 *N)类似。
(5)演示图的结果不是按照秩去合并,只是为了更加直观看清楚效果,并不影响最终结果。

方法二:

参考:https://blog.csdn.net/freezhanacmore/article/details/8767413
把确定了相对关系的节点放在同一棵树中

每个节点对应的 r[]值记录他与根节点的关系:
0:同类,
1:被父亲节点吃,
2: 吃父亲节点

每次输入一组数据 d, x, y判断是否超过 N 后,先通过find()函数找他们的根节点从而判断他们是否在同一棵树中。(也就是是否有确定的关系)

1.如果在同一棵树中find(x) == find(y):直接判断是否说谎。

1)如果 d ==1,那么 x 与 y 应该是同类,他们的r[]应该相等

如果不相等,则说谎数 +1

2)如果 d==2,那么 x 应该吃了 y,也就是 (r[x]+1)%3 == r[y]

如果不满足,则说谎数 +1
如何判断 x 吃了 y 是 (r[x]+1)%3 == r[y],请看下图:(PS:箭头方向指向被吃方)

这里写图片描述
2.如果不在同一棵树中:那么合并 x 与 y 分别所在的树。
合并树时要注意顺序,我这里是把 x 树的根当做主根,否则会WA的很惨

注意:找父亲节点时,要不断更新 r[]的值。

这里有一个关系:如果 x 和y 为关系 r1, y 和 z 为关系 r2
那么 x 和z的关系就是 (r1+r2)%3
如何证明?

无非是3*3 = 9种情况而已
(a, b) 0:同类 、 1:a被b吃 、 2:a吃b

(x, y) (y, z) (x,z) 如何判断
0 0 0 0+0 = 0
0 1 1 0+1 = 1
0 2 2 0+2 = 2
1 0 1 1+0 = 1
1 1 2 1+1 = 2
1 2 0 (1+2)% 3 = 0
2 0 2 2+0 = 2
2 1 0 (2+1)% 3 = 0
2 2 1 (2+2)% 3 = 1

关于合
并时r[]值的更新:

如果 d == 1则 x和y 是同类 ,那么 y 对 x 的关系是 0
如果 d == 2 则 x 吃了 y, 那么 y 对 x 的关系是 1, x 对 y 的关系是 2.
综上所述 ,无论 d为1 或者是为 2, y 对 x 的关系都是 d-1

定义 :fx 为 x 的根点, fy 为 y 的根节点

合并时,如果把 y 树合并到 x 树中
如何求 fy 对 fx 的r[]关系?
fy 对 y 的关系为 3-r[y]
y 对 x 的关系为 d-1
x 对 fx 的关系为 r[x]
所以 fy 对 fx 的关系是(3-r[y] + d-1 + r[x])%3

这里写图片描述

代码

#include <iostream>
#include <cstdio>
#include <map>
using namespace std;

const int MAXNUM = 50000 + 10;
int id[MAXNUM];
int Size[MAXNUM];
int r[MAXNUM];//存与父节点的关系 0 同一类,1被父节点吃,2吃父节点
// 初始化
void make_set(int n){
    for(int i = 1 ; i <= n ; i++){
        id[i] = i;
        Size[i] = 1;
        r[i] = 0;
    }
}

// 查找父节点
int Find(int p) {
    while (id[p] != id[id[p]]) {   //如果q不是其所在子树的根节点的直接孩子
        r[p] = (r[p] + r[id[p]] ) % 3;
         id[p] = id[id[p]];          //对其父节点到其爷爷节点之间的路径进行压缩
      }
    return id[p];
}

// 合并 p ,q节点
void Union(int p, int q, int d) {
    int pRoot = Find(p);
    int qRoot = Find(q);

    if (pRoot == qRoot) {
        return;
    }
    id[qRoot] = pRoot;
    r[qRoot] = (r[p] - r[q] + 3 + (d - 1)) % 3;
}

int main(){
    //freopen("input.txt","r",stdin);
    //freopen("output.txt","w",stdout);
    int N,K;
    int d,x,y;
    scanf("%d %d", &N,&K);
    make_set(N);
    int ans = 0;

    for(int i = 0;i < K; i++){
        scanf("%d %d %d",&d,&x,&y);

        if(x <= 0|| N < x || y <= 0 || N < y){
            ans++;
            continue;
        }
        if(Find(x) == Find(y)){
            // 不是同类
            if(d == 1 && r[x] != r[y])
                ans++;
            // 如果 x 没有吃 y
            if(d == 2 && (r[x] + 1) % 3 != r[y])
                ans++;
        }else{
            Union(x,y,d);
        }
    }
    printf("%d\n",ans);

    return 0;
}

猜你喜欢

转载自blog.csdn.net/u013075699/article/details/80324085
今日推荐