学习笔记:并查集

news2024/11/16 8:43:13

并查集

并查集被很多 OIer \texttt{OIer} OIer 认为是最简洁而优雅的数据结构之一,主要用于解决一些 元素分组 的问题。它管理一系列 不相交的集合,并支持两种操作:

  • 合并:把两个不相交的集合合并为一个集合。
  • 查询:查询两个元素是否在同一个集合中。

先来看看并查集最直接的一个应用场景:亲戚问题

洛谷P1551 亲戚

题目背景

若某个家族人员过于庞大,要判断两个是否是亲戚,确实还很不容易,现在给出某个亲戚关系图,求任意给出的两个人是否具有亲戚关系。

题目描述

规定: x x x y y y 是亲戚, y y y z z z 是亲戚,那么 x x x z z z 也是亲戚。如果 x x x y y y 是亲戚,那么 x x x 的亲戚都是 y y y 的亲戚, y y y 的亲戚也都是 x x x 的亲戚。

输入格式

第一行:三个整数 n , m , p n,m,p n,m,p,( n , m , p ≤ 5000 n,m,p \le 5000 n,m,p5000),分别表示有 n n n 个人, m m m 个亲戚关系,询问 p p p 对亲戚关系。

以下 m m m 行:每行两个数 M i M_i Mi M j M_j Mj 1 ≤ M i ,   M j ≤ n 1 \le M_i,~M_j\le n 1Mi, Mjn,表示 M i M_i Mi M j M_j Mj 具有亲戚关系。

接下来 p p p 行:每行两个数 P i , P j P_i,P_j Pi,Pj,询问 P i P_i Pi P j P_j Pj 是否具有亲戚关系。

输出格式

p p p 行,每行一个 YesNo。表示第 i i i 个询问的答案为“具有”或“不具有”亲戚关系。

这其实是一个很有现实意义的问题。我们可以建立模型,把所有人划分到若干个不相交的集合中,每个集合里的人彼此是亲戚。为了判断两个人是否为亲戚,只需看它们是否属于同一个集合即可。因此,这里就可以考虑用并查集进行维护了。

简单并查集

并查集的重要思想在于,用集合中的一个元素代表集合。有这样一个有趣的比喻,把集合比喻成 狗帮,而代表元素则是 狗王。接下来我们利用这个比喻,看看并查集是如何运作的。

img

最开始,所有狗狗各自为战。他们各自的狗王自然就是自己(对于只有一个元素的集合,代表元素自然是唯一的那个元素)。

现在 1 1 1 号和 3 3 3 号比武,假设 1 1 1 号赢了(这里具体谁赢暂时不重要),那么 3 3 3 号就认 1 1 1 号作帮主(合并 1 1 1 号和 3 3 3 号所在的集合, 1 1 1 号为代表元素)。

img

现在 2 2 2 号想和 3 3 3 号比武(合并 3 3 3 号和 2 2 2 号所在的集合),但 3 3 3 号表示,别跟我打,让我狗王来收拾你(合并代表元素)。不妨设这次又是 1 1 1 号赢了,那么 2 2 2 号也认 1 1 1 号做狗王。

img

现在我们假设 4 4 4 5 5 5 6 6 6 号也进行了一番狗帮合并,局势变成下面这样:

img

现在假设 2 2 2 号想与 6 6 6 号比,跟刚刚说的一样,喊狗王 1 1 1 号和 4 4 4 号出来打一架。 1 1 1 号胜利后, 4 4 4 号认 1 1 1 号为帮主,当然他的下属也都是跟着投降了。

img

好了,比喻结束了。如果你有一点图论基础,相信你已经觉察到,这是一个 **树 **状的结构,要寻找集合的代表元素,只需要一层一层往上访问 父节点(图中箭头所指的圆),直达树的 根节点(图中橙色的圆)即可。根节点的父节点是它自己。我们可以直接把它画成一棵树:

img

(好像有点像个火柴人?)

用这种方法,我们可以写出最简单版本的并查集代码。

初始化

int f[MAXN];
for(int i = 1 ; i <= n ; i ++)f[i] = i;

假如有编号为 1 , 2 , 3 , . . . , n 1, 2, 3, ..., n 1,2,3,...,n n n n 个元素,我们用一个数组 f[] 来存储每个元素的父节点(因为每个元素有且只有一个父节点,所以这是可行的)。一开始,我们先将它们的父节点设为自己。

查询

int find(int x){
    if(fa[x] == x)return x;
    else return find(fa[x]);
}

我们用递归的写法实现对代表元素的查询:一层一层访问父节点,直至根节点(根节点的标志就是父节点是本身)。要判断两个元素是否属于同一个集合,只需要看它们的根节点是否相同即可。

