2024042701-disjoint-set

并查集 Disjoint-Set

一、前言

并查集的历史

1964年, Bernard A. Galler 和 Michael J. Fischer 首次描述了不相交的并查集,1975 年,Robert Tarjan 是第一个证明O(ma(n))(逆阿克曼函数)算法时间复杂度的上限,并且在 1979 年表明这是受限情况的下限。

2007 年,Sylvain Conchon 和 Jean-Christophe Filliâtre 开发并查集数据结构的半持久版本,并使用证明助手 Coq 将其正确性形式化。 “半持久”意味着结构的先前版本被有效地保留,但访问数据结构的先前版本会使以后的版本无效。它们最快的实现了几乎与非持久算法一样高效的性能且不执行复杂性分析。

二、并查集数据结构

并查集数据结构(也称为联合-查找数据结构或合并-查找集)基于数组实现的一种跟踪元素的数据结构,这些元素被划分为多个不相交(非重叠)的子集。

它提供了近乎恒定的时间操作(以逆阿克曼函数为界)来添加新集合、合并现有集合以及确定元素是否在同一个集合中。除了推荐算法、好友关系链、族谱等,并查集在 Kruskal 的算法中扮演着关键角色,用于寻找无向边加权图的最小生成树。

并查集的定义乍一看有些抽象,也不知道到底在什么场景使用。所以小傅哥给大家举个例子;在以前江湖上有很多门派,各门派见的徒子徒孙碰面难免切磋。为了不让大家打乱套,都要喊一句:”报上名来“ —— 在下叶问,佛山咏春派,师承陈华顺。那么对于这样的场景,我们可以使用并查集给各门派成员合并,方便汇总查询。如图;

  • 张无忌:既然你不是明教,也不是武当的,我就不客气了。
  • 赵敏:不客气你还能咋!我学过咏春!
  • 张无忌:看招!
  • 赵敏:张无忌放开啊,我讨厌你!😒

🤔 但各门派徒子徒孙众多,如果下回遇到赵敏的A丫鬟的Aa丫鬟,没等Aa报家门找族谱完事,也被抠脚了咋办?所以基于这样的情况,要对并查集的各级元素进行优化合并,减少排查路径。

01:粗暴合并02:数量合并03:排序合并04:压缩路径
0→6、6→0 不控制合并数量少合并到数量多排序小合并到排序大排序合并时压缩路径

为了尽可能少的检索次数到根元素,在01:粗暴合并的基础上,有了基于数量、排序的合并方式,同时还包括可以压缩路径。这样再索引到根节点的时间复杂度就又降低了。接下来小傅哥就带着大家看看各个场景的在代码中的操作过程。

三、并查集结构实现

并查集的实现非常巧妙,只基于数组就可以实现出一个树的效果(基于数组实现的还有二叉堆也是树的结构)。

public class DisjointSet {
	  // 元素
    public int[] items;
    // 数量【可选】
	public int[] count;
	// 排序【可选】
	public int[] rank;
}

并查集的元素存放在数组中,通过对数组元素的下标索引指向其他元素,构成一棵树。count 数量、rank 排序,是用于对并查集合并元素时的优化处理。

  • 源码地址:https://github.com/fuzhengwei/java-algorithms
  • 本章源码:https://github.com/fuzhengwei/java-algorithms/tree/main/data-structures/src/main/java/disjoint_set
  • 动画演示:https://visualgo.net/zh/ufds?slide=2—— 并查集结构初次理解还是比较困难的,可以结合学习内容的同时做一些动画演示。

1. 默认合并 - union(1, 8)

@Override
public int find(int i) {
    if (i < 0 || i >= items.length) throw new IllegalArgumentException("Index out of range.");
    return items[i];
}

@Override
public void union(int parent, int child) {
    int parentVal = find(parent);
    int childVal = find(child);
    if (parentVal == childVal) return;
    for (int i = 0; i < items.length; i ++){
        // 所有值等于原孩子节点对应值的都替换为新的父节点值
        if (items[i] == childVal){
            items[i] = parentVal;
        }
    }
}

