POJ 1182 食物链 【带权并查集】

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

题意:A能吃B,B能吃C,C能吃A,输入几个说法,D = 1,则 x 和 y 是同类,如果D = 2,则x 能吃 y;说法有多个,问有几个说法是和前面说过的说法相矛盾的;

这道题有两种解法,第一种解法是在《挑战程序设计》里面有提到的,还有另一种是在博客看到的,先看第一种;

开3*N的数组空间, 1到N,表示的是N个动物A的关系集合,N+1到2*N是N个动物B的关系集合,2*N+1到3*N是N个动物C的关系集合,如果表示x和y是同类,因为我们无法断定x和y到底是什么动物,这里绝对不能随便假设x是某个动物,是会影响最后的结果的;既然不知道的话,我们可以把三种情况都考虑进去,假设x和y都是A动物,那么合并(x,y),假设都是B动物,那么合并(x+N,y+N),假设都是C动物,那么合并(x+2*N, y+2*N);如果是x吃y的关系又怎么处理,因为不是同类关系,那么这里我们要合并的,是另外两个集合的数,比如假设x是A动物,y是B动物,那么合并(x,y+N);假设x是B动物,那么合并(x+N, y+2*N);以此类推,这是合并的规则,可能会有很多人不懂为什么要这样合并数据,我们这里先把大致的步骤先看一遍在思考一下;那么我们如何去判断当前的说法是否与之前的说法相矛盾?假设当前说法是 x和y同类,如果x和y真的同类的话,那么三个集合就一定不会出现x吃y或者y吃x在一个集合的情况,因为我们每次合并的时候,都是三个集合同时操作的,A吃B成立,另外两个同样成立;如果我们之前有处理过x吃y的情况,那么,第一个集合就一定会出现(x和y+N)这两个数,我们判断的时候只需要判断三个中其中一个集合就行了,因为x和y+N在A集合里面,那么x+N和y+2*N也一定在B集合里面,由此,我们判断x和y是否矛盾,只要判断之前有没有处理过x吃y或者y吃x的情况,有则矛盾,没有则成立,然后按照刚刚的步骤合并两个同类的数据;同样的,判断x吃y是否矛盾,我们只需判断之前是否处理过x和y同类,或者y吃x的情况;

#include<cstdio>
#include<cstring>
#include<map>
#include<iostream>

using namespace std;

#define Maxn 50005

int pre[Maxn*3];

void make (int x) {
    pre[x] = x;
}

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

void union_ (int x,int y) {
    int xx = Find (x);
    int yy = Find (y);

    if (xx == yy) return ;
    pre[yy] = xx;
}

void init (int x) {
    for (int i = 1; i <= x*3; ++i) make (i);
}

bool check (int x, int y) {
    if(Find (x) == Find (y)) return true;
    else return false;
}

int main(void )
{
    int N,k,D,x,y,cnt;
    scanf("%d%d",&N,&k);

    init (N); cnt = 0;

    while (k--) {
        scanf("%d%d%d",&D,&x,&y);

        if(x > N || y > N) { cnt++; continue; }
        if(D == 2 && x == y) { cnt++; continue; }
        if(D == 1 && x == y) continue;

        if(D == 1) {
            if(check (x,y+N) || check (y,x+N)) cnt++;
            else {
                union_ (x,y);
                union_ (x+N,y+N);
                union_ (x+2*N,y+2*N);
            }
        }
        if(D == 2) {
            if(check (x,y) || check (y,x+N)) cnt++;
            else {
                union_ (x,y+N);
                union_ (x+N,y+2*N);
                union_ (x+2*N,y);
            }
        }
    }
    printf("%d\n",cnt);
    return 0;
}

第二种方法就比较高级一点,适用于很多难度大点的并查集问题;

https://blog.csdn.net/niushuai666/article/details/6981689

这篇博客已经写的很好了,有兴趣的可以去学习一下;

第二种方法,有一个非常关键的结论:如果你设置的关系值是数字,而且是相邻的话,就能以向量的形式去相加权值求出最末尾那个节点与第一个节点之间的关系值;在上面的博客里面有讲到这个向量相加的原理是什么;

我们假设  ret = 0 表示为子节点与父节点同类, ret = 1 表示父节点吃子节点;ret = 2 表示父节点被子节点吃;我在之前的博客也有提到这个并查集的结构,其实只有两层,也就是说,并查集的所有子节点都是直接指向父节点的;我们这里假设有两个集合,rootx-  >x   ; rooty - > y;  如果x与y之前存在关系的话,那么就有  rootx->rooty = root->x + x->y + y->rooty;我们怎么去理解这个结论? 看看图片;