合并

 void merge(int i, int j){
     fa[find(i)] = find(j);
}

合并操作也是很简单的,先找到两个集合的代表元素,然后将前者的父节点设为后者即可。当然也可以将后者的父节点设为前者,这里暂时不重要。本文末尾会给出一个更合理的比较方法。

路径压缩

最简单的并查集效率是比较低的。例如,来看下面这个场景:

img

现在我们要 merge(2,3),于是从 2 2 2 找到 1 1 1f[1]=3,于是变成了这样:

img

然后我们又找来一个元素 4 4 4,并需要执行 merge(2,4)

img

2 2 2 找到 1 1 1,再找到 3 3 3,然后 f[3]=4,于是变成了这样:

img

大家应该有感觉了,这样可能会形成一条长长的 ,随着链越来越长,我们想要从底部找到根节点会变得越来越难。

怎么解决呢?我们可以使用 路径压缩 的方法。既然我们只关心一个元素对应的 根节点,那我们希望每个元素到根节点的路径尽可能短,最好只需要一步,像这样:

img

其实这说来也很好实现。只要我们在查询的过程中,**把沿途的每个节点的父节点都设为根节点 **即可。下一次再查询时,我们就可以省很多事。这用递归的写法很容易实现:

合并

int find(int x){
    if(x == f[x])return x;
    else return f[x] = find(f[x]);
}

注意赋值运算符 = 的优先级没有三元运算符 ? : 高,这里要加括号。

路径压缩优化后,并查集的时间复杂度已经比较低了,绝大多数不相交集合的合并查询问题都能够解决。然而,对于某些时间卡得很紧的题目,我们还可以进一步优化。

按秩合并

有些人可能有一个误解,以为路径压缩优化后,并查集始终都是一个 菊花图。但其实,由于路径压缩只在查询时进行,也只压缩一条路径,所以并查集最终的结构仍然可能是比较复杂的。例如,现在我们有一棵较复杂的树需要与一个单元素的集合合并:

img

假如这时我们要 merge(7,8),如果我们可以选择的话,是把 7 7 7 的父节点设为 8 8 8 好,还是把 8 8 8 的父节点设为 7 7 7 好呢?

当然是后者。因为如果把 7 7 7 的父节点设为 8 8 8 ,会使树的 深度 加深,原来的树中每个元素到根节点的距离都变长了,之后我们寻找根节点的路径也就会相应变长。虽然我们有路径压缩,但路径压缩也是会消耗时间的。而把 8 8 8 的父节点设为 7 7 7,则不会有这个问题,因为它没有影响到不相关的节点。

img

这启发我们:我们应该把简单的树往复杂的树上合并,而不是相反。因为这样合并后,到根节点距离变长的节点个数比较少。

我们用一个数组 rnk[] 记录每个根节点对应的树的深度(如果不是根节点,其 rnk 相当于以它作为根节点的 子树 的深度)。一开始,把所有元素的 rnk)设为 1 1 1。合并时比较两个根节点,把 rnk 较小者往较大者上合并。

路径压缩和按秩合并如果一起使用,时间复杂度接近 O ( n ) O(n) O(n),但是很可能会破坏 rnk 的准确性。

初始化

int f[MAXN];
for(int i = 1 ; i <= n ; i ++)f[i] = i;
for(int i = 1 ; i <= n ; i ++)rnk[i] = 1;

合并

void merge(int i, int j){
    int x = find(i), y = find(j);
    if(rnk[x] <= rnk[y])fa[x] = y;
    else fa[y] = x;
    if(rnk[x] == rnk[y] && x != y)
        rnk[y]++;
}

为什么深度相同,新的根节点深度要 + 1 +1 +1?如下图,我们有两个深度均为 2 2 2 的树,现在要 merge(2,5)

img

这里把 2 2 2 的父节点设为 5 5 5,或者把 5 5 5 的父节点设为 2 2 2,其实没有太大区别。我们选择前者,于是变成这样:

img

显然树的深度增加了 1 1 1。另一种合并方式同样会让树的深度 + 1 +1 +1

种类并查集

一般的并查集,维护的是具有连通性、传递性的关系,例如亲戚的亲戚是亲戚。有时候,我们要维护另一种关系:敌人的敌人是朋友。种类并查集就是为了解决这个问题而诞生的。

洛谷P1525 [NOIP2010 提高组] 关押罪犯

题目描述