目标:union(1, 8) 将8的根节点合并到1的根节点

  • union 是合并元素的方法,两个入参意思是把 child 指向的根节点,指向 parent 指向的根节点。后面所有案例中 union 方法属性字段意思相同。
  • find 找到元素对应的根节点值,之后使用 union 方法对 items 数组内的元素全部遍历,把所有值等于 child 的节点,都替换为 parent 节点值。
  • 每次合并都for循环比较耗时,所以后续做了一些列的优化。

2. 粗暴合并 - union(1, 8)

@Override
public int find(int i) {
    if (i < 0 || i >= items.length)
        throw new IllegalArgumentException("Index out of range.");
    // 找到元素的根节点,当i == item[i],就是自己指向自己,这个节点就是根节点
    while (i != items[i]) {
        i = items[i];
    }
    return i;
}

@Override
public void union(int parent, int child) {
    // 父亲节点的根节点下标值
    int parentRootIdx = find(parent);
    // 孩子节点的根节点下标值
    int childRootIdx = find(child);
    if (parentRootIdx == childRootIdx) return;
    // 孩子节点值替换为父节点值
    items[childRootIdx] = items[parentRootIdx];
}

目标:union(1, 8) 将8的根节点合并到1的根节点

  • find 循环找到置顶节点的最终根节点,例如;8 → 6、6 → 6,那么说明8的根节点是6,因为6自己指向自己了,它就是根节点。
  • union 将 8 指向的根节点 6,更换为 1 指向的根节点 0。最终替换完就是 6 → 0,那么8的根节点有也是0了。
  • 这样虽然减少了每次 for 循环更新,但粗暴的合并会对节点的索引带来一定的复杂度。所以还需要继续优化。

3. 数量合并 - union(1, 8)

@Override
public int find(int i) {
    if (i < 0 || i >= items.length)
        throw new IllegalArgumentException("Index out of range.");
    // 找到元素的根节点,当i == item[i],就是自己指向自己,这个节点就是根节点
    while (i != items[i]) {
        i = items[i];
    }
    return i;
}

@Override
public void union(int parent, int child) {
    // 父亲节点的根节点下标值
    int parentRootIdx = find(parent);
    // 孩子节点的根节点下标值
    int childRootIdx = find(child);
    if (parentRootIdx == childRootIdx) return;
    if (count[parentRootIdx] >= count[childRootIdx]) {
        items[childRootIdx] = items[parentRootIdx];
        count[parentRootIdx] += count[childRootIdx];
    } else {
        items[parentRootIdx] = items[childRootIdx];
        count[childRootIdx] += count[parentRootIdx];
    }
}

目标:union(1, 8) 将8的根节点合并到1的根节点 & 基于节点的 count 值合并

  • find 循环找到置顶节点的最终根节点,例如;8 → 6、6 → 6,那么说明8的根节点是6,因为6自己指向自己了,它就是根节点。
  • union 在进行元素的根节点合并时,会判断哪个根下的元素少,用少的元素合并到多的元素下。因为这样可以减少多的元素因为处于更低位置所带来的索引耗时。树越深,子叶节点越多,越耗时。

4. 排序合并 - union(8, 1)

@Override
public int find(int i) {
    if (i < 0 || i >= items.length)
        throw new IllegalArgumentException("Index out of range.");
    // 找到元素的根节点,当i == item[i],就是自己指向自己,这个节点就是根节点
    while (i != items[i]) {
        i = items[i];
    }
    return i;
}

@Override
public void union(int parent, int child) {
    // 父亲节点的根节点下标值
    int parentRootIdx = find(parent);
    // 孩子节点的根节点下标值
    int childRootIdx = find(child);
    if (parentRootIdx == childRootIdx)
        return;
    if (rank[parentRootIdx] > rank[childRootIdx]) {
        items[childRootIdx] = items[parentRootIdx];
    } else if (rank[parentRootIdx] < rank[childRootIdx]) {
        items[parentRootIdx] = items[childRootIdx];
    } else {
        items[childRootIdx] = items[parentRootIdx];
        rank[parentRootIdx]++;
    }
}

