并查集详解及应用

news2024/11/18 1:33:57

文章和代码已经归档至【Github仓库:https://github.com/timerring/algorithms-notes 】或者公众号【AIShareLab】回复 算法笔记 也可获取。

文章目录

    • 并查集
        • 优化方法
      • 例题:合并集合
        • code
      • 例题:连通块中点的数量
        • code
      • 模板总结
      • 例题:食物链
        • 基本分析
        • code

并查集

1.将两个集合合并

2.询问两个元素是否在一个集合当中。

基本原理:每个集合用一棵树来表示。**树根的编号就是整个集合的编号。**每个节点存储它的父节点,p[]表示x的父节点。

  • 如何判断树根:if (p[x]== x) //它的父节点就是它自己
  • 如何求x的集合编号: while (p[x]!= x)x= p[x];
  • 如何合并两个集合:px是x的集合编号,py是y的集合编号。p[x]=y

优化方法

路径压缩(常用),按秩合并。

路径压缩就是将已经找到根节点的所有点,修改其父节点为根节点,以达到降低时间复杂度近似为O(1)的目的。(因为若一直采用循环来找父节点,时间复杂度将会由树的高度决定,十分冗余)。这里是采用递归的方式完成。

注意:通常scanf在读字符串时会自动忽略空格和回车。因此在用scanf读入一个字符或者字母时,推荐采用字符串的形式%S。

例题:合并集合

一共有 n 个数,编号是 1∼n,最开始每个数各自在一个集合中。

现在要进行 m 个操作,操作共有两种:

  1. M a b,将编号为 a 和 b 的两个数所在的集合合并,如果两个数已经在同一个集合中,则忽略这个操作;
  2. Q a b,询问编号为 a 和 b 的两个数是否在同一个集合中;

输入格式

第一行输入整数 n 和 m。

接下来 m 行,每行包含一个操作指令,指令为 M a bQ a b 中的一种。

输出格式

对于每个询问指令 Q a b,都要输出一个结果,如果 a 和 b 在同一集合内,则输出 Yes,否则输出 No

每个结果占一行。

数据范围

1 ≤ n , m ≤ 1 0 5 1≤n,m≤10^5 1n,m105

输入样例:

4 5
M 1 2
M 3 4
Q 1 2
Q 1 3
Q 3 4

输出样例:

Yes
No
Yes

code

#include <iostream>
using namespace std;

const int N = 100010;

int p[N];

int find(int x)
{
    // 注意这里是通过 递归 的方式进行查找并且路径压缩的
    // 由于返回时逐层返回,因此会把每一层的父节点都置为根节点
    if(p[x] != x) p[x] = find(p[x]);
    // 返回根节点 即p[x] == x 的节点
    return p[x];
}

int main()
{
    int n, m;
    scanf("%d%d", &n, &m);
    for(int i = 1; i <= n; i ++ )p[i] = i;
    
    while(m --)
    {
        char op[2];
        int a, b;
        scanf("%s%d%d", op, &a, &b);
        if(*op == 'M') p[find(a)] = find(b);
        else
        {
            if(find(a) == find(b)) puts("Yes");
            else puts("No");
        }
    }
    return 0;
}

例题:连通块中点的数量

给定一个包含 n 个点(编号为 1∼n)的无向图,初始时图中没有边。

现在要进行 m 个操作,操作共有三种:

  1. C a b,在点 a 和点 b 之间连一条边,a 和 b 可能相等;
  2. Q1 a b,询问点 a 和点 b 是否在同一个连通块中,a 和 b 可能相等;
  3. Q2 a,询问点 a 所在连通块中点的数量;

输入格式

第一行输入整数 n 和 m。

接下来 m 行,每行包含一个操作指令,指令为 C a bQ1 a bQ2 a 中的一种。

输出格式

对于每个询问指令 Q1 a b,如果 a 和 b 在同一个连通块中,则输出 Yes,否则输出 No

对于每个询问指令 Q2 a,输出一个整数表示点 a 所在连通块中点的数量

每个结果占一行。

数据范围

1 ≤ n , m ≤ 1 0 5 1≤n,m≤10^5 1n,m105

输入样例:

5 5
C 1 2
Q1 1 2
Q2 1
C 2 5
Q2 5

输出样例:

Yes
2
3

