数据结构之并查集

简介

并查集(Union-Find)是一种用于处理一些元素分组和组成员查询问题的数据结构。它能够管理和合并不交叉的集合,并快速查询元素所在集合的信息。

两种操作

查找:确定某个元素属于哪一个子集。这可以用来确定两个元素是否位于同一个子集

联合:将两个子集合并成一个集合。

表示方法

并查集通常使用一个整数数组 parent[] 来表示,其中 parent[i] 表示元素 i 的父节点。如果 parent[i] == i,则表示 i 是该集合的根节点。(以下代码中是pre数组)

合并

在合并当中,所有的操作都是在根上

假如我需要把x和y两个点合并,只需要将x的根指向y的根或者让y的根指向x的根 。

在这里值得注意的是不能简单的把pre[x]=y,否则只能将一个节点的父亲节点修改为y的根节点,而不能让根根相连。

void union(int x, int y) {
    int rootX = root(x);
    int rootY = root(y);
    if (rootX != rootY) {
        pre[rootY] = rootX;  // 将一个根节点指向另一个根节点
    }
}

查询操作之路径压缩: 

由于路径压缩可以减少时间复杂度,就只介绍最优的了

路径压缩是指在执行查找操作的时候,让集合中的每个元素都直接指向根节点,这样可以减少时间复杂度。

以下代码实现的是递归来找到某个元素的根节点

int root(int x) {
    // 递归查找根节点,同时实现路径压缩
    // 如果pre[x]不是自己,说明还没有找到根节点
    return pre[x] = (pre[x] == x ? x : root(pre[x]));
}

练习题.蓝桥幼儿园 - 蓝桥云课 (lanqiao.cn)icon-default.png?t=N7T8https://www.lanqiao.cn/problems/1135/learning/?page=1&first_category_id=1&problem_id=1135

连通块中点的数量

如果我们不仅仅需要判断两个点是否属于同一个集合还需要求出每个集合中元素的数量,那么我们应该怎么做呢?

我们可以维护一个size的数组,初始化为1,在每次合并的时候,我们就可以增加size的大小

#include <bits/stdc++.h>
using namespace std;
const int N = 1e5+10;
int root[N],size[N];
int find(int x)
{
    return (root[x] == x ? x : find(root[x]));
}
int main() {

    int n; cin  >> n;
    for(int i = 0; i < n; i++) {
            root[i] = i;
            size[i] = 1;//初始化size
    }

    for(int i = 0; i < n; i++){
        int x,y; cin >> x >> y;
        int rootX = find(x);
        int rootY = find(y);
        if(rootX != rootY){
            size[rootY]+=size[rootX];//把x合并到y中
            root[rootX] = rootY;//存在顺序,与上面的不可互换

        }
    }

    int f; cin >> f;
    cout << size[find(f)];//输出集合的个数
    return 0;
}

并查集中的“删除”操作

并查集(Union-Find)通常用于管理元素集合的合并和查询操作,并不直接支持删除操作,因为它主要是用来处理动态连接性问题,而不是元素的增删。但我们可以模拟删除某个节点。

预制副本

  • 为每个元素创建一个副本,并使副本成为元素的直接父节点。同时也让副本的父节点是自己。这样,每个元素实际上都是其副本的唯一子节点。
  • 这种做法的目的是将每个元素与其他元素隔离开,保证在进行删除操作时,只有该元素及其副本受到影响,不会干扰到其他元素。

模拟删除

  • 查找元素的根节点:

    • 使用 find 函数找到元素 x 的根节点 rootX。这一步是必要的,因为我们需要知道当前元素所属的集合大小,以及确保我们操作的是集合的根节点。
  • 更新集合大小

    • 通过 --size[rootX] 减少集合的大小。这是因为我们将要从逻辑上移除一个元素,所以其所在集合的元素数量应该减少。
  • 断开连接

    • parent[n + x] = x;:将元素 x 的副本节点的父节点设置为它自己,使副本节点变成一个独立的根节点。这一步实际上在逻辑上移除了原始元素与其副本之间的连接。
    • parent[x] = x;:将元素 x 的父节点也设置为它自己,使得原始元素也成为一个独立的根节点。这一步彻底断开了元素 x 与集合的任何连接,实现了其“删除”。

对应代码

#include <iostream>
#include <vector>

using namespace std;

vector<int> parent;
vector<int> size;

void initialize(int n) {
    parent.resize(2 * n);
    size.resize(2 * n, 1);  // 初始化大小为1,因为每个节点最初都是独立的集合,表示每个集合的大小
    for (int i = 0; i < n; i++) {
        parent[i] = n + i;  // 每个元素i的父节点是i的副本
        parent[n + i] = n + i;  // 每个副本的父节点是其自身,也是根节点
    }
}

int find(int x) {
    if (parent[x] != x) {
        parent[x] = find(parent[x]);  // 路径压缩
    }
    return parent[x];
}

void simulateDelete(int x, int n) {
    int rootX = find(x);
    --size[rootX];  // 减少集合的大小
    parent[n + x] = x;  // 将副本的父节点指向自己,模拟删除
    parent[x] = x;      // 将原始元素的父节点也指向自己,彻底断开连接
}

