并查集练习题

知识点

数据结构之并查集-CSDN博客icon-default.png?t=N7T8https://blog.csdn.net/gege_0606/article/details/139069814?spm=1001.2014.3001.5501

题型一

题目链接:食物链 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

分析

题目中给出了三类动物A,B,C,关系如下图所示

对于传统的并查集,其只能表示单一的身份或者集合,但在本题当中,显然,我们需要表示捕食者,被捕食者和中立者,那么我们应该怎么办呢?

这个时候我们就可以用到种类并查集了,我们把并查集分成三个部分,1~n表示一种类群,n~2n表示一种类群,2n~3n表示一种类群。

  • x:代表动物本身作为某一类型(比如类型B)。
  • x + n:代表作为另一类型(比如类型A),这个类型是直接吃x表示的类型B。
  • x + 2 * n:代表作为第三种类型(比如类型C),这个类型是被类型B吃的,或者是吃类型A的类型。

题目的要求输出假话的个数,假话分以下几种类别

接下来就是根据输入的type来决定x和y之间的关系。

type==1如果x与y是同类关系。

什么时候会跟前面的真话冲突呢?

        如果吃x(类型B)的类型A和y属于同一类,冲突 

        如果类型C(被x吃的类型)和y属于同一类别,冲突

不冲突

        合并的时候要把所有的映射关系都合并,合并x,y;合并x+n,y+n;合并x+2*n,y+2*n;

type==2如果x吃y。

那么什么时候会跟前面的真话冲突呢?

        如果x和y属于同一个类群,冲突

        如果吃x的类型A和y属于同一个类群,冲突

        如果x和y相等,自己不可能吃自己,冲突

不冲突

        合并所有的映射关系,x和y+n,x+n和y+2*n,x+2*n和y;

代码

#include <bits/stdc++.h>

using namespace std;
/*int read()
{
	int f=1,w=0;char ch=getchar();
	while(ch<'0'||ch>'9')ch=getchar();
	while('0'<=ch&&ch<='9'){w=w*10+ch-48;ch=getchar();}
//	cout<<f*w<<endl;
	return f*w;
}*/
const int N = 100005;  // 最大可能的动物数量
int pre[3 * N];  // 并查集数组,每个动物有三种可能的状态

// 查找根节点,并实施路径压缩
int root(int x) {
    if (pre[x] != x) pre[x] = root(pre[x]);
    return pre[x];
}

// 合并两个集合
void Union(int x, int y) {
    int rootX = root(x);
    int rootY = root(y);
    if (rootX != rootY) pre[rootY] = rootX;
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(0);

    int n, k;
    cin>>n>>k;
    int ans = 0;

    // 初始化并查集
    for (int i = 0; i < 3 * n; i++) {
        pre[i] = i;
    }

    while (k--) {
        int type, x, y;
       

        if (x > n || y > n || x < 1 || y < 1) {  // 检查 x 和 y 是否在有效范围内
            ans++;
            continue;
        }

        if (type == 1) {  // X 和 Y 是同类
            if (root(x+n) == root(y) || root(x +2* n) == root(y)) {
                ans++;
            } else {
                Union(x, y);
                Union(x + n, y + n);
                Union(x + 2 * n, y + 2 * n);
            }
        } else if (type == 2) {  // X 吃 Y
            if (x == y || root(x) == root(y) || root(y) == root(x + n)) {
                ans++;
            } else {
                Union(x+2*n, y );
                Union(x + n, y+2*n);
                Union(x , y + n);
            }
        }
    }

    cout << ans << '\n';

    return 0;
}

题目二

P1196 [NOI2002] 银河英雄传说 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

分析

以下两张图片是题目的要求

对于命令M,我们只需要使用并查集的基本操作,合并两个根节点即可 。

对于命令C,我们不仅要判断i和j战舰是否在同一列中,还要输出它们之间布置多少的战舰。对于判断是否是在同一列中,也只需要运用到并查集的基本操作,找根(查找这一操作)。

