zoukankan      html  css  js  c++  java
  • POJ 1182 食物链(带权并查集)

    题目链接

    题意 : 中文题不详述。

    思路 : 转载自……

    Part I - 权值(relation)的确定。
    我们根据题意,森林中有3种动物。A吃B,B吃C,C吃A。
    我们还要使用并查集,那么,我们就以动物之间的关系来作为并查集每个节点的
    权值。
    注意,我们不知道所给的动物(题目说了,输入只给编号)所属的种类。
    所以,我们可以用动物之间“相对”的关系来确定一个并查集。
    0 - 这个节点与它的父节点是同类
    1 - 这个节点被它的父节点吃
    2 - 这个节点吃它的父节点。

    注意,这个0,1,2所代表的意义不是随便制定的,我们看题目中的要求。
    说话的时候,第一个数字(下文中,设为d)指定了后面两种动物的关系:
    1 - X与Y同类
    2 - X吃Y

    我们注意到,当 d = 1的时候,( d - 1 ) = 0,也就是我们制定的意义
    当 d = 2的时候,( d - 1 ) = 1,代表Y被X吃,也是我们指定的意义。
    所以,这个0,1,2不是随便选的


    Part II - 路径压缩,以及节点间关系确定
    确定了权值之后,我们要确定有关的操作。
    我们把所有的动物全初始化。
    struct Animal
    {
    int num; //该节点(node)的编号
    int parent; //该node的父亲
    int relation; //该node与父节点的关系,0同类,1被父节点吃,2吃父节点
    }; Animal ani[50010];
    初始化为
    For i = 0 to N do
    ani[i].num = i;
    ani[i].parent = i;
    ani[i].relation = 0 ; //自己和自己是同类
    End For

    (1)路径压缩时的节点算法
    我们设A,B,C动物集合如下:(为了以后便于举例)
    A = { 1 , 2 , 3 ,4 ,5 }
    B = { 6 , 7 , 8 ,9 ,10}
    C = { 11, 12, 13,14,15}
    假如我们已经有了一个集合,分别有3个元素
    SET1 = {1,2},我们规定集合中第一个元素为并查集的“代表”
    假如现在有语句:
    2 2 6
    这是一句真话
    2是6的父亲
    ani[6].parent = 2;
    ani[6].relation = 1;
    那么,6和1的关系如何呢?
    ani[2].parent = 1;
    ani[2].relation = 0;
    我们可以发现6与2的关系是 1.
    通过穷举我们可以发现
    ani[now].parent = ani[ani[now].parent].parent;
    ani[now].relation = ( ani[now].relation + ani[now.parent].relation ) % 3;
    这个路径压缩算法是正确的
    关于这个路径压缩算法,还有一点需要注意的地方,我们一会再谈
    注意,根据当前节点的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
    这就是路径压缩的节点算法
    (2) 集合间关系的确定
    在初始化的时候,我们看到,每个集合都是一个元素,就是他本身。
    这时候,每个集合都是自洽的(集合中每个元素都不违反题目的规定)
    注意,我们使用并查集的目的就是尽量的把路径压缩,使之高度尽量矮
    假设我们已经有一个集合
    set1 = {1,2,7,10}
    set2 = {11,4,8,13},每个编号所属的物种见上文
    set3 = {12,5,4,9}
    现在有一句话
    2 13 2
    这是一句真话,X = 13,Y = 2
    我们要把这两个集合合并成一个集合。
    直接
    int a = findParent(ani[X]);
    int b = findParent(ani[Y]);
    ani[b].parent = a;
    就是把Y所在集合的根节点的父亲设置成X所在集合的根节点。
    但是,但是!!!!
    Y所在集合的根结点与X所在集合的根节点的关系!!!要怎么确定呢?
    我们设X,Y集合都是路径压缩过的,高度只有2层
    我们先给出计算的公式
    ani[b].relation = ( 3 - ani[Y].relation + ( d - 1 ) + ani[X].relation) % 3;
    这个公式,是分三部分,这么推出来的
    第一部分,好理解的一部分:
    ( d - 1 ) :这是X和Y之间的relation,X是Y的父节点时,Y的relation就是这个
    3 - ani[Y].relation = 根据Y与根节点的关系,逆推根节点与Y的关系
    这部分也是穷举法推出来的,我们举例:
    j
    子 父相对于子的relation(即假如子是父的父节点,那么父的relation应该是什么,因为父现在是根节点,所以父.relation = 0,我们只能根据父的子节点反推子跟父节点的关系)
    0 ( 3 - 0 ) % 3 = 0
    1(父吃子) ( 3 - 1 ) % 3 = 2 //父吃子
    2(子吃父) ( 3 - 2 ) % 3 = 1 //子吃父,一样的哦亲
    ——————————————————————————————————————————————————————
    我们的过程是这样的:
    把ani[Y],先连接到ani[X]上,再把ani[Y]的根节点移动到ani[X]上,最后,把ani[Y]的根节点移动到ani[X]的根节点上,这样算relation的
    还记得么,如果我们有一个集合,压缩路径的时候父子关系是这么确定的
    ani[爷爷].relation = ( ani[父亲].relation + ani[儿子].relation ) % 3
    我们已知道,( d - 1 )就是X与Y的relation了
    而 (3 - ani[Y].relation)就是 以Y为根节点时,他的父亲的relation
    那么
    我们假设把Y接到X上,也就说,现在X是Y的父亲,Y原来的根节点现在是Y的儿子
    Y的relation + ani[Y]根节点相对于ani[Y]的relation
    ( ( d - 1 ) + ( 3 - ani[Y].relation) ) % 3
    就是ani[Y]的父亲节点与ani[X]的relation了!

    那么,不难得到,ani[Y]的根节点与ani[X]根节点的关系是:
    ( ( d - 1 ) + ( 3 - ani[Y].relation) + ani[X].relation ) % 3 ->应用了同余定理
    注意,这个当所有集合都是初始化状态的时候也适用哦
    还是以最开头我们给的三个集合(分别代表三个物种)为例
    2 1 6
    带入公式
    ani[6].relation = ( ( 2 - 1 ) + ( 3 - 0 ) + 0 ) % 3 = 1
    也就是,6被1吃
    Part III - 算法正确性的证明
    首先,两个自洽的集合,合并以后仍然是自洽的
    这个不难想吧,数学上有个什么对称性定理跟他很像的。
    如果理解不了,就这么想!!
    当set1和set2合并之后,set2的根节点得到了自己关于set1根节点的
    正确relation值,变成了set1根节点的儿子,那么
    set2的所有儿子只要用
    ( ani[X].relation + ani[Y].relation ) % 3就能得到自己正确的relation值了
    所以说,针对不在同一集合的两个元素的话,除非违背了(2)和(3),否则永远是真的
    (无论这句话说的是什么,我们都可以根据所给X,Y推出两个子节点之间应有的关系,这个关系一确定,所有儿子的关系都可以确定)

    其实所有的不同集合到最后都会被合并成一个集合的。
    我们只要在一个集合中找那些假话就可以了。
    首先,如何判断
    1 X Y是不是假话。//此时 d = 1
    if ( X 和 Y 不在同一集合)
    Union(x,y,xroot,yroot,d)
    else
    if x.relation != y.relation ->假话
    其次,如何判断
    2 X Y是不是假话 //此时d = 2
    if ( X 和 Y 不在同一集合)
    Union(x,y,xroot,yroot,d)
    else
    (ani[y].relation + 3 - ani[x].relation ) % 3 != 1 ->假话
    这个公式是这么来的:
    3 - ani[x].relation得到了根节点关于x的relation
    ani[y] + 3 - ani[x].relation得到了y关于x的relation
    所以,只要y关于x的relation不是1,就是y不被x吃的话,这句话肯定是假话!

    (2)路径压缩要特别注意的一点(错在这里,要检讨自己)
    路径压缩的时候,记得要
    先findParent,再给当前节点的relation赋值。
    否则有可能因为当前节点的父节点的relation不正确而导致错的稀里哗啦。
    例子:
    set1 = {1,2,7,10}
    set2 = {3,4,8,11}
    set3 = {12,5,14,9}
    Union(1,3,1,3,1)
    Union(3,12,3,12,2)
    1 5 1
    算5的relation
    如果不先更新parent的relation,算出来应该是
    ( 3 - 0 + 0 + 1 ) % 3 = 1,5被1吃,显然不对
    这里面,+ 0的那个0是指根节点 12 的relation(未更新,这里的0是指12与11的relation)
    如果更新完了的话,应该是
    ( 3 - 0 + 2 + 1 ) % 3 = 0 ,5与1是同一物种,对了
    这里面的 2 是更新节点12的relation(12与1的relation)

     1 #include <cstdio>
     2 #include <cstring>
     3 #include <iostream>
     4 
     5 using namespace std ;
     6 
     7 int father[50100] ,rankk[50100] ;
     8 int N,K,cnt;
     9 
    10 void Init()
    11 {
    12     cnt = 0 ;
    13     memset(rankk,0,sizeof(rankk)) ;
    14    for(int i = 0 ; i <= N ; i++)
    15         father[i] = i ;
    16 }
    17 int Find(int x)
    18 { 
    19     if(x != father[x])
    20     {
    21         int fx = Find(father[x]) ;
    22         rankk[x] = (rankk[x] + rankk[father[x]]) % 3 ;
    23         father[x] = fx ;
    24     }
    25     return father[x] ;
    26 }
    27 int mergee(int x,int y,int flag)
    28 {
    29     int fx = Find(x) ;
    30     int fy = Find(y) ;
    31     if(fx == fy)
    32     {
    33         if((rankk[y]-rankk[x] + 3)%3 != flag) return true ;
    34         else return false ;
    35     }
    36     father[fy] = fx ;
    37     rankk[fy] = (rankk[x]-rankk[y] + flag + 3) % 3 ;
    38     return false ;
    39 }
    40 int main()
    41 {
    42     scanf("%d %d",&N,&K) ;
    43     int D,X,Y ;
    44     Init() ;
    45     for(int i = 0 ; i < K ; i++)
    46     {
    47         scanf("%d %d %d",&D,&X,&Y) ;
    48         if(X > N || Y > N || (X == Y && D == 2))
    49             cnt ++ ;
    50         else if(mergee(X,Y,D-1))
    51             cnt ++ ;
    52     }
    53     printf("%d
    ",cnt) ;
    54     return 0 ;
    55 }
    View Code
  • 相关阅读:
    cogs 1272. [AHOI2009] 行星序列
    1027. 打印沙漏(20)
    1026. 程序运行时间(15)
    1023. 组个最小数 (20)
    《C语言程序设计(第四版)》阅读心得(四 文件操作)
    1022. D进制的A+B (20)
    1021. 个位数统计 (15)
    1020. 月饼 (25)
    1015. 德才论 (25)
    1009. 说反话 (20)
  • 原文地址:https://www.cnblogs.com/luyingfeng/p/3917783.html
Copyright © 2011-2022 走看看