int main() {
    int n = 10; // 假设有10个元素
    initialize(n);

    // 模拟删除元素3
    simulateDelete(3, n);

    // 输出每个元素的根节点,检查删除操作的效果
    for (int i = 0; i < n; i++) {
        cout << "Root of element " << i << " is " << find(i) << endl;
    }

    return 0;
}

并查集中的“移动”操作 

  1. 查找根节点:

    • 使用 find 函数分别找到 xy 的根节点,分别存储在 fxfy 中。
    • 如果 xy 已经在同一个集合中(即 fx 等于 fy),函数直接返回,因为不需要移动。
  2. 更新父节点:

    • x 的父节点直接设置为 fy,这样 x 现在属于 y 的集合。这是移动操作的核心,直接修改父节点可以快速改变元素的集合归属。
  3. 更新集合大小:

    • x 原来的集合(fx)的大小中减去 1 (--size[fx]),因为 x 被移出了这个集合。
    • y 的集合(fy)的大小中加上 1 (++size[fy]),因为 x 被加入到了这个集合。
void move(int x, int y) {
    int fx = find(x), fy = find(y);
    if (fx == fy) return;  // 如果x和y已经在同一个集合中,则无需移动

    parent[x] = fy;  // 将x的父节点直接设置为y的根节点
    --size[fx];  // 从x原来的集合中减去1
    ++size[fy];  // 将x加入到y的集合中
}

对应的练习题

P1197 [JSOI2008] 星球大战 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

详细题解 

并查集练习题-CSDN博客icon-default.png?t=N7T8https://blog.csdn.net/gege_0606/article/details/139074616?csdn_share_tail=%7B%22type%22%3A%22blog%22%2C%22rType%22%3A%22article%22%2C%22rId%22%3A%22139074616%22%2C%22source%22%3A%22gege_0606%22%7D

带权并查集 

带权并查集在基础的并查集上增加了权值的概念。这里的“权值”可以代表集合中元素间的相对距离或者相对大小关系。

结构

在带权并查集中,每个节点除了有一个指向父节点的指针外,还维护一个权值,这个权值表示当前节点与其父节点之间的关系(例如距离、差值等)这个距离需要具体问题具体分析。

操作
  • 查找(Find)查找操作不仅要返回元素的根节点(代表元素),还需要返回从该元素到根节点的累计权值。在执行查找的过程中,通常会进行路径压缩,即把查找路径上的每个节点直接连接到根节点上,同时更新路径上每个节点到根节点的权值。

  • 合并(Union) 合并操作需要考虑两个集合的根节点及其权值。当合并两个集合时,需要选择一个集合的根作为新的根,并更新权值以保持集合内元素间的相对关系正确

root函数是用来寻找根,offset数组是用来储存每个节点到根的距离,size是维护集合的大小

以下代码实现了将x合并在y集合后,并记录下x的根距离y的根的距离 

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];
    }
}

例题: 

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

P1525 [NOIP2010 提高组] 关押罪犯 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

详细题解,题目二

并查集练习题-CSDN博客

种类并查集

种类并查集是一种特殊类型的并查集,它用于处理问题中存在多种分类或种类的情况。这种数据结构可以有效地支持元素间不仅是属于不同集合的判断,还能处理属于不同种类的分类问题。它通过扩大原始并查集的存储空间,允许维护多种可能的关系,如敌对、朋友或其他特定关系。以下是关于种类并查集的总结:

主要特点

  1. 多关系维护: 种类并查集能够同时处理和维护集合中元素的多种关系,如敌对、友好等,这是通过将原始并查集空间扩大多倍实现的。

  2. 空间扩展: 对于需要区分 k 类关系的场景,种类并查集将空间扩展为 k 倍。这样,每种关系都有自己的专属区间进行管理和操作。

  3. 虚拟分区: 扩展出的每个区间代表不同的关系类型。这些区间虽然在逻辑上区分元素的关系,但它们本身并不代表任何实际的物理或自然分类,仅是为了操作的方便而设。

应用场景

种类并查集适用于复杂的场景,其中元素间的关系不仅仅是简单的集合归属问题,而是涉及多重可能的相互作用或状态。例如:

  • 食物链问题:在生态系统中,某些生物之间的关系可能是捕食、竞争或共生,种类并查集可以帮助维护这些关系的逻辑清晰。
  • 社交网络:可以用来维护用户之间的不同社交状态,如朋友、屏蔽、敌人等。
  • 冲突解决:在某些情况下,特定的规则需要判断元素间是否存在特定的敌对或合作关系。

实现方法

实现种类并查集时,通常会使用一个数组来扩展存储空间,并将元素及其关系映射到不同的区间。每个区间内部操作与普通的并查集相同,但在处理元素间的关系转换时需要特别注意转换逻辑和正确的映射。

P2024 [NOI2001] 食物链 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

P1892 [BOI2003] 团伙 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

食物链题目一 CSDN博客icon-default.png?t=N7T8https://blog.csdn.net/gege_0606/article/details/139074616?csdn_share_tail=%7B%22type%22%3A%22blog%22%2C%22rType%22%3A%22article%22%2C%22rId%22%3A%22139074616%22%2C%22source%22%3A%22gege_0606%22%7D

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值