那么我们如何计算i和j战舰之间有多少的战舰呢?

可以想到需要维护一个数组,记录每个叶子结点(战舰)相对于根的距离。以及一个size数组来维护每一列的元素的个数。

接下来就是find函数和Union(合并)函数了;

对于find函数

我们不仅每一次都需要递归找到它的根节点,还需要每次更新每个战舰距离根节点的距离。

int find(int x) {
    if (parent[x] == x) {
        return x;
    }
    int original_parent = parent[x];
    parent[x] = find(parent[x]);  
    offset[x] += offset[original_parent]; 
    return parent[x];
}
代码解释:

变量的含义:

original_parent表示的是x的直接父亲节点。

offset数组通过累加记录了每一个元素与根节点的距离。

offset[x] += offset[original_parent]; 

假设有节点链x -> a -> b -> root,其中->表示指向父节点,初始时每个节点到其父节点的offset为0(或者在之前的操作中设置了某些值)。在调用find(x)时,会发生以下步骤:

  1. 递归调用find(a),接着是find(b),最终到find(root)
  2. 在递归回溯过程中:
    • 首先,broot的直接子节点,offset[b]更新为broot的距离。
    • 然后,aoffset需要更新为ab的距离加上broot的距离(offset[b]),最终是a到根的距离。
    • 最后,xoffset更新为xa的距离加上更新后的offset[a],最终是x到根的距离

这个过程确保,每个节点的offset最终反映的是从该节点到根节点的总距离,而不仅仅是到直接父节点的距离。

合并函数:

void Union(int x, int y) {
    int rootX = find(x), rootY = find(y);
    if (rootX != rootY) {
        parent[rootX] = rootY;
        offset[rootX] = size[rootY];
        size[rootY] += size[rootX];
    }
}
代码解释: 
  • parent[rootX] = rootY;:这行代码的作用是将rootX的父节点指向rootY,即将rootX集合合并到rootY集合下,此时rootY成为了合并后的集合的新根节点。
  • offset[rootX] = size[rootY];:当将rootX接到rootY后面时,我们需要为rootX设置一个偏移量,这个偏移量是rootY集合当前的大小。这样做的目的是确保rootX及其所有子节点的位置能正确反映它们相对于新根节点rootY的位置。

为什么用size[rootY]作为偏移量?

因为size[rootY]表示rootY集合中元素的数量,如果我们把rootX放在rootY的所有元素后面,那么rootXrootY的距离就应该是rootY集合的大小。

  • size[rootY] += size[rootX];:在合并两个集合后,新的根节点rootY所在的集合的大小应该是原来两个集合大小的总和。这行代码更新了rootY集合的大小,将rootX集合的大小加到了rootY上。

代码

#include <iostream>
#include <cstdlib>
#include <cmath>

const int MAXN = 30001;
int parent[MAXN], offset[MAXN], size[MAXN];

int find(int x) {
    if (parent[x] == x) {
        return x;
    }
    int original_parent = parent[x];
    parent[x] = find(parent[x]);
    offset[x] += offset[original_parent];//offset表示x距离根节点的距离
    return parent[x];
}

void Union(int x, int y) {
    int rootX = find(x), rootY = find(y);
    if (rootX != rootY) {
        parent[rootX] = rootY;
        offset[rootX] = size[rootY];
        size[rootY] += size[rootX];
    }
}

int main() {
    std::ios::sync_with_stdio(false);
    int n;
    std::cin >> n;
//初始化
    for (int i = 1; i < MAXN; i++) {
        parent[i] = i;
        offset[i] = 0;
        size[i] = 1;
    }

    char op;
    int x, y;
    for (int i = 0; i < n; i++) {
        std::cin >> op >> x >> y;
        if (op == 'M') {
            Union(x, y);
        } else if (op == 'C') {
            if (find(x) != find(y)) {
                std::cout << "-1\n";
            } else {
                std::cout << abs(offset[x] - offset[y]) - 1 << "\n";
            }
        }
    }
    return 0;
}

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值