我们假设 x的ret = 1;也就是rootx吃x;y的ret = 1,也就是 rooty吃y,这里的黑色箭头是最初始的关系方向,这里方向十分重要;左边集合的箭头的权值是x的ret值,右边的箭头的权值是y的ret值;如果x和y存在着关系,假设是 x吃y,那么我们合并的时候,就一定要去改变那个被合并集合的父节点的ret值,我们假设rooty合并到rootx中,那么rooty对应于rootx的关系就可能不是目前rooty的ret关系值了,这个时候我们就要用到上面的结论来判断rooty的ret应该怎么改;要想知道rootx->rooty的关系值,我们需要对rooty->y的这个方向做一下整改,因为rootx->rooty = rootx->x + x->y + y->rooty;也就是说y->rooty要做反向处理,因为关系就只有0,1,2三个值,当 y的ret是1的时候,要想反向, ret值只要改成 2就行了,因为1是rooty吃x,而反向就是x吃rooty,如果y的ret值是2也是同样的道理,如果是0的话,也就是同类的情况下,是不影响的,那么我们这里应该如何处理这三种情况的反向? (3-ret)%3,可以在纸上算一下,这个表达式就能做到反向的处理,那我们来试一下,如果x吃y,那么y对x的ret值就是 1;那么 rootx->rooty = (1 + 1 + (3-1) ) %3 = 1;也就是说 rootx 和rooty 的关系是rootx吃rooty;看看是不是?有疑惑的可以多试几次,验证一下这个表达式的正确性;我们到这里就能够判断出rooty对rootx的关系值;我们要做的就是直接把rooty的父节点指向rootx,并且修改rooty的ret值,那其他的节点怎么办? 回想一下并查集的Find()的操作,其实跟并查集的压缩路径相似,也可以通过这种压缩路径的方式给其他的节点的ret值更新一遍;因为在合并过后,所有的节点的ret值,除了rooty外,都是没有改变的,方向也是没有改变的;我们一样可以用刚刚那个表达式来写一个更新ret的代码;合并过后,y的父节点还是指向rooty的,当再经历一遍Find()的时候,y的父节点就会指向rootx,那么就会有这样的关系;

我们要求的就是蓝色箭头的权值,也就是y对rootx的关系ret,同样的利用刚刚的那个表达式,rootx->y = rootx->rooty + rooty -> y; 上面的理解了,这里的就不会很难懂;合并的部分解决了,更新的部分解决了,剩下的就是判断是否矛盾的部分了,已经确立好关系的节点一定都在一个集合里面,如果不在一个集合那么就合并他们,而且是一定不矛盾的,如果在一个集合里面,当D == 1 的时候,只要判断两个节点的关系ret相不相等就能判断是否矛盾,如果D == 2 那么说明x吃y,我只要判断x 的ret是否等于y的ret或者有没有y吃x的情况;这里y吃x的情况,也是通过上面那个向量关系的表达式来判断的;应该就这么多了,下面看看代码,其实主要还是看上面的思路;

#include<cstdio>

using namespace std;

#define Maxn 50005

struct Pre {
    int F,ret;
} pre[Maxn];

void make (int x) {
    pre[x].F = x;
    pre[x].ret = 0;
}

int Find (int x) {
    int tmp = pre[x].F; // 当节点去找父节点的时候,pre[x].F的值就已经是父节点,
    if(pre[x].F != x) { // 所以要把当前点的上一个关系节点保存下来
        pre[x].F = Find (pre[x].F);
        pre[x].ret = (pre[tmp].ret + pre[x].ret)%3; // 更新关系
    }
    return pre[x].F;
}

void union_ (int x, int y,int D) {
    int xx = Find (x);
    int yy = Find (y);

    if(xx == yy) return;
    pre[yy].ret = (pre[x].ret + D-1 + (3-pre[y].ret))%3; // 改变父节点的ret,子节点的ret不变,但是会在Find的时候
    pre[yy].F = xx;                                         //逐个更新关系
}

int main(void)
{
    int N,k,D,x,y,cnt = 0;
    scanf("%d%d",&N,&k);
    for (int i = 1; i <= N; ++i) make (i);
    while (k--) {
        scanf("%d%d%d",&D,&x,&y);
        if(D == 2 && x == y) { cnt++; continue; }
        else if(x > N || y > N) { cnt++; continue; }
        else if(D == 1 && x == y) continue;

        if(Find (x) != Find (y)) {  
                union_ (x,y,D);
                continue;
        }

        if(D == 1 && pre[x].ret != pre[y].ret) cnt++; // 如果同类,但是关系不相等,就矛盾
        else if(D == 2) {
            int tmp = Find (x);  //这里的tmp是x的父节点
            if(pre[x].ret == pre[y].ret ||            // 如果x吃y,只要有x与y同类或者y吃x就矛盾
               ((pre[y].ret+D-1+(3-pre[x].ret))%3 == pre[tmp].ret)) cnt++;
        }
    }
    printf("%d\n",cnt);
    return 0;
}

猜你喜欢

转载自blog.csdn.net/godleaf/article/details/81256445