目标:union(8, 1) 将1的根节点合并到8的根节点(其实效果和union(1,8)是一样的,之所以用union(8, 1)主要体现基于 rank 排序后的合并)& 基于节点的 rank 值合并

  • find 循环找到置顶节点的最终根节点,例如;8 → 6、6 → 6,那么说明8的根节点是6,因为6自己指向自己了,它就是根节点。
  • union 在进行元素的根节点合并时,会判断哪个根的排序小,用少的元素合并到大的根元素下。因为这样可以减少树深大的元素因为处于更低位置所带来的索引耗时。树越深,子叶节点越多,越耗时。
  • 那么此时基于 count、rank 都可以进行优化,不过优化过程中 1→0、0→2 还有2个树高,也可以优化。这就是压缩路径的作用

5. 压缩路径 - union(8, 1)

@Override
public int find(int i) {
    if (i < 0 || i >= items.length)
        throw new IllegalArgumentException("Index out of range.");
    while (i != items[i]) {
        // 路径压缩
        items[i] = items[items[i]];
        i = items[i];
    }
    return i;
}

@Override
public void union(int parent, int child) {
    // 父亲节点的根节点下标值
    int parentRootIdx = find(parent);
    // 孩子节点的根节点下标值
    int childRootIdx = find(child);
    if (parentRootIdx == childRootIdx)
        return;
    if (rank[parentRootIdx] > rank[childRootIdx]) {
        items[childRootIdx] = items[parentRootIdx];
    } else if (rank[parentRootIdx] < rank[childRootIdx]) {
        items[parentRootIdx] = items[childRootIdx];
    } else {
        items[childRootIdx] = items[parentRootIdx];
        rank[parentRootIdx]++;
    }
}

目标:union(8, 1) 在rank合并下,压缩路径长度。

  • 这里的 union 方法与4. 排序合并相比并没有变化,变化的地方主要在 find 过程中压缩路径。
  • find 基于查找根元素时,对当前元素值对应的父节点值,替换给当前元素。减少一级路径,做到压缩路径的目的。

四、并查集实现测试

单元测试

@Test
public void test_04() {
    IDisjointSet disjointSet = new DisjointSet04(9);
    System.out.println(disjointSet);
    System.out.println("\n合并元素:\n");
    disjointSet.union(0, 1);
    disjointSet.union(2, 3);
    disjointSet.union(2, 1);
    disjointSet.union(6, 4);
    disjointSet.union(6, 5);
    disjointSet.union(6, 7);
    disjointSet.union(6, 8);
    
    System.out.println(disjointSet);
    disjointSet.union(8, 1);
    System.out.println(disjointSet);
}
  • 关于并查集的测试共有6个案例,文中测试举例测试第4个,基于 Rank 优化合并。

测试结果

坐标 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 
-----------------------------------------
排序 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 
-----------------------------------------
指向 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 


合并元素:

坐标 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 
-----------------------------------------
排序 | 2 | 1 | 3 | 1 | 1 | 1 | 2 | 1 | 1 | 
-----------------------------------------
指向 | 2 | 0 | 2 | 2 | 6 | 6 | 6 | 6 | 6 | 

坐标 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 
-----------------------------------------
排序 | 2 | 1 | 3 | 1 | 1 | 1 | 2 | 1 | 1 | 
-----------------------------------------
指向 | 2 | 0 | 2 | 2 | 6 | 6 | 2 | 6 | 6 | 
  • 经过测试对比图例和控制台输出结果可以看到,(4、5、6、7)→6,6→2,1→0,(0、3)→2,这也是最终树的体现结果。
  • 其他案例源码读者可以测试验证调试,这也可以更好的学习掌握。

五、常见面试题

  • 并查集叙述?
  • 并查集的使用场景?
  • 并查集怎么合并元素?
  • 并查集合并元素的优化策略?
  • 如何压缩路径?

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

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

相关文章