S 城现有两座监狱,一共关押着 N N N 名罪犯,编号分别为 1 − N 1-N 1N。他们之间的关系自然也极不和谐。很多罪犯之间甚至积怨已久,如果客观条件具备则随时可能爆发冲突。我们用“怨气值”(一个正整数值)来表示某两名罪犯之间的仇恨程度,怨气值越大,则这两名罪犯之间的积怨越多。如果两名怨气值为 c c c 的罪犯被关押在同一监狱,他们俩之间会发生摩擦,并造成影响力为 c c c 的冲突事件。

每年年末,警察局会将本年内监狱中的所有冲突事件按影响力从大到小排成一个列表,然后上报到 S 城 Z 市长那里。公务繁忙的 Z 市长只会去看列表中的第一个事件的影响力,如果影响很坏,他就会考虑撤换警察局长。

在详细考察了 N N N 名罪犯间的矛盾关系后,警察局长觉得压力巨大。他准备将罪犯们在两座监狱内重新分配,以求产生的冲突事件影响力都较小,从而保住自己的乌纱帽。假设只要处于同一监狱内的某两个罪犯间有仇恨,那么他们一定会在每年的某个时候发生摩擦。

那么,应如何分配罪犯,才能使 Z 市长看到的那个冲突事件的影响力最小?这个最小值是多少?

输入格式

每行中两个数之间用一个空格隔开。第一行为两个正整数 N , M N,M N,M,分别表示罪犯的数目以及存在仇恨的罪犯对数。接下来的 M M M 行每行为三个正整数 a j , b j , c j a_j,b_j,c_j aj,bj,cj,表示 a j a_j aj 号和 b j b_j bj 号罪犯之间存在仇恨,其怨气值为 c j c_j cj。数据保证 1 < a j ≤ b j ≤ N , 0 < c j ≤ 1 0 9 1<a_j\leq b_j\leq N, 0 < c_j\leq 10^9 1<ajbjN,0<cj109,且每对罪犯组合只出现一次。

输出格式

1 1 1 行,为 Z 市长看到的那个冲突事件的影响力。如果本年内监狱中未发生任何冲突事件,请输出 0

其实很容易想到,这里可以 贪心,把所有矛盾关系 从大到小 排个序,然后尽可能地把矛盾大的犯人关到不同的监狱里,直到不能这么做为止。这看上去可以用并查集维护,但是有一个问题:我们得到的信息,不是哪些人应该在 相同 的监狱,而是哪些人应该在 不同 的监狱。这怎么处理呢?这个题其实有很多做法(例如二分图匹配之类的)。但这里,我们介绍使用种类并查集的做法。

我们开一个 **两倍大小 **的并查集。例如,假如我们要维护 4 4 4 个元素的并查集,我们改为开 8 8 8 个单位的空间:

img

我们用 1 − 4 1-4 14 维护朋友关系(就这道题而言,是指关在同一个监狱的狱友),用 5 − 8 5-8 58 维护敌人关系(这道题里是指关在不同监狱的仇人)。现在假如我们得到信息: 1 1 1 2 2 2 是敌人,应该怎么办?

我们merge(1, 2+n), merge(1+n, 2),对于 1 1 1 个编号为 i i i 的元素, i + n i+n i+n 是它的敌人。所以这里的意思就是: 1 1 1 2 2 2 的敌人, 2 2 2 1 1 1 的敌人。

img

现在假如我们又知道 2 2 2 4 4 4 是敌人,我们merge(2, 4+n), merge(2+n, 4);

img

发现了吗,敌人的敌人就是朋友 2 2 2 4 4 4 是敌人, 2 2 2 1 1 1 也是敌人。所以这里, 1 1 1 4 4 4 通过 2 + n 2+n 2+n 这个元素 间接 地连接起来了。这就是种类并查集工作的原理。

代码如下:

#include <iostream>
#include <algorithm>
#define MAXN 20005
#define MAXM 100005
using namespace std;
int f[MAXN], rnk[MAXN];
struct data{
    int a, b, w;
    bool friend operator<(data &a, data &b){
        return a.w > b.w;
    }
}a[MAXM];
int read(){
    int t = 1, x = 0;char ch = getchar();
    while(!isdigit(ch)){if(ch == '-')t = -1;ch = getchar();}
    while(isdigit(ch)){x = (x << 1) + (x << 3) + (ch ^ 48);ch = getchar();}
    return x * t;
}
void write(int x){
    if(x < 0){putchar('-');x = -x;}
    if(x >= 10)write(x / 10);
    putchar(x % 10 ^ 48);
}
int find(int x){
    if(f[x] == x)return x;
    else return f[x] = find(f[x]);
}
int query(int a, int b){
    if(find(a) == find(b))return true;
    else return false;
}
void merge(int a, int b){
    int x = find(a), y = find(b);
    if(rnk[x] >= rnk[y])f[y] = x;
    else f[x] = y;
    if(rnk[x] == rnk[y] && x != y)rnk[x]++;
}
int main(){
    int n = read(), m = read();
    for(int i = 1 ; i <= (n << 1) ; i ++)f[i] = i;
    for(int i = 1 ; i <= (n << 1) ; i ++)rnk[i] = 1;
    for(int i = 1 ; i <= m ; i ++)
        a[i].a = read(),a[i].b = read(),a[i].w = read();
    sort(a + 1, a + m + 1);
    for(int i = 1 ; i <= m ; i ++){
        if(query(a[i].a, a[i].b) == true){
            write(a[i].w);putchar('\n'); 
            break;
        }else{
            merge(a[i].a, a[i].b + n);
            merge(a[i].b, a[i].a + n);
            if(i == m - 1)puts("0");
        }
    }
    return 0;
}

