食物链(带权并查集经典题)

                                                 食物链    POJ - 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),输出假话的总数。 

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

参考博客:https://www.cnblogs.com/haoabcd2010/p/5688902.html

参考博客:https://blog.csdn.net/c0de4fun/article/details/7318642(看过这个博客以后再去看第一个博客会帮助理解很多)

我将两篇博客总结如下:

PART 1  权值 r[i] 的确定

     我们根据题意,森林中有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不是随便选的

     设 :f[i] 表示 i 号动物的父节点的编号 , r[N]是对父节点的关系 : 0 同类,1 被父节点吃,2 吃父节点 

 

Part II - 路径压缩,以及节点间关系确定

    确定了权值之后,我们要确定有关的操作。

    我们把所有的动物全初始化。

for(int i = 1; i <= n ; i++) 
    {
        f[i] = i; // 初始化父亲为自己
        r[i] = 0; // 自己与自己的关系是同类
    }

     (1)路径压缩时的节点算法

     我们设A,B,C动物集合如下:(为了以后便于举例)

     A = { 1 , 2 , 3 ,4 ,5 }

     B = { 6 , 7 , 8 ,9 ,10}

     C = { 11, 12, 13,14,15}

     假如我们已经有了一个并查集合

     SET1 = {1,2},我们规定集合中第一个元素为并查集的“代表”

      假如现在有语句:

      2 2 6

      这是一句真话  2吃6

      我们根据这条语句赋值 即2是6的父亲

      f[6] = 2  ;  r[6] = 1 ;

      那么此时和2在一个并查集里的1与6的关系如何呢

      f[2] = 1  ;   r[2] = 0;

      我们可以发现6与1的关系是 1. (2吃6,2与1是同类,所以1也吃6)

       通过穷举我们可以发现:

             r[f[x]]            r[x]

        父亲对爷爷  儿子对父亲  儿子对爷爷 

               0                   0                 0      

               0                   1                 1   

               0                   2                 2   

               1                   0                 1  

               1                   1                 2  

               1                   2                 0  

               2                   0                 2  

               2                   1                 0     

               2                   2                 1

      所以,儿子对爷爷的关系就可以推出这个式子  (r[x]  +  r[f[x]] ) % 3

      用这个式子就解决了压缩路径的关系转换 

      对应代码:

int Find(int x) // 寻找x的根节点
{
    int fx = x;
    if(x != f[x])
    {
        fx = Find(f[x]);//先更新父节点
        r[x] = (r[x] + r[f[x]])%3; // 更新为跟爷爷的关系
        f[x] = fx;
    }
    return f[x];
}

      这段代码就是先压缩到跟爷爷的关系,再找爷爷的爸爸,然后压缩到与爷爷的爸爸的关系......

      通过递归一直找下去并且压缩,直至压缩至与根节点的关系

     

      (2) 集合间关系的确定

      在初始化的时候,我们看到,每个集合都是一个元素,就是他本身。

      这时候,每个集合都是自洽的(集合中每个元素都不违反题目的规定)

      注意,我们使用并查集的目的就是尽量的把路径压缩,使之高度尽量矮

      假设我们已经有三个并查集集合

      set1 = {1,2,7,10}

      set2 = {11,4,8,13}     每个编号所属的物种见上文

      set3 = {12,5,4,9}

       如果给你一句话 d x y,你该如何判断是否真假呢

       有两种情况 

       1)Find(x)  = Find(y)  ,  即x,y在同一个并查集中, 只需判断关系是否矛盾即可。 

       如果  (3 - r[x] + r[y]) % 3 != d-1  代表假话

       解释如下:

       r[y] 是 y 与其根节点的关系 :根   —   y

       r[x] 是 x 与其根节点的关系: 根   —   x

       ( 3 -  r[x] ) %3   =  根据 x 与 根节点 的关系,逆推 根节点 与 x 的关系 :x  —  根

       这部分也是穷举法推出来的,我们举例:

       x (以x为节点)                                r[x]

       0                                           ( 3 - 0 ) % 3 = 0

       1(父吃子)                         ( 3 - 1 ) % 3 = 2 //父吃子

      2 (子吃父)                           ( 3 - 2 ) % 3 = 1 //子吃父

     所以根据 x — 根  , 根   —   y  以 y 为 儿子 , 根 为 爸爸 , x 为爷爷 

     再根据上文退出的 儿子对爷爷的关系: (r[x]  +  r[f[x]] ) % 3

     可以得出:(3 - r[x] + r[y] ) % 3 即 x 与 y 的 关系 ,若与d - 1不相符即为假话

     例如: 1 2 7 

     判断:Find(2) = Find(7)

     then : r[2] = 0  (2和1是同类);  r[7] =  1(7被1吃)

     then :  (3 - 0 + 1 ) % 3 = 1  !=   d-1       

    7被2吃,并不是同类  所以 1 2 7 是假话

   

     2)Find(x)  !=  Find(y)

       此时x与y不在一个集合中,我们需要先把两个集合合并到一个集合中

       如果直接:

    int fx = Find(x);
    int fy = Find(y);
    if(fx != fy) 
    {
        f[fy] = fx;
    }

      就是把Y所在集合的根节点的父亲设置成X所在集合的根节点 

      但是,Y所在集合的根结点与X所在集合的根节点的关系 ,要怎么确定呢?

      我们设X,Y集合都是路径压缩过的,高度只有2层

      先给出 根y 与 根x 关系更新公式:

      r[y] = ( 3 - r[y] + ( d - 1 ) + r[x]) % 3;

     这个公式,是分三部分,这么推出来的

     第一部分:   ( d - 1 )   表示 : y 与 x 的关系 即 x —  y

     第二部分:   3 - r[y]    表示 : 根y 与 y 的关系 即 y — 根y

     第三部分:    r[x]        表示 : x 与 根x 的关系 即 根x — x

     我们的过程是这样的:

     先通过 (  ( 3 - r[y] + ( d - 1 )  )  % 3 得到 根y 与 x 的关系 即 x — 根y

     再通过(  ( 3 - r[y] + ( d - 1 )  )  % 3 + r[x] ) %3 得到 根y 与 根x 的关系

    根据同余定理,最终得到 根y 与 根x 关系的更新式:

     r[y] = ( 3 - r[y] + ( d - 1 ) + r[x]) % 3; 

     最终合并集合的核心代码为:

    int fx = Find(x);
    int fy = Find(y);
    if(fx != fy) 
    {
        f[fy] = fx;
        r[fy] = (3 - r[y] + d-1 + r[x])% 3;
    }

 

 

最终代码:


#include <cstdio>
#include <iostream>
#include <string.h>
#include <string>
#include <map>
#include <queue>
#include <deque>
#include <vector>
#include <set>
#include <algorithm>
#include <math.h>
#include <cmath>
#include <stack>

using namespace std;
typedef long long ll;
typedef long double ld;
typedef double db;
const int maxn=50005,inf=0x3f3f3f3f;
const double pi = acos(-1.0);

int f[maxn],r[maxn];

int Find(int x) // 寻找x的根节点
{
    int fx = x;
    if(x != f[x])
    {
        fx = Find(f[x]);//先更新父节点
        r[x] = (r[x] + r[f[x]])%3; // 更新为跟爷爷的关系
        f[x] = fx;
    }
    return f[x];
}

int Union(int d,int x,int y)
{
    int fx = Find(x);
    int fy = Find(y);
    if(fx != fy) // 不在同一集合合并集合
    {
        f[fy] = fx;
        r[fy] = (3 - r[y] + d-1 + r[x])% 3;
        return 0;
    }
    //在同一个集合就判断关系
    if((r[y] - r[x] + 3) % 3 != d-1)
        return 1;
    return 0;
}

int main()
{
    int n,k;
    int d,x,y;
    scanf("%d%d",&n,&k);
    int ans = 0;
    for(int i = 1; i <= n ;i++) // 初始化
    {
        f[i] = i;
        r[i] = 0;
    }

    while(k--)
    {
        scanf("%d%d%d",&d,&x,&y);
        if(x > n || y > n || (x == y && d == 2))
        {
            ans++;
            continue;
        }
        if(Union(d,x,y)) ans++;
    }
    printf("%d\n",ans);
    return 0;
}

 

      

 

 

 

      

     

 

 

 

                                                                                                        

 

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
带权并查集(Weighted Union-Find)是在普通并查集的基础上进行了扩展,它在每个节点上存储了额外的权重信息。带权并查集主要用于解决一些需要考虑权重或者秩的问,例如求解最小生成树、最大连通子图等。 在普通并查集中,每个节点都有一个父节点指针,用于表示该节点所属的集合。在带权并查集中,除了父节点指针外,每个节点还有一个权重值。这个权重值可以是任意类型的,例如整数、浮点数等,根据问的需求而定。 带权并查集的基本操作与普通并查集类似,包括初始化、查找和合并: 1. 初始化:对于每个元素,将其视为一个独立的集合,即每个元素的父节点都是它自己,同时将权重值初始化为初始值。 2. 查找操作(Find):查找元素所属的集合,即找到元素的根节点。通过沿着父节点指针链向上遍历,直到找到根节点。返回根节点的同时可以累加路径上所有节点的权重值,以实现路径压缩和权重更新。 3. 合并操作(Merge):将两个集合合并成一个集合,即将一个集合的根节点的父节点指向另一个集合的根节点。在合并操作中,需要考虑集合的权重信息。通常,我们将权重较小的集合合并到权重较大的集合上,并更新根节点的权重值。 带权并查集的优化策略主要包括按秩合并和路径压缩。按秩合并是根据集合的秩(树的高度或节点数量)来进行合并操作,将秩较小的集合合并到秩较大的集合上,以保持树的平衡。路径压缩则是在查找操作中,将经过的每个节点直接连接到根节点,并更新路径上所有节点的权重值。 带权并查集的时间复杂度也取决于查找操作的路径长度,但由于路径压缩和按秩合并的优化,一般情况下可以达到接近常数时间复杂度。 带权并查集是一个非常有用的数据结构,可以解决一些需要考虑权重或秩的问。通过存储额外的权重信息,并结合路径压缩和按秩合并等优化策略,可以提高算法的效率。 希望这个解释对您有所帮助!如果您还有其他问,请随时提问。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值