简易CAD程序:Qt多文档程序的一种实现

注&#xff1a;文中所列代码质量不高&#xff0c;但不影响演示我的思路 实现思路说明 实现DemoApplication 相当于MFC中CWinAppEx的派生类&#xff0c;暂时没加什么功能。 DemoApplication.h #pragma once#include <QtWidgets/QApplication>//相当于MFC中CWinAppEx的派生…

c语言:strcmp

strcmp函数是用于比较两个字符串的库函数&#xff0c;其功能是根据ASCII值逐一对两个字符串进行比较。 语法&#xff1a;strcmp(str1, str2) 返回值&#xff1a; 如果str1等于str2&#xff0c;则返回0。 如果str1小于str2&#xff0c;则返回负数&#xff08;具体值取决于C…

【数据分析面试】51. 读取大型csv文件

题目 假设你是一家科技公司的数据分析师。近期由于管理层变动&#xff0c;新的总经理上任&#xff0c;他想要了解公司过往的交易情况数据&#xff0c;并且这个任务由数据分析团队负责完成。历史交易数据下载导出完成后&#xff0c;团队发现Csv文件大小超过了5个G&#xff0c;使…

解决Wordpress中Cravatar头像无法访问问题

一、什么是Cravatar Gravatar是WordPress母公司Automattic推出的一个公共头像服务&#xff0c;也是WordPress默认的头像服务。但因为长城防火墙的存在&#xff0c;Gravatar在中国时不时就会被墙一下&#xff0c;比如本次从2021年2月一直到8月都是不可访问状态。 在以往的时候&…

OVS名词解释(随手记)

qbr&#xff1a;Linux网桥&#xff0c;为虚拟机提供安全组服务&#xff0c;负责安全隔离&#xff0c;有关安全组的实现。 veth-pair&#xff1a;一对虚拟端口设备 br-int&#xff1a;OVS的核心网桥之一。二三层流量均需经过该网桥&#xff0c;通过local vlan tag实现主机内部不…

Greco:使用ZKP来证明FHE参与方的RLWE密文格式正确

1. 引言 以太坊基金会Enrico Bottazzi 2024年论文Greco: Fast Zero-Knowledge Proofs for Valid FHE RLWE Ciphertexts Formation&#xff0c;开源代码见&#xff1a; https://github.com/privacy-scaling-explorations/greco&#xff08;Rust & Python&#xff09;【基于…

2024最新彩虹聚合DNS管理系统源码v1.3 全开源

简介&#xff1a; 2024最新彩虹聚合DNS管理系统源码v1.3 全开源 聚合DNS管理系统可以实现在一个网站内管理多个平台的域名解析&#xff0c;目前已支持的域名平台有&#xff1a;阿里云、腾讯云、华为云、西部数码、DNSLA、CloudFlare。 本系统支持多用户&#xff0c;每个用户可…

数据库|SQLServer数据库管理系统基本使用

哈喽&#xff0c;你好啊&#xff0c;我是雷工&#xff01; 此处学习是以SQL Server为例。 学习数据服务打开、服务器名称的集中写法、TCP/IP协议的打开和登录模式修改的四个步骤,以下为学习笔记。 数据库管理系统包括&#xff1a;客户端服务端&#xff08;运行在服务器上面的一…

Python编程-后端开发之Django5应用请求处理与模板基础

Python编程-后端开发之Django5应用请求处理与模板基础 最近写项目&#xff0c;刚好用到了Django&#xff0c;现在差不多闲下来&#xff0c;个人觉得单体项目来讲django确实舒服&#xff0c;故写此总结 模板语法了解即可&#xff0c;用到了再看&#xff0c;毕竟分离已经是主流操…

深度学习之基于Yolov3的行人重识别

欢迎大家点赞、收藏、关注、评论啦 &#xff0c;由于篇幅有限&#xff0c;只展示了部分核心代码。 文章目录 一项目简介 二、功能三、系统四. 总结 一项目简介 一、项目背景 行人重识别&#xff08;Person Re-Identification&#xff0c;简称ReID&#xff09;是计算机视觉领域…

AI大模型日报#0523:中国大模型价格战的真相、大模型「上车」、王小川首款 AI 应用

导读&#xff1a;AI大模型日报&#xff0c;爬虫LLM自动生成&#xff0c;一文览尽每日AI大模型要点资讯&#xff01;目前采用“文心一言”&#xff08;ERNIE 4.0&#xff09;、“零一万物”&#xff08;Yi-Large&#xff09;生成了今日要点以及每条资讯的摘要。欢迎阅读&#xf…

Vitis HLS 学习笔记--控制驱动TLP - Dataflow视图

目录 1. 简介 2. 功能特性 2.1 Dataflow Viewer 的功能 2.2 Dataflow 和 Pipeline 的区别 3. 具体演示 4. 总结 1. 简介 Dataflow视图&#xff0c;即数据流查看器。 DATAFLOW优化属于一种动态优化过程&#xff0c;其完整性依赖于与RTL协同仿真的完成。因此&#xff0c;…

指针,指针变量,引用,取地址符,malloce()函数使用,C中“—>” 和“ . ” 作用与区别

目录 一&#xff1a;指针,指针变量&#xff0c;引用&#xff0c;取地址符&#xff1a; 前提 &#xff1a; 1.“ * ” 的两种用途 2." & “的两种用途 2.1&#xff1a;引用 2.2&#xff1a;取地址 补充&#xff1a; 二 : malloc(),动态申请地址空间 1.原型定义…

提权方式及原理汇总

一、Linux提权 1、SUID提权 SUID&#xff08;设置用户ID&#xff09;是赋予文件的一种权限&#xff0c;它会出现在文件拥有者权限的执行位上&#xff0c;具有这种权限的文件会在其执行时&#xff0c;使调用者暂时获得该文件拥有者的权限。 为可执行文件添加suid权限的目的是简…

安卓CardView使用

目录 前言一、基础使用1.1 依赖导入1.2 CardView的常用属性1.3 CardView继承关系 二、关于Z轴的概念三、CardView效果3.1 圆角 CardView3.2 阴影 CardView3.3 设置卡片背景3.4 设置卡片背景&#xff08;内部颜色&#xff09;3.5 同时设置背景颜色 前言 CardView是Android支持库…

C#Csharp,SharpPcap网络抓包程序及源码(适合网络分析直接使用或源码二次开发)

目录 1.程序简介2.程序截图3.程序源码 1.程序简介 C#Csharp,SharpPcap网络抓包程序及源码&#xff08;适合网络分析直接使用或源码二次开发&#xff09; 2.程序截图 3.程序源码 https://download.csdn.net/download/xzzteach/89325817

BOM..

区别&#xff1a;

html5 笔记01

01 表单类型和属性 input的type属性 单行文本框: typetext 电子邮箱 : typeemail 地址路径 : type url 定义用于输入数字的字段: typenumber 手机号码: typetel 搜索框 : typesearch 定义颜色选择器 : typecolor 滑块控件 : typerange 定义日期 :typedate 定义输入时间的控件…

【OceanBase诊断调优】—— 直连普通租户时遇到报错:Tenant not in this server

本文介绍了直连 OceanBase 数据库中的普通租户时&#xff0c;出现报错&#xff1a;ERROR 5150 (HY000) : Tenant not in this server 的处理方法。 问题描述 在 n-n 或者 n-n-n (n>1) 的部署架构中&#xff0c;使用 2881 端口 直连 OceanBase 集群的普通租户&#xff0c;可…

首都师范大学聘请旅美经济学家向凌云为客座教授

2024年4月17日&#xff0c;首都师范大学客座教授聘任仪式在首都师范大学资源环境与旅游学院举行。首都师范大学资源环境与旅游学院院长吕拉昌主持了仪式&#xff0c;并为旅美经济学家向凌云教授颁发了聘书。 吕拉昌院长指出&#xff0c;要贯彻教育部产学研一体化战略&#xff0…
最新文章