种类并查集可以维护 敌人的敌人是朋友 这样的关系,这种说法不够准确。较为本质地说,种类并查集(包括普通并查集)维护的是一种 循环对称 的关系。

img

所以如果是三个及以上的集合,只要每个集合都是等价的,且集合间的每个关系都是等价的,就能够用种类并查集进行维护。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1142961.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Windows VS C++工程:包含目录、库目录、附加依赖项、附加包含目录、附加库目录配置与静态库、动态库的调用

文章目录 1 包含目录/附加包含目录1.1 区别和作用1.2 设置路径 2 库目录/ 附加库目录2.1 用途2.2 设置路径 3 附加依赖项3.1 用途3.2 设置路径 4 注意运行库的设置4 静态链接库调用方法5 动态链接库的调用方法 利用Visual Studio编写C工程文件时&#xff0c;时常需要自行配置自…

SQL中:语法总结(group by,having ,distinct,top,order by,like等等)

语法总结&#xff1a;group by&#xff0c;distinct ...... 1.group by2.聚集函数count 3.order by4.增insert、删&#xff08;drop、delete&#xff09;、改&#xff08;update、alter&#xff09;5.查select嵌套查询不相关子查询相关子查询使用的谓词使用的谓词子查询的相关谓…

大厂面试题-JVM中的三色标记法是什么?

目录 问题分析 问题答案 问题分析 三色标记法是Java虚拟机(JVM)中垃圾回收算法的一种&#xff0c;主要用来标记内存中存活和需要回收的对象。 它的好处是&#xff0c;可以让JVM不发生或仅短时间发生STW(Stop The World)&#xff0c;从而达到清除JVM内存垃圾的目的&#xff…

蓝桥杯每日一题2023.10.27

题目描述 快速排序 - 蓝桥云课 (lanqiao.cn) #include <stdio.h>int quick_select(int a[], int l, int r, int k) {int p rand() % (r - l 1) l;int x a[p];{int t a[p]; a[p] a[r]; a[r] t;}int i l, j r;while(i < j) {while(i < j && a[i] &…

centos 8 yum源不能使用问题

问题&#xff1a;新安装的centos 8 不能使用wget就不能下载和安装其他的软件 错误&#xff1a;为仓库 appstream 下载元数据失败 : Cannot prepare internal mirrorlist: No URLs in mirrorlist 解决&#xff1a; [rootlocalhost ~]# cd /etc/yum.repos.d [rootlocalhost yu…

栈、队列、矩阵的总结

栈的应用 括号匹配 表达式求值&#xff08;中缀&#xff0c;后缀&#xff09; 中缀转后缀&#xff08;机算&#xff09; 中缀机算 后缀机算 总结 特殊矩阵 对称矩阵的压缩存储 三角矩阵 三对角矩阵 稀疏矩阵的压缩存储

windows服务器环境下使用php调用com组件

Office设置 安装 office2013 且通过正版激活码激活 在组件服务 计算机 我的电脑 DOM 中找到 Microsoft Word 97 - 2003 文档 服务&#xff0c;右键属性 身份验证调整为 无 在 标识中 调整为 交互式用户 php环境设置 开启com组件扩展 在php.ini中设置 extensionphp_com_dotn…

关于亚马逊 CodeWhisperer 的测试反馈

CodeWhisperer 是亚马逊推出的实时 AI 编程助手&#xff0c;是一项基于机器学习的服务&#xff0c;它可以分析开发者在集成开发环境&#xff08;IDE&#xff09;中的注释和代码&#xff0c;并根据其内容生成多种代码建议。 亚马逊云科技开发者社区为开发者们提供全球的开发技术…

python---continue关键字对for...else结构的影响

代码&#xff1a; str1 laowang for i in str1:if i w:print(遇w不打印)continueprint(i) else:print(循环正常结束之后执行的代码) 图示&#xff1a;

速卖通商品详情API接口(标题|主图|SKU|价格|商品描述)

速卖通商品详情接口的用途是获取商品信息。 速卖通商品详情接口可以获取到商品的完整详细信息&#xff0c;包括商品名称、价格、图片、描述、规格、库存等&#xff0c;这些信息能够帮助用户了解商品特点、性能和市场定位&#xff0c;并做出购买决策。同时&#xff0c;通过使用…

0基础学习VR全景平台篇第114篇:全景图优化和输出 - PTGui Pro教程

上课&#xff01;全体起立~ 大家好&#xff0c;欢迎观看蛙色官方系列全景摄影课程&#xff01; 前情回顾&#xff1a;之前&#xff0c;我们详细介绍了如何用编辑器、控制点、垂直线等功能优化错位和矫正水平&#xff0c;然而这些调整不会马上生效。 我们需要在【优化】选项卡…

react-native调试

一、调试页面js代码 我用的真机调试&#xff0c;手机摇晃会出现出现的页面&#xff0c;点击debug 点击debug后&#xff0c;页面会出现&#xff0c;点按提示操作快捷键会出现开发者工具 注意&#xff1a;Chrome 中并不能直接看到 App 的用户界面&#xff0c;而只能提供 consol…

百度超级链XuperChain使用JavaSDK接入

环境 &#xff1a; ubuntu20 xuperchain 5.3 go 1.17 springboot : 2.5.14 前言 请提前启动好xchain的节点&#xff0c;我选择简单启一个xchain节点作为测试&#xff0c;并且使用默认端口37101 SpringBoot项目初始化 我们先进行SpringBoot项目的配置进行讲解&#xff0c;这里…

安卓逆向之雷电模拟器中控

一, 雷电模拟器 安装使用 官方地址: https://www.ldmnq.com ,官方论坛 https://www.ldmnq.com/forum/ . 有一个多开管理器,还有就是设置手机的参数比较关键。 二,雷电模拟器开启面具,安装LSP。 设置root 权限。

搜索引擎搜索技巧总结

晚上在B站上刷到一个关于搜索技巧的干货视频&#xff0c;这个视频真的不错&#xff0c;结尾还提到了AI时代的搜索思路之前自己也零碎的探索出了一些搜索技巧&#xff0c;但是没有总结&#xff0c;就没法稳定的加入自己的工作流&#xff0c;持续提高效率受到这个视频的启发&…

线扫相机DALSA--分频倍频计算公式及原理

分频倍频计算公式及原理 推导原理&#xff1a; 假设编码器脉冲精度为P&#xff1b;同步轮/辊周长为C&#xff0c;Fov为视野&#xff0c;Res为线扫相机分辨率&#xff0c;N代表N倍频编码器&#xff0c;分频为D&#xff0c;倍频为M 线扫项目常规采用N&#xff08;N 4&#xff0…

化身全能战士:ChatGPT助我横扫办公室【文末送书两本】

化身全能战士&#xff1a;ChatGPT助我横扫办公室 半年签约 16 本书有 ChatGPT 不会的吗&#xff1f;解锁 ChatGPT 秘技&#xff0c;化身全能战士ChatGPT 基本知识办公自动化职场学习与变现 作者简介结语购买链接参与方式往期赠书回顾 &#x1f3d8;️&#x1f3d8;️个人简介&a…

用HTML+CSS+JS实现一个简单的弹幕滚动留言板

在线演示地址&#xff1a;https://www.ewbang.com/community/board.html 本文利用HTMLCSSJS写了一个简单的弹幕滚动留言板小功能。 <!DOCTYPE html> <html><head><meta http-equiv"content-type" content"text/html;charsetutf-8" /&…

shell实验

1&#xff0e;编写脚本for1.sh&#xff0c;使用for循环创建20账户&#xff0c;账户名前缀由用户从键盘输入&#xff0c;账户初始密码由用户输入&#xff0c;例如&#xff1a;test1、test2、test3、....、test10 编写脚本&#xff0c;使用read -p提醒用户从键盘输入账户名前缀以…

轻量封装WebGPU渲染系统示例<1>-彩色三角形(源码)

当前示例源码github地址: https://github.com/vilyLei/voxwebgpu/blob/version-1.01/src/voxgpu/sample/VertColorTriangle.ts 此示例渲染系统实现的特性: 1. 用户态与系统态隔离。 2. 高频调用与低频调用隔离。 3. 面向用户的易用性封装。 4. 渲染数据和渲染机制分离。 …