食物链(权值并查集)

Description

动物王国中有三类动物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

solution1

在这里插入图片描述
我们把它们之间的关系转化成模3向量,A吃B, B吃C,C吃A,就是 A B ⃗ = B C ⃗ = C A ⃗ = 1 \vec{AB}=\vec{BC}=\vec{CA}=1 AB =BC =CA =1 A C ⃗ = 2 \vec{AC}=2 AC =2 就表示C吃A。

三类关系,假设x的父亲是y,y->x:

  • 0 同类
  • 1 y吃x
  • 2 x吃y

然后我们看各个情况的传递关系:(图中的含义大家对应着翻译过来就很清晰了)
在这里插入图片描述
下图分别对应code1中的46行和57行。
在这里插入图片描述

code1

#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 5;
int f[N]; //f[i]存点i的父节点编号
int r[N]; //r[i]存点i与父节点的关系,0,1,2
int findd(int x)
{
    if (f[x] == x)
        return x;
    int t = f[x];
    f[x] = findd(f[x]); //每次压缩路径前把原来的父子结点对应的关系r更新
    r[x] = (r[x] + r[t]) % 3;
    return f[x];
}
void mergee(int d, int x, int y)
{
    int xx = findd(x), yy = findd(y);
    f[yy] = xx;
    r[yy] = (r[x] + (d - 1) - r[y] + 3) % 3;
}
int main()
{
    ios::sync_with_stdio(false), cin.tie(nullptr), cout.tie(nullptr);
    int n, k;
    cin >> n >> k;
    for (int i = 0; i <= n; i++)
    {
        f[i] = i;
    }
    int ans = 0;
    while (k--)
    {
        int d, x, y;
        cin >> d >> x >> y;
        if (x > n || y > n || (d == 2 && x == y))
        {
            ans++;
            continue;
        }
        if (d == 1) // x,y是同类
        {
            if (findd(x) != findd(y)) // 如果x和y之间还没有任何关系,直接按照关系d合并
            {
                mergee(d, x, y);
            }
            else if (r[x] != r[y]) // 如果x和y之前已经存在某种关系,这里应该是r[x]+0!=r[y],对应上面的传递关系
            {
                ans++;
            }
        }
        else // d==2 x吃y
        {
            if (findd(x) != findd(y))
            {
                mergee(d, x, y);
            }
            else if ((r[x] + 1) % 3 != r[y]) // 对应上述传递关系
            {
                ans++;
            }
        }
    }
    cout << ans << endl;
    return 0;
}

solution2

三倍并查集方法。上面solution1的方法中结点 i i i 与父节点的关系有同类、吃与被吃三种,但是solution2的这个方法点之间的关系是通过不同群系之间的编号来确定的,边连接的两个点之间并没有某种确定的关系。
我们假设三个群系 G 0 G_0 G0 G 1 G_1 G1 G 2 G_2 G2,每个群系中都有相同的N个动物,分别编号1~n、n+1~2n、2n+1~3n。
G i G_i Gi 来说, G ( i − 1 + 3 ) % 3 G_{(i-1+3)\%3} G(i1+3)%3 狩猎 G i G_i Gi G ( i + 1 ) % 3 G_{(i+1)\%3} G(i+1)%3 G i G_i Gi 狩猎,简单说就是 G 0 G_0 G0 G 1 G_1 G1 G 1 G_1 G1 G 2 G_2 G2 G 2 G_2 G2 G 0 G_0 G0
同类、吃、被吃这三种关系是通过不同群系之间的编号来确定的,只要它们合并到一个集合中,那么就可以通过编号来判断它们之间的关系。 点与点之间的连线并没有什么实际意义。
在这里插入图片描述
比如样例,上图是路径压缩后的并查集, x → y x\rightarrow y xy 表示 f [ y ] = x f[y]=x f[y]=x,A吃B,B吃C,C吃A。
现在有三个集合 {A1,C3,B2},{B1,A3,C2},{C1,A2,B3}。比如第一个集合就可以通过A吃B、B吃C、C吃A来判断:A1吃B2,B2吃C3,C3吃A1。同理其他两个集合。

code2

#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 5;
int f[N * 3]; // A:1~n B:n+1~2n C:2n+1~3n
int findd(int x)
{
    return f[x] == x ? x : f[x] = findd(f[x]);
}
int main()
{
    ios::sync_with_stdio(false), cin.tie(nullptr), cout.tie(nullptr);
    int n, k;
    cin >> n >> k;
    for (int i = 1; i <= n * 3; i++)
    {
        f[i] = i;
    }
    int ans = 0;
    int t = 1;
    while (k--)
    {
        int d, x, y;
        cin >> d >> x >> y;
        if (x > n || y > n || (d == 2 && x == y))
        {
            ans++;
            continue;
        }
        if (d == 1)
        {
        	// 判断x和y是否同类的方法是 x不吃y,y不吃x
            if (findd(x) == findd(y + n) || findd(x + n) == findd(y))
            {
                ans++;
            }
            else
            {
            	// 合并
                f[findd(y)] = findd(x);
                f[findd(y + n)] = findd(x + n);
                f[findd(y + n + n)] = findd(x + n + n);
            }
        }
        else
        {
        	// 若x、y是同类,或者y吃x,则是假话
            if (findd(x) == findd(y) || findd(x + n) == findd(y))
            {
                ans++;
            }
            else
            {
            	// 合并
                f[findd(y + n)] = findd(x);
                f[findd(y + n + n)] = findd(x + n);
                f[findd(y)] = findd(x + n + n);
            }
        }
    }
    cout << ans << endl;
    return 0;
}
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
克鲁斯卡尔算法是一种用于求解最小生成树的贪心算法,而并查集是一种用于维护元素分组信息的数据结构。它们在解决图论问题中经常一起使用。 克鲁斯卡尔算法的基本思想是,通过不断选择边权值最小且不会产生环路的边,逐步构建最小生成树。在实现过程中,使用并查集来判断两个节点是否属于同一个连通分量,以避免形成环路。 并查集是一种用于解决集合合并与查询问题的数据结构。它通过维护一棵树来表示每个元素所属的集合,其中每个节点指向其父节点,树的根节点表示该集合的代表元素。通过路径压缩和按秩合并等优化策略,可以提高并查集的效率。 在克鲁斯卡尔算法中,首先将图中的所有边按权值从小到大排序,然后依次选择边进行判断。当选择一条边时,判断该边连接的两个节点是否属于同一个连通分量。如果不属于同一个连通分量,则选择该边,并将两个节点合并到同一个连通分量中。重复这个过程直到选择了 n-1 条边,其中 n 是图中节点的个数,即得到最小生成树。 克鲁斯卡尔算法的时间复杂度主要取决于排序边的时间复杂度,一般情况下为 O(ElogE),其中 E 是边的数量。并查集的操作时间复杂度为 O(α(n)),其中 α(n) 是一个非常慢增长的函数,可以认为是常数级别。因此,整个算法的时间复杂度为 O(ElogE)。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值