code

#include <iostream>

using namespace std;
const int N = 1e5 + 10;
int p[N], cnt[N];

int find (int x)
{
    if(p[x] != x) p[x] = find(p[x]);
    return p[x];
}

int main()
{
    int n, m;
    cin >> n>> m;
    for(int i = 1; i <= n; i ++)
    {
        p[i] = i;
        cnt[i] = 1;
    }
    
    while(m --)
    {
        string op;
        cin >> op;
        int a, b;

        if(op == "C")
        {
            cin >> a >> b;
            if(find(a) == find(b)) continue; // 如果同集合 则跳过 避免多次合并
            cnt[find(b)] += cnt[find(a)];
            p[find(a)] = find(b);
        }
        
        if(op == "Q1")
        {
            cin >> a >> b;
            if(find(b) == find(a)) puts("Yes");
            else puts("No");
        }
        
        if(op == "Q2")
        {
            cin >> a;
            cout << cnt[find(a)] << endl;
        }
        
    }
    return 0;
}

以上的版本比较便于理解(推荐),也可以采用如下的写法·:

#include<iostream>
using namespace std;

const int N = 100010;
int n, m;
int p[N], cnt[N];

int find(int x)
{
    if(p[x] != x) p[x] = find(p[x]);
    return p[x];
}

int main()
{
    cin >> n >> m;
    
    for(int i = 1; i <= n; i ++)
    {
        p[i] = i;
        cnt[i] = 1;
    }
    
    while(m --)
    {
        string op;
        cin >> op;
        int a, b;
        
        if(op == "C")
        {
            cin >> a >> b;
            // 注意这里是先将两个根节点取出来了
            a = find(a), b = find(b);
            // 避免同一集合中进行合并
            if(a != b)
            {
                // 然后再进行改动
                p[a] = b;
                cnt[b] += cnt[a];
                // 如果不先取出两个根节点保存,而是直接进行操作的话,必须要先进行计数赋值,再改动根节点(即上面两句话颠倒过来,否则会造成原来的根节点发生了变化,存储的cnt也相应改变了)
            }
        }
        else if(op == "Q1")
        {
            cin >> a >> b;
            if(find(a) == find(b)) puts("Yes");
            else puts("No");
        }
        else
        {
            cin >> a;
            cout << cnt[find(a)] << endl;
        }
    }
    return 0;
}

这里需要额外注意的是:a = find(a), b = find(b)先将两个根节点取出来了,如果不先取出两个根节点保存,而是直接进行操作的话,必须要先进行计数赋值,再改动根节点(否则会造成原来的根节点发生了变化,存储的cnt也相应改变了)

图解分析:

将1,5合并,find(1) = 3 find(5) = 4

p[3] = 4

这时候有8个点相连接,合并的数目更新方式:

  • size[3] = 4 以3为根节点下有4个连通块
  • size[4] = 4 以4为根节点下有4个连通块

更新4节点的连通块情况 size[4] = size[4] + size[3] = 8

模板总结

int find(int x)
{
    // 注意这里是通过递归的方式进行查找并且路径压缩的
    // 由于返回时逐层返回,因此会把每一层的父节点都置为根节点
    if(p[x] != x) p[x] = find(p[x]);
    return p[x];
}

例题:食物链

动物王国中有三类动物 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 和 K 句话,输出假话的总数。

输入格式

第一行是两个整数 N 和 K,以一个空格分隔。

以下 K 行每行是三个正整数 D,X,Y,两数之间用一个空格隔开,其中 D 表示说法的种类。

若 D=1,则表示 X 和 Y 是同类。

若 D=2,则表示 X 吃 Y。

输出格式

只有一个整数,表示假话的数目。

数据范围

1 ≤ N ≤ 50000 1≤N≤50000 1N50000,
0 ≤ K ≤ 100000 0≤K≤100000 0K100000

输入样例:

100 7
1 101 1 
2 1 2
2 2 3 
2 3 3 
1 1 3 
2 3 1 
1 5 5

输出样例:

3

基本分析

余1:可以吃根节点

余2:可以被根节点吃

余0:与根节点是同类

因此合并后,x与y同类,则可以说 d[x] + d[px] 与 d[y] 在mod3的意义下是同余的,即可以用以下式子表示

(d[x] + d[px] - d[y]) % 3 = 0 可知 d[px] = d[y] - d[x]

code

#include <iostream>

using namespace std;

const int N = 50010;

int n, m;
//p[]寻找根节点,d[]求到父节点(find更新前)/ 根节点(find更新后)的距离
int p[N], d[N];

int find(int x)
{
    if (p[x] != x)
    {
        int u = p[x];  // u记录旧的父节点
        p[x] = find(p[x]); // 路径压缩,新父节点变成根节点了
        d[x] += d[u];  // x到新根节点的距离等于x到旧父节点的距离加上旧父节点到根节点的距离
        
        // 以下版本不好理解:
        // 保存根节点
        //int t = find(p[x]);
        // 更新距离 d[x] = d[x] + d[p[x]]
        //d[x] += d[p[x]];
        // 指向根节点
        //p[x] = t;
    }
    return p[x];
}

int main()
{
    scanf("%d%d", &n, &m);

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

    int res = 0;
    while (m -- )
    {
        int t, x, y;
        scanf("%d%d%d", &t, &x, &y);
        // 条件2:当前的话中 X 或 Y 比 N 大,就是假话
        if (x > n || y > n) res ++ ;
        else
        {
            // 寻找根节点
            int px = find(x), py = find(y);
            if (t == 1)
            {
                // 若在同一个集合 则同类相距距离必是3的倍数 若不是则是假话
                // 其中 (d[x] - d[y]) % 3 不可写为 d[x] % 3 != d[y] % 3, 因为 d[x], d[y] 可能为负数(一正一负),可改做 (d[x] % 3 + 3) % 3 != (d[y] % 3 + 3) % 3, 注意:负数 mod 正数为负数
                // 相对来说直接写(d[x] - d[y]) % 3还是比较简洁的,因为不为0就是假话
                if (px == py && (d[x] - d[y]) % 3) res ++ ;
                else if (px != py)
                {
                    // 若不在同一个集合,x 合并到 y 中
                    p[px] = py;
                    // 两个距离相减就会变成负数,代码中有d[px] = d[y] - d[x];
                    d[px] = d[y] - d[x];
                }
            }
            else
            {
                // 若在同一个集合 则x吃y必满足mod3的意义下d[x]比d[y]大1  可表示为(d[x] - d[y] - 1) mod 3 = 0
                if (px == py && (d[x] - d[y] - 1) % 3) res ++ ;
                else if (px != py)
                {
                    p[px] = py;
                    // (d[x] - d[y] - 1) % 3 == 0, d[x] + d[px] - 1 = d[y]
                    d[px] = d[y] + 1 - d[x];
                }
            }
        }
    }

    printf("%d\n", res);
    return 0;
}

⚠关于公式的详细理解:如果还是难以理解,可以尝试模拟一下过程:

1.初始状态下:

2.普通情况下:

⚠其中关于find的详细理解:

find(x)有两个功能: 1 路径压缩, 2 更新 d[x]

假设有一棵树 a -> b -> c -> d, 根节点为 d。d[b]一开始等于 b、c 之间的距离,再执行完路径压缩命令之后,d[b] 等于b、d之间的距离。 d[a] += d[b]: 为了确保d[a]等于 节点a、d的距离,d[b]必须等于b 、d的距离,所以要先调用find(b)更新d[b], 同时p[x] = find(b)会改变p[x]的值,结果就会变成d[a] += d[d],所以先用一个变量把p[a]的值存起来。

int t = p[x]; // t = p[a] = b
p[x] = find(p[x]); // b = find(b) 每次递归都会对路径进行压缩
d[x] += d[t];// d[a] = d[a](a --> b) + d[b](b --> d)
// find(b):
int t = p[x]; // t = p[b] = c
p[x] = find(p[x]); // c = find(c)
d[x] += d[t];// d[b] = d[b](b --> c) + d[c](c --> d)

关键就是既要先执行find(p[x]), 又要让d[x] += d[p[x]]中p[x]的值保持不变,所以代码可以这么写

int t = p[x];
p[x] = find(p[x]);
d[x] += d[t];

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

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

相关文章

(链表) 143. 重排链表 ——【Leetcode每日一题】

❓143. 重排链表 难度&#xff1a;中等 给定一个单链表 L 的头节点 head &#xff0c;单链表 L 表示为&#xff1a; L 0 L_0 L0​ → L 1 L_1 L1​ → … → L n − 1 L_{n-1} Ln−1​ → L n L_n Ln​ 请将其重新排列后变为&#xff1a; L 0 L_0 L0​ → L n L_n Ln​ →…

hudi系列-文件系统视图(FileSystemView)

hudi表的数据一直在演变过程中,存储在文件系统中的数据文件也在不断增加和版本迭代,hudi提供了表级别的文件系统视图(filesystem view)来简单、直观地了解表中的数据分布情况、数据文件的状态和变化,以及数据的版本控制信息。文件系统视图提供了以下一些功能: 获取最新的ba…

如何看待分层测试?

分层测试分了个寂寞&#xff1f; 分层测试这个风吹了好多年&#xff0c;不分层都不好意思说自己是专业测试。各互联网公司更是对此乐此不疲&#xff0c;测试架构、测试平台&#xff0c;搞了一套又一套&#xff0c;然而。。。 理想总是丰满&#xff0c;现实总是骨干&#xff0…

【探索 Kubernetes|集群搭建篇 系列 5】简化 Kubernetes 的部署,深入解析其工作流程

前言 大家好&#xff0c;我是秋意零。 在前面 4 个章节中&#xff0c;我们充分了解了容器技术和 Kubernes 原生时代引擎的架构和设计思想&#xff0c;今天分享的主要内容是&#xff0c;探索 Kubernetes 部署&#xff0c;深入解析其工作流程 &#x1f47f; 简介 &#x1f3e0…

OpenCV 项目开发实战--对图像进行对齐 (ECC)-附带 ( C++ / Python )代码实现

文末附基于Python和C++两种方式实现的测试代码下载链接 图 1. 左图:来自 Prokudin-Gorskii 收藏的图像。右:相同的图像,通道对齐。 左边的图片是名为Prokudin-Gorskii收藏的历史照片集的一部分。这张照片是一位俄罗斯摄影师在 1900 年代早期使用早期彩色相机拍摄的。由于相…

升级丨AMAA汽车社媒营销归因升级,提升ROI还用愁吗?

“如其抱怨&#xff0c;不如享受卷的过程。昨天的手机行业&#xff0c;就是今天的汽车行业。” “以后国内可能只剩下五个 ‘东西南北中’&#xff0c;打麻将一样。” 在刚结束不久的2023年中国汽车重庆论坛&#xff08;CACS&#xff09;上&#xff0c;来自车企大佬们的“声音…

【JVM】JVM 判断对象存活算法(引用计数算法与根可达性分析算法)

文章目录 引用计数算法介绍问题 根可达性分析算法介绍GC Root 在 JVM 中&#xff0c;需要检查出还有哪些存活对象&#xff08;就是哪些对象还在使用&#xff09;&#xff0c;哪些未存活对象&#xff0c;未存活对象又被称之为垃圾对象&#xff0c;只有知道了哪种是垃圾对象&…

实战必看!工程项目中简单高效的示教编程

大家好&#xff0c;我是华山自控编程朱老师 示教编程是一种简单高效的方法&#xff0c;通过可视化界面和图像采集&#xff0c;可以指导编程操作&#xff0c;提高生产效率。 在本视频中&#xff0c;我们将分享一个项目&#xff0c;重点介绍如何进行简单高效的示教编程。 实战必…

端午节到了,用Python画一个粽子吧!

文章目录 粽子曲面真粽子曲面正弦曲面 粽子曲面 之前通过matplotlib绘制了圆锥曲面&#xff0c;但matplotlib绘制曲面图有几个问题&#xff0c;其中plot_surface需要有规范的xOy坐标&#xff0c;然后根据其坐标绘制z轴参数&#xff1b;plot_trisurf则必须有明确的三角面的顶点…

【AI人工智能】你们都是用它来干嘛?

&#x1f680; 个人主页 极客小俊 ✍&#x1f3fb; 作者简介&#xff1a;web开发者、设计师、技术分享博主 &#x1f40b; 希望大家多多支持一下, 我们一起进步&#xff01;&#x1f604; &#x1f3c5; 如果文章对你有帮助的话&#xff0c;欢迎评论 &#x1f4ac;点赞&#x1…

基于Python的接口自动化测试框架

