【算法基础:数据结构】2.3 并查集

文章目录

  • 并查集
    • 算法原理(重要!⭐)
  • 经典例题
    • 836. 合并集合(重要!模板!⭐)
    • 837. 连通块中点的数量(维护连通块大小的并查集)
    • 240. 食物链(维护额外信息的并查集)🚹🚹🚹
  • 相关题目练习
    • 684. 冗余连接(并查集寻找冗余的边)
    • 685. 冗余连接 II⭐(分情况讨论)
    • 734. 句子相似性(这题不需要使用并查集)
    • 737. 句子相似性 II
    • 721. 账户合并⭐⭐⭐⭐⭐
  • 相关链接
  • 其余相关题目

并查集

https://oi-wiki.org/ds/dsu/
在这里插入图片描述

操作:

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

(路径压缩优化之后):近乎 O ( 1 ) O(1) O(1)

算法原理(重要!⭐)

在这里插入图片描述

将每个集合使用树的形式存储。

每个集合的编号 是 树根的编号。

每个节点存储 它的父节点是谁。 p[x] 表示 x 的父节点。


Q:如何判断树根?
A:p[x] == x

Q:如何求 x 的集合编号?
A:while (p[x] != x) x = p[x];
使用 路径压缩 的话就是 if (p[x] != x) p[x] = find(x);

Q:如何合并两个集合?
A:p[x] = y,即 把一棵树的根节点当成另一个树根节点的儿子。 (优化!:将这棵树中的所有节点都当成另一棵树根节点的子节点——优化之后几乎就是 O ( 1 ) O(1) O(1) 的了)(这个优化叫做 路径压缩

经典例题

836. 合并集合(重要!模板!⭐)

https://www.acwing.com/activity/content/problem/content/885/
在这里插入图片描述

注意下面 find(x) 的写法。

import java.util.Arrays;
import java.util.Scanner;

public class Main {
    static int[] p;
    public static void main(String[] args){
        Scanner scanner = new Scanner(System.in);
        int n = scanner.nextInt(), m = scanner.nextInt();
        p = new int[n + 1];
        Arrays.setAll(p, e -> e);   // 初始化,各个节点的祖宗节点设置成自己
        while (m-- != 0) {
            char op = scanner.next().charAt(0);
            int a = scanner.nextInt(), b = scanner.nextInt();
            if (op == 'M') {
                p[find(a)] = find(b);
            } else {
                if (find(a) == find(b)) System.out.println("Yes");
                else System.out.println("No");
            }
        }
    }

    // 返回 x 的祖宗节点 + 路径压缩
    static int find(int x) {
        if (p[x] != x) p[x] = find(p[x]);	// 路径压缩
        return p[x];
    }
}

837. 连通块中点的数量(维护连通块大小的并查集)

https://www.acwing.com/activity/content/problem/content/886/

在这里插入图片描述

find(x) 方法没有变化。

在合并两个点时:
先检查两个点是否已经在同一个连通块里了,直接 continue;
否则,先将一个点的根节点维护的数量加到另一个点的根节点维护的数量,然后再合并这两个点。

import java.util.Arrays;
import java.util.Scanner;

public class Main {
    static int[] p, size;       // p[i][0]是父节点  p[i][1]是该连通块中点的数量
    public static void main(String[] args){
        Scanner scanner = new Scanner(System.in);
        int n = scanner.nextInt(), m = scanner.nextInt();
        p = new int[n + 1];
        size = new int[n + 1];
        Arrays.setAll(p, e ->e);   // 初始化,各个节点的祖宗节点设置成自己
        Arrays.fill(size, 1);   // 初始化每个连通块的大小是 1
        while (m-- != 0) {
            String op = scanner.next();
            int a = scanner.nextInt();
            if ("C".equals(op)) {
                int b = scanner.nextInt();
                if (find(a) == find(b)) continue;       // 已经在同一个连通块里了
                size[find(b)] += size[find(a)];
                p[find(a)] = find(b);
            } else if ("Q1".equals(op)) {
                int b = scanner.nextInt();
                if (find(a) == find(b)) System.out.println("Yes");
                else System.out.println("No");
            } else {
                System.out.println(size[find(a)]);
            }
        }
    }

    // 返回 x 的祖宗节点 + 路径压缩
    static int find(int x) {
        if (p[x] != x) p[x] = find(p[x]);
        return p[x];
    }
}

240. 食物链(维护额外信息的并查集)🚹🚹🚹

https://www.acwing.com/activity/content/problem/content/887/
在这里插入图片描述

同一个集合表示:这一个集合内的所有动物之间的关系都可以确定。

对于每一句话中的 x 和 y,先检查它们是否在一个集合里,如果在同一个集合里,那么就可以判断这句话是否是假话。
如果是假话—— ++ans
如果是真话—— 将它们所在的两个集合合并。

如何确定同一个集合里各个动物之间的关系?
使用 d 数组维护各个节点到根节点之间的距离。(具体可以看下图:)

在这里插入图片描述
注意看代码中注释的解释。

import java.util.Arrays;
import java.util.Scanner;

public class Main {
    static int[] p, d;
    public static void main(String[] args){
        Scanner scanner = new Scanner(System.in);
        int n = scanner.nextInt(), k = scanner.nextInt(), ans = 0;
        // 一个集合里的所有动物之间的关系是可以推理出来的(这里同一个连通块并不表示它们是同一个种类,而是它们之间的关系可以确定)
        // 记录每个点和根节点的关系(用每个点和根节点之间的距离来表示它和根节点之间的距离)
        p = new int[n + 1];
        Arrays.setAll(p, e ->e);    // 初始化,各个节点的祖宗节点设置成自己
        d = new int[n + 1];         // d 维护的是各个节点到根节点的距离(距离表示它和根节点之间的关系)

        while (k-- != 0) {
            int op = scanner.nextInt(), x = scanner.nextInt(), y = scanner.nextInt();
            if (x > n || y > n) {               // x 或 y 比 n 大 —— 是假话
                ++ans;
                continue;
            }

            int px = find(x), py = find(y);     // 找到 x 和 y 的祖宗节点
            if (op == 1) {
                if (px == py && (d[x] - d[y]) % 3 != 0) ++ans;  // 已经在一个集合里了,判断是否合理
                else if (px != py) {            // 不在一个集合里,确定关系
                    p[px] = p[py];
                    d[px] = d[y] - d[x];        // 是为了令 d[px] + d[x] == d[y]
                }
            } else {
                if (px == py && (d[x] - d[y] - 1) % 3 != 0) ans++;  // 已经在一个集合里了,判断是否合理
                else if (px != py) {
                    p[px] = py;
                    d[px] = d[y] - d[x] + 1;    // x 吃 y,是为了令d[px] + d[x] = d[y] + 1。
                }
            }
        }
        System.out.println(ans);
    }

    static int find(int x) {
        if (p[x] != x) {
            int t = find(p[x]);     // t 是 x 的父节点的父节点
            d[x] += d[p[x]];        // 在路径压缩的过程中,子节点需要继承父节点到根节点的距离。(因为没压缩之前 d[x]是x到p[x]之间的距离)
            p[x] = t;
        }
        return p[x];
    }
}

关于 d[px] 的确定:
在这里插入图片描述

相关题目练习

684. 冗余连接(并查集寻找冗余的边)

https://leetcode.cn/problems/redundant-connection/
在这里插入图片描述

从前往后枚举每个边,将边的两个节点所在的集合合并,如果两个节点已经在同一个集合里了,说明这条边是多余的,那就保留这条边作为答案。

class Solution {
    public int[] findRedundantConnection(int[][] edges) {
        int n = edges.length;
        int[] p = new int[n + 1], ans = new int[]{-1, -1};
        Arrays.setAll(p, e -> e);
        for (int[] edge: edges) { 
            int x = edge[0], y = edge[1];
            if (find(x, p) == find(y, p)) ans = edge;   // 如果已经在一个集合了,说明这条边是多余的
            else p[find(x, p)] = find(y, p);            // 合并两个集合
        }
        return ans;
    }

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

685. 冗余连接 II⭐(分情况讨论)

https://leetcode.cn/problems/redundant-connection-ii/

在这里插入图片描述

需要选择一条边删除,删除之后使得有向图变成一个有根树。(有根树的定义见 题目)。

删除边之前,删除边的原因有三种:

  1. 有环
  2. 有冲突,两个节点指向了同一个节点
  3. 既有环又有冲突

解法参考:https://programmercarl.com/0685.%E5%86%97%E4%BD%99%E8%BF%9E%E6%8E%A5II.html#%E6%80%9D%E8%B7%AF

如果图中存在冲突,则一定需要删除冲突的两个边其中之一。
如果没有冲突,则一定存在环,使用并查集找到哪条边出现后图中出现了环。

class Solution {
    int[] p = new int[1001];

    public int[] findRedundantDirectedConnection(int[][] edges) {
        int n = edges.length;
        Arrays.setAll(p, e -> e);
        int[] inDegree = new int[n + 1];
        for (int[] e: edges) inDegree[e[1]]++;
        List<Integer> ls = new ArrayList();       // 记录入度为2的节点所对应的边
        for (int i = 0; i < n; ++i) {
            if (inDegree[edges[i][1]] == 2) ls.add(i);
        }

        // 检查删除哪条造成入度=2的边可以让图变成有根树
        if (ls.size() > 0) {
            if (isTreeAfterRemoveEdge(edges, ls.get(1))) return edges[ls.get(1)];
            else return edges[ls.get(0)];
        }

        // 删除造成有向环的那个
        return getRemoveEdge(edges);
    }

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

    // 检查删除一条边之后是否是有根树
    boolean isTreeAfterRemoveEdge(int[][] edges, int k) {
        for (int i = 0; i < edges.length; ++i) {
            if (i == k) continue;
            int x = edges[i][0], y = edges[i][1];
            if (find(x) == find(y)) return false;
            p[find(x)] = find(y);
        }
        return true;
    }

    // 检查存在环的情况下应该删除哪条边
    int[] getRemoveEdge(int[][] edges) {
        for (int[] e: edges) {
            int x = e[0], y = e[1];
            if (find(x) == find(y)) return e;
            p[find(x)] = find(y);
        }
        return new int[]{-1, -1};
    }
}

734. 句子相似性(这题不需要使用并查集)

https://leetcode.cn/problems/sentence-similarity/
在这里插入图片描述

在这里插入代码片

737. 句子相似性 II

https://leetcode.cn/problems/sentence-similarity-ii/

在这里插入图片描述

这一题的相似关系是可传递的,所以可以使用并查集。

在这里插入代码片

721. 账户合并⭐⭐⭐⭐⭐

https://leetcode.cn/problems/accounts-merge/description/

在这里插入图片描述

在这里插入代码片

相关链接

https://oi-wiki.org/ds/dsu/

其余相关题目

1851. 包含每个查询的最小区间

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【AI绘画】AI绘画乐趣:稳定增强扩散技术展现

目录 前言一、Stable Diffusion是什么&#xff1f;二、安装stable-diffusion-webui1. python安装2. 下载模型3. 开始安装&#xff1a;4. 汉化&#xff1a;5. 模型使用&#xff1a;6. 下载新模型&#xff1a;7. 基础玩法 三、总结 前言 本文将借助stable-diffusion-webui项目来…

【数据结构与算法】哈夫曼编码(最优二叉树)实现

哈夫曼编码 等长编码&#xff1a;占的位置一样 变长编码&#xff08;不等长编码&#xff09;&#xff1a;经常使用的编码比较短&#xff0c;不常用的比较短 最优&#xff1a;总长度最短 最优的要求&#xff1a;占用空间尽可能短&#xff0c;不占用多余空间&#xff0c;且不…

网络版计算器

本次我们实现一个服务器版本的简单的计算器&#xff0c;通过自己在应用层定制协议来将数据传输出去。 协议代码 此处我们为了解耦&#xff0c;采用了两个类&#xff0c;分别表示客户端的请求和服务端的响应。 Request class Request { public:Request(){}Request(int x, int…

Unity 任意数据在Scene窗口Debug

任意数据在Scene窗口Debug &#x1f354;效果&#x1f96a;食用方法 &#x1f354;效果 如下所示可以很方便的把需要Debug的数据绘制到Scene中&#xff08;普通的Editor脚本只能够对MonoBehaviour进行Debug&#xff09; &#x1f96a;食用方法 &#x1f4a1;. 新建脚本继承Z…

实例018 类似windows xp的程序界面

实例说明 在Windows XP环境下打开控制面板&#xff0c;会发现左侧的导航界面很实用。双击展开按钮&#xff0c;导航栏功能显示出来&#xff0c;双击收缩按钮&#xff0c;导航按钮收缩。下面通过实例介绍此种主窗体的设计方法。运行本例&#xff0c;效果如图1.18所示。 ​编辑…

如何顺势而为,让ChatGPT为教育所用?

恐惧和回避无法阻挡科技的浪潮&#xff0c;教育与AI的深度融合时代已经到来&#xff0c;如何把AI当做工具&#xff0c;把其成为教育的机会而非威胁&#xff0c;是教育体系未来不得不得面对的新变化。 接受ChatGPT作为一种教学辅助工具&#xff0c;成为教师的朋友或者帮手&…

Leetcode每日一题:979. 在二叉树中分配硬币(2023.7.14 C++)

目录 979. 在二叉树中分配硬币 题目描述&#xff1a; 实现代码与解析&#xff1a; dfs&#xff08;后序遍历&#xff09; 原理思路&#xff1a; 979. 在二叉树中分配硬币 题目描述&#xff1a; 给定一个有 N 个结点的二叉树的根结点 root&#xff0c;树中的每个结点上都对…

5.postgresql--COALESCE

在 PostgreSQL 中&#xff0c; COALESCE函数返回第一个非空参数。它通常与 SELECT 语句一起使用以有效处理空值。 COALESCE函数接受无限数量的参数。它返回第一个不为空的参数。如果所有参数都为 null&#xff0c;则 COALESCE函数将返回 null。 COALESCE函数从左到右计算参数&a…

doris恢复库恢复表

今天眼疾手快 不小心删了公司生产环境的表 而且碰巧这个数据没有备份的 当时哥们就呆住 还好doris升级过1.2 刚推出了恢复数据的功能~~~~~这里给老天爷磕一个了~~~~~~ 数据删除恢复 Doris为了避免误操作造成的灾难&#xff0c;支持对误删除的数据库/表/分区进行数据恢复&…

MongoDB初体验-安装使用教程2023.7

前言&#xff1a;博主第一次接触MongoDB&#xff0c;看了一圈网上现有的教程&#xff0c;不是缺少细节就是有问题没交代清楚&#xff0c;特整理了一下自己安装运行的过程&#xff0c;从下载安装到开机自启&#xff0c;全程细节齐全、图文并茂、简单易懂。 目录 1. 从官网下载2…

JavaWeb课程设计项目实战(03)——开发准备工作

版权声明 本文原创作者&#xff1a;谷哥的小弟作者博客地址&#xff1a;http://blog.csdn.net/lfdfhl 在正式进入项目开发之前请先完成以下准备工作。 数据库语句 请创建数据库和表并完成数据初始化工作。 初始化数据库 请在MySQL数据库中创建名为studentinformationmanag…

Nacos服务注册和配置中心(Config,Eureka,Bus)1

SCA(Spring Cloud Alibaba)核心组件 Spring Cloud是若干个框架的集合&#xff0c;包括spring-cloud-config、spring-cloud-bus等近20个子项目&#xff0c;提供了服务治理、服务网关、智能路由、负载均衡、断路器、监控跟踪、分布式消息队列、配置管理等领域的解决方案,Spring C…

ADB 命令结合 monkey 的简单使用,超详细

一&#xff1a;ADB简介 1&#xff0c;什么是adb&#xff1a; ADB 全称为 Android Debug Bridge&#xff0c;起到调试桥的作用&#xff0c;是一个客户端-服务器端程序。其中客户端是用来操作的电脑&#xff0c;服务端是 Android 设备。ADB 也是 Android SDK 中的一个工具&…

unity背景缓动动效

这算是一个很常见的小功能&#xff0c;比如我们在玩横版游戏的时候&#xff0c;背景动画会以一定的频率运动&#xff0c;其实现方式也有很多种。 比如&#xff0c;使用UGUI的imageanimtion动画的方式&#xff0c;自己k桢实现。 还可以使用材质球本身的功能来实现&#xff0c;关…

【MySQL】查询进阶

查询进阶 数据库约束约束类型NULL , DEFAULT , UNIQUE 约束主键约束外键约束 聚合查询聚合函数group by子句HAVING 联合查询内连接外连接自连接子查询单行子查询多行子查询 数据库约束 约束类型 NOT NULL #表示某行不能储存空值 UNIQUE #保证每一行必须有唯一的值 DEFAULT #规…

UnxUtils工具包,Windows下使用Linux命令

1. 前言 最近写批处理多了&#xff0c;发现Windows下的bat批处理命令&#xff0c;相比Linux的命令&#xff0c;无论是功能还是多样性&#xff0c;真的差太多了。但有时候又不得不使用bat批处理&#xff0c;好在今天发现了一个不错的工具包&#xff1a;UnxUtils&#xff0c;这个…

【Java/大数据】Kafka简介

Kafka简介 Kafka概念关键功能应用场景 Kafka的原理Kafka 的消息模型早期的队列模型发布-订阅模型Producer、Consumer、Broker、Topic、PartitionPartitionoffsetISR Consumer Groupleader选举Controller leaderPartition leader producer 的写入流程 多副本机制replicas的同步时…

Godot实用代码-存取存档的程序设计

1. Settings.gd 全局变量 用于保存玩家设置 对应Settings.json 2. Data.gd 全局变量 用于保存玩具数据 对应Data.json 实践逻辑指南 1.在游戏开始的时候&#xff08;游戏场景入口的_ready()处&#xff0c; Settings.gd

基于linux下的高并发服务器开发(第一章)- 模拟实现 ls-l 命令

这一小节会用到上面两张图的红色框里面的变量 任务&#xff1a; 模拟实现 ls -l 指令 -rw-rw-r-- 1 nowcoder nowcoder 12 12月 3 15:48 a.txt #include <stdio.h> #include <sys/types.h> #include <sys/stat.h> #include <unistd.h> #include <p…

keepalived 实现双机热备

文章目录 一、说明二、概念解释三、环境准备四、操作过程五、验证 一、说明 我们经常听说 nginx keepalived 双机热备&#xff0c;其实在这里&#xff0c;双机热备只是利用 keepalived 实现两个节点的故障切换&#xff0c;当主节点挂了&#xff0c;备用节点顶上&#xff0c;保…