目录 前言&#xff1a; 项目背景 工具选择 框架思路 第三方库介绍 代码实现 不足之处 前言&#xff1a; Python是一种流行的编程语言&#xff0c;Python的易学性和易用性使得它成为编写接口自动化测试框架的理想选择。在Python中&#xff0c;有许多库可以帮助我们执行HTTP请求…

【探索 Kubernetes|作业管理篇 系列 8】探究 Pod 的 API 对象属性级别与重要字段用法

前言 大家好&#xff0c;我是秋意零。 本专栏上一篇中&#xff0c;我们介绍了 Pod 的核心思想以及 Pod 的工作原理。今天我们还是探讨 Pod &#xff0c;聊一聊 Pod 的基本概念。 &#x1f47f; 简介 &#x1f3e0; 个人主页&#xff1a; 秋意零&#x1f9d1; 个人介绍&#x…

Centos7安装OpenJDK8到指定目录

系统配置 阿里云轻量应用服务器&#xff08;Linux, centos7.9 x64&#xff09; 安装步骤 1、官方下载OpenJDK 1.8 Java Platform, Standard Edition 8 ReferenceImplementations 2、通过WinScp工具&#xff0c;将下载好的安装包&#xff0c;上传到服务器 WinScp下载地址&…

ES7、ES8、ES9、ES10、ES11、ES12都增加了那些新特性?

大厂面试题分享 面试题库 前后端面试题库 &#xff08;面试必备&#xff09; 推荐&#xff1a;★★★★★ 地址&#xff1a;前端面试题库 web前端面试题库 VS java后端面试题库大全 ES7 ES2016&#xff08;ES7&#xff09;中新增了如下特性&#x1f447; Array.prototype…

批量压缩和修复Access数据库 Batch Access Database Compactor Crack

批量压缩和修复Access数据库 Batch Access Database Compactor 快速压缩和修复访问数据库。它是一个批处理Access数据库压缩程序&#xff0c;可以在不安装Office的情况下压缩和修复Access数据库。 Batch Access数据库压缩程序是一种软件工具&#xff0c;正如其标题所示&#x…

AC电源延长转换线:同为科技(TOWE)品牌,专注品质,安全连接

AC电源转换线虽是连接用电设备的一个配件&#xff0c;结构也相对简单&#xff0c;然而对用电设备的安全稳定运行起着重要的作用。AC电源转换线是通过电压较高的交流电的线材&#xff0c;因此产品质量需要具有一定安全标准。 即使AC电源转换线结构并不十分复杂&#xff0c;但也不…

学会使用这些Lumion照片级渲染技巧,秒出大片

Lumion 是一种渲染软件&#xff0c;可帮助建筑师以清晰、感性的方式传达他们的设计。十年来&#xff0c;人们发现 Lumion 的每个新版本都有新的功能、工作流程和控制方法。他们可以在 Revit、SketchUp 或其他 BIM 程序等软件中建模&#xff0c;并将模型导入 Lumion 进行渲染&am…

隐语“官网”新版上线!彩蛋连连,速来探索

距2022年7月隐语开源马上就要1周年啦&#x1f389;&#xff5e;隐语官网也迎来了2.0全新大改版。为了方便用户快速找到所需的文档&#xff0c;我们新增了“关键词”检索功能。前往官网&#xff0c;立即体验 https://www.secretflow.org.cn&#xff5e;还有惊喜彩蛋等你来探索&a…

国内精选五大优质互联网理财软件app最新排名(综合测评)

随着移动网络技术的飞速发展和大众经济水平的逐渐提高&#xff0c;现在市场上有很多互联网理财软件&#xff0c;选择一款好的软件是非常重要的。首先&#xff0c;要确保软件的安全性和可靠性&#xff0c;因为金融交易需要非常高的安全保障。 其次&#xff0c;要考虑软件的交易…

Channel通道使用进阶:通道关闭原则、生产者消费者问题、高并发map

1.Channel情况总结 在进行Channel通道使用之前&#xff0c;先根据总结有缓冲型channel使用的情况&#xff0c;若对下表有疑问可以前往Golang Channel 实现原理与源码分析进行阅读&#xff0c;如下所示&#xff1a; 从上表中我们可以发现&#xff0c;若我们已经对channel初始化…