数据结构与算法十 并查集

news2024/9/26 5:16:46

一 并查集

并查集是一种树型的数据结构 ,并查集可以高效地进行如下操作:

  • 查询元素p和元素q是否属于同一组
  • 合并元素p和元素q所在的组

在这里插入图片描述

1.1 并查集结构

并查集也是一种树型结构,但这棵树跟我们之前讲的二叉树、红黑树、B树等都不一样,这种树的要求比较简单:

  1. 每个元素都唯一的对应一个结点;
  2. 每一组数据中的多个元素都在同一颗树中;
  3. 一个组中的数据对应的树和另外一个组中的数据对应的树之间没有任何联系;
  4. 元素在树中并没有子父级关系的硬性要求;

在这里插入图片描述

1.2 并查集API设计

在这里插入图片描述

1.3 并查集的实现

1.3.1 UF(int N)构造方法实现

  1. 初始情况下,每个元素都在一个独立的分组中,所以,初始情况下,并查集中的数据默认分为N个组;
  2. 初始化数组eleAndGroup;
  3. 把eleAndGroup数组的索引看做是每个结点存储的元素,把eleAndGroup数组每个索引处的值看做是该结点所在的分组,那么初始化情况下,i索引处存储的值就是i

在这里插入图片描述

1.3.2 union(int p,int q)合并方法实现

  1. 如果p和q已经在同一个分组中,则无需合并
  2. 如果p和q不在同一个分组,则只需要将p元素所在组的所有的元素的组标识符修改为q元素所在组的标识符即可
  3. 分组数量-1

在这里插入图片描述

3.3.3 代码

//并查集代码
public class UF {
	//记录结点元素和该元素所在分组的标识
	private int[] eleAndGroup;
	//记录并查集中数据的分组个数
	private int count;
	//初始化并查集
	public UF(int N){
		//初始情况下,每个元素都在一个独立的分组中,所以,初始情况下,并查集中的数据默认分为N个组
		this.count=N;
		//初始化数组
		eleAndGroup = new int[N];
		//把eleAndGroup数组的索引看做是每个结点存储的元素,把eleAndGroup数组每个索引处的值看做是该
		结点所在的分组,那么初始化情况下,i索引处存储的值就是i
		for (int i = 0; i < N; i++) {
			eleAndGroup[i]=i;
		}
	}
	//获取当前并查集中的数据有多少个分组
	public int count(){
		return count;
	}
	//元素p所在分组的标识符
	public int find(int p){
		return eleAndGroup[p];
	}
	//判断并查集中元素p和元素q是否在同一分组中
	public boolean connected(int p,int q){
		return find(p)==find(q);
	}
	//把p元素所在分组和q元素所在分组合并
	public void union(int p,int q){
		//如果p和q已经在同一个分组中,则无需合并;
		if (connected(p,q)){
			return;
		}
		//如果p和q不在同一个分组,则只需要将p元素所在组的所有的元素的组标识符修改为q元素所在组的标识
		符即可
		int pGroup = find(p);
		int qGroup = find(q);
		for (int i = 0; i < eleAndGroup.length; i++) {
			if (eleAndGroup[i]==pGroup){
			eleAndGroup[i]=qGroup;
		}
	}
	//分组数量-1
	count--;
	}
}
//测试代码
public class Test {
	public static void main(String[] args) {
		Scanner sc = new Scanner(System.in);
		System.out.println("请录入并查集中元素的个数:");
		int N = sc.nextInt();
		UF uf = new UF(N);
		while(true){
		System.out.println("请录入您要合并的第一个点:");
		int p = sc.nextInt();
		System.out.println("请录入您要合并的第二个点:");
		int q = sc.nextInt();
		//判断p和q是否在同一个组
		if (uf.connected(p,q)){
			System.out.println("结点:"+p+"结点"+q+"已经在同一个组");
			continue;
		}
		uf.union(p,q);
		System.out.println("总共还有"+uf.count()+"个分组");
		}
	}
}

1.3.4 并查集应用举例

如果我们并查集存储的每一个整数表示的是一个大型计算机网络中的计算机,则我们就可以通过connected(int p,int q)来检测,该网络中的某两台计算机之间是否连通?如果连通,则他们之间可以通信,如果不连通,则不能通信,此时我们又可以调用union(int p,int q)使得p和q之间连通,这样两台计算机之间就可以通信了。

一般像计算机这样网络型的数据,我们要求网络中的每两个数据之间都是相连通的,也就是说,我们需要调用很多次union方法,使得网络中所有数据相连,其实我们很容易可以得出,如果要让网络中的数据都相连,则我们至少要调用N-1次union方法才可以,但由于我们的union方法中使用for循环遍历了所有的元素,所以很明显,我们之前实现的合并算法的时间复杂度是O(N^2),如果要解决大规模问题,它是不合适的,所以我们需要对算法进行优化。

1.3.5 UF_Tree算法优化

为了提升union算法的性能,我们需要重新设计find方法和union方法的实现,此时我们先需要对我们的之前数据结构中的eleAndGourp数组的含义进行重新设定:

  1. 我们仍然让eleAndGroup数组的索引作为某个结点的元素;
  2. eleAndGroup[i]的值不再是当前结点所在的分组标识,而是该结点的父结点;

在这里插入图片描述

1.3.5.1 UF_Tree API设计

在这里插入图片描述

1.3.5.2 find(int p)查询方法实现

  1. 判断当前元素p的父结点eleAndGroup[p]是不是自己,如果是自己则证明已经是根结点了;
  2. 如果当前元素p的父结点不是自己,则让p=eleAndGroup[p],继续找父结点的父结点,直到找到根结点为止;

在这里插入图片描述

1.3.5.3 union(int p,int q)合并方法实现

  1. 找到p元素所在树的根结点
  2. 找到q元素所在树的根结点
  3. 如果p和q已经在同一个树中,则无需合并;
  4. 如果p和q不在同一个分组,则只需要将p元素所在树根结点的父结点设置为q元素的根结点即可;
  5. 分组数量-1

在这里插入图片描述

1.3.5.4 代码

package cn.itcast;
public class UF_Tree {
	//记录结点元素和该元素所的父结点
	private int[] eleAndGroup;
	//记录并查集中数据的分组个数
	private int count;
	//初始化并查集
	public UF_Tree(int N){
		//初始情况下,每个元素都在一个独立的分组中,所以,初始情况下,并查集中的数据默认分为N个组
		this.count=N;
		//初始化数组
		eleAndGroup = new int[N];
		//把eleAndGroup数组的索引看做是每个结点存储的元素,把eleAndGroup数组每个索引处的值看做是该
		结点的父结点,那么初始化情况下,i索引处存储的值就是i
		for (int i = 0; i < N; i++) {
			eleAndGroup[i]=i;
		}
	}
	//获取当前并查集中的数据有多少个分组
	public int count(){
		return count;
	}
	//元素p所在分组的标识符
	public int find(int p){
		while(true){
			//判断当前元素p的父结点eleAndGroup[p]是不是自己,如果是自己则证明已经是根结点了;
			if (p==eleAndGroup[p]){
			return p;
		}
		//如果当前元素p的父结点不是自己,则让p=eleAndGroup[p],继续找父结点的父结点,直到找到根
		结点为止;
		p=eleAndGroup[p];
		}
	}
	//判断并查集中元素p和元素q是否在同一分组中
	public boolean connected(int p,int q){
		return find(p)==find(q);
	}
	//把p元素所在分组和q元素所在分组合并
	public void union(int p,int q){
		//找到p元素所在树的根结点
		int pRoot = find(p);
		//找到q元素所在树的根结点
		int qRoot = find(q);
		//如果p和q已经在同一个树中,则无需合并;
		if (pRoot==qRoot){
			return;
		}
		//如果p和q不在同一个分组,则只需要将p元素所在树根结点的父结点设置为q元素的根结点即可;
		eleAndGroup[pRoot]=qRoot;
		//分组数量-1
		count--;
	}
}

1.3.5.5 优化后的性能分析

我们优化后的算法union,如果要把并查集中所有的数据连通,仍然至少要调用N-1次union方法,但是,我们发现union方法中已经没有了for循环,所以union算法的时间复杂度由O(N^2)变为了O(N)。

但是这个算法仍然有问题,因为我们之前不仅修改了union算法,还修改了find算法。我们修改前的find算法的时间复杂度在任何情况下都为O(1),但修改后的find算法在最坏情况下是O(N):

在这里插入图片描述
在union方法中调用了find方法,所以在最坏情况下union算法的时间复杂度仍然为O(N^2)。

1.3.6 路径压缩

UF_Tree中最坏情况下union算法的时间复杂度为O(N^2),其最主要的问题在于最坏情况下,树的深度和数组的大小一样,如果我们能够通过一些算法让合并时,生成的树的深度尽可能的小,就可以优化find方法。
之前我们在union算法中,合并树的时候将任意的一棵树连接到了另外一棵树,这种合并方法是比较暴力的,如果我们把并查集中每一棵树的大小记录下来,然后在每次合并树的时候,把较小的树连接到较大的树上,就可以减小树的深度。

在这里插入图片描述
只要我们保证每次合并,都能把小树合并到大树上,就能够压缩合并后新树的路径,这样就能提高find方法的效率。为了完成这个需求,我们需要另外一个数组来记录存储每个根结点对应的树中元素的个数,并且需要一些代码调整数组中的值。

1.3.6.1 UF_Tree_Weighted API设计

在这里插入图片描述

1.3.6.2 代码

public class UF_Tree_Weighted {
	//记录结点元素和该元素所的父结点
	private int[] eleAndGroup;
	//存储每个根结点对应的树中元素的个数
	private int[] sz;
	//记录并查集中数据的分组个数
	private int count;
	//初始化并查集
	public UF_Tree_Weighted(int N){
		//初始情况下,每个元素都在一个独立的分组中,所以,初始情况下,并查集中的数据默认分为N个组
		this.count=N;
		//初始化数组
		eleAndGroup = new int[N];
		sz = new int[N];
		//把eleAndGroup数组的索引看做是每个结点存储的元素,把eleAndGroup数组每个索引处的值看做是该结点的父结点,那么初始化情况下,i索引处存储的值就是i
		for (int i = 0; i < N; i++) {
			eleAndGroup[i]=i;
		}
		//把sz数组中所有的元素初始化为1,默认情况下,每个结点都是一个独立的树,每个树中只有一个元素
		for (int i = 0; i < sz.length; i++) {
			sz[i]=1;
		}
	}
	//获取当前并查集中的数据有多少个分组
	public int count(){
		return count;
	}
	//元素p所在分组的标识符
	public int find(int p){
		while(true){
			//判断当前元素p的父结点eleAndGroup[p]是不是自己,如果是自己则证明已经是根结点了;
			if (p==eleAndGroup[p]){
				return p;
			}
			//如果当前元素p的父结点不是自己,则让p=eleAndGroup[p],继续找父结点的父结点,直到找到根
			结点为止;
			p=eleAndGroup[p];
		}
	}
	//判断并查集中元素p和元素q是否在同一分组中
	public boolean connected(int p,int q){
		return find(p)==find(q);
	}
	//把p元素所在分组和q元素所在分组合并
	public void union(int p,int q){
		//找到p元素所在树的根结点
		int pRoot = find(p);
		//找到q元素所在树的根结点
		int qRoot = find(q);
		//如果p和q已经在同一个树中,则无需合并;
		if (pRoot==qRoot){
			return;
		}
		//如果p和q不在同一个分组,比较p所在树的元素个数和q所在树的元素个数,把较小的树合并到较大的树上
		if (sz[pRoot]<sz[qRoot]){
			eleAndGroup[pRoot] = qRoot;
			//重新调整较大树的元素个数
			sz[qRoot]+=sz[pRoot];
		}else{
			eleAndGroup[qRoot]=pRoot;
			sz[pRoot]+=sz[qRoot];
		}
		//分组数量-1
		count--;
	}
}

1.3.7 案例-畅通工程

某省调查城镇交通状况,得到现有城镇道路统计表,表中列出了每条道路直接连通的城镇。省政府“畅通工程”的目标是使全省任何两个城镇间都可以实现交通(但不一定有直接的道路相连,只要互相间接通过道路可达即可)。问最少还需要建设多少条道路?

城镇道路统计表,下面是对数据的解释:
在这里插入图片描述
总共有20个城市,目前已经修改好了7条道路,问还需要修建多少条道路,才能让这20个城市之间全部相通?

解题思路:

  1. 创建一个并查集UF_Tree_Weighted(20);
  2. 分别调用union(0,1),union(6,9),union(3,8),union(5,11),union(2,12),union(6,10),union(4,8),表示已经修建好的道路把对应的城市连接起来;
  3. 如果城市全部连接起来,那么并查集中剩余的分组数目为1,所有的城市都在一个树中,所以,只需要获取当前并查集中剩余的数目,减去1,就是还需要修建的道路数目;

代码:

public class Traffic_Project {
	public static void main(String[] args)throws Exception {
	//创建输入流
	BufferedReader reader = new BufferedReader(new
	InputStreamReader(Traffic_Project.class.getClassLoader().getResourceAsStream("traffic_projec
	t.txt")));
	//读取城市数目,初始化并查集
	int number = Integer.parseInt(reader.readLine());
	UF_Tree_Weighted uf = new UF_Tree_Weighted(number);
	//读取已经修建好的道路数目
	int roadNumber = Integer.parseInt(reader.readLine());
	//循环读取已经修建好的道路,并调用union方法
	for (int i = 0; i < roadNumber; i++) {
		String line = reader.readLine();
		int p = Integer.parseInt(line.split(" ")[0]);
		int q = Integer.parseInt(line.split(" ")[1]);
		uf.union(p,q);
	}
	//获取剩余的分组数量
	int groupNumber = uf.count();
	//计算出还需要修建的道路
	System.out.println("还需要修建"+(groupNumber-1)+"道路,城市才能相通");
	}
}

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

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

相关文章

【Stable Diffusion】基本概念之hypernetwork

1.基本概念 hypernetwork&#xff0c;中文名为超网络&#xff0c;是一种神经网络架构,它允许动态生成神经网络的参数(权重)。简而言之,hypernetwork可以生成其他神经网络。 在Stable Diffusion中,hypernetwork被用于动态生成分类器的参数&#xff0c;为Stable Diffusion模型添加…

牛客刷SQL题Day5

SQL69 返回产品并且按照价格排序 select prod_name , prod_price from Products where prod_price between 3 and 6 select prod_name , prod_price from Products where 6>prod_price and prod_price >3 踩坑1&#xff1a; between......and.......包括边界。 踩坑2&am…

ES6之箭头函数

文章目录 前言一、定义二、简化1.当函数参数只有一个时2.当函数体只有一条return语句时 三、注意1.箭头函数的this2.不能作为构造函数实例化对象3.不能使用argument变量 总结 前言 简单的讲&#xff0c;箭头函数是将原function关键字和函数名删掉的一种简写函数形式。 一、定义…

二维体光子晶体的平面波展开法代码

%书上的代码&#xff0c;和FEM符合的更好 %在这个代码里试着把单位原胞的相对介电常数分布画出来 %这个代码的单位原胞的中心就是(0,0)点&#xff0c;也就是坐标原点 %The program for the computation of the PhC photonic %band structure for 2D PhC. %Parameters of the st…

Jvm --java虚拟机(上)

为什么学习jvm 如果你这辈子只甘心做一个平庸的Java码农&#xff0c;那么你可以利用阅读本文的时间去学习其他新的技术知识&#xff0c;但是如果你想成为一个更更更更优秀的中高级程序员&#xff01;那么请继续阅读本文&#xff0c;希望这篇文章会对你有所帮助&#xff0c;那么…

国考省考结构化面试:综合分析题,名言哲理(警句观点启示)、漫画反驳题等

国考省考结构化面试&#xff1a;综合分析题&#xff0c;名言哲理&#xff08;警句观点启示&#xff09;、漫画反驳题等 2022找工作是学历、能力和运气的超强结合体! 公务员特招重点就是专业技能&#xff0c;附带行测和申论&#xff0c;而常规国考省考最重要的还是申论和行测&a…

从面向过程到面向对象

目录 1、抽象 2、UML类图 3、类定义 4、类成员函数 &#xff08;1&#xff09;构造函数&#xff08;constructor&#xff09; &#xff08;2&#xff09;析构函数&#xff08;destructor&#xff09; 5、对象实现 6、封装 7、getter、setter方法 为什么要从面向过程转…

C++实现ini配置文件解析——API设计

什么是配置文件 INI文件&#xff08;Initialization File&#xff09;是一种文本文件格式&#xff0c;通常用于存储配置数据。INI文件最初由Microsoft在Windows系统中引入&#xff0c;用于存储应用程序的配置信息。 INI文件的结构相对简单&#xff0c;由一系列的节&#xff0…

国外15家值得关注的AI创业公司

文 | 小戏、iven 星星之火&#xff0c;可以燎原。 在大模型横空出世的这个疯狂的春天&#xff0c;一场关于 AI 产品的革命也正在席卷全球。这边是大公司一个接一个模型搞军备竞赛&#xff0c;那边是各路豪强纷纷下场创业招兵买马。那么&#xff0c;除了咱们耳熟能详的 OpenAI 以…

数字化转型导师坚鹏:企业数字化营销

企业数字化营销 ————助力零售业务向批量化开发转变&#xff0c;对公业务向智慧化转变 课程背景&#xff1a; 很多企业存在以下问题&#xff1a; 不清楚数字化营销对企业发展有什么影响&#xff1f; 不知道如何提升企业数字化营销能力&#xff1f; 不知道企业如何开…

面试官:一千万的数据,你是怎么查询的

面试官&#xff1a;一千万的数据&#xff0c;你是怎么查询的&#xff1f; 前言 面试官&#xff1a;来说说&#xff0c;一千万的数据&#xff0c;你是怎么查询的&#xff1f;B哥&#xff1a;直接分页查询&#xff0c;使用limit分页。面试官&#xff1a;有实操过吗&#xff1f;B…

word@通配符@高级搜索查找@替换@中英文标点符号替换

文章目录 高级搜索通配符批量选中引用序号上标调整搜索替换作用范围设置&#x1f388;通过样式选择作用区域通过鼠标选择作用区域 高级替换操作顺序 标点符号替换&#x1f388;将英文逗号替换为中文逗号使用普通查找和替换&#xff1a;使用通配符替换 将英文句点替换为中文句号…

【Stable Diffusion】ControlNet基本教程(二)

接上篇【Stable Diffusion】ControlNet基本教程&#xff08;一&#xff09;&#xff0c;本篇介绍两个ControlNet常见的基本用法&#xff0c;更多用法欢迎关注博主&#xff0c;博主还会更新更多有趣的内容。 3.ControlNet基本用法 3.1漫画线稿上色 &#xff08;1&#xff09;上传…

Mysql索引(3):索引分类

1 索引分类 在MySQL数据库&#xff0c;将索引的具体类型主要分为以下几类&#xff1a;主键索引、唯一索引、常规索引、全文索引。 分类含义特点关键字主键索引针对于表中主键创建的索引 默认自动创建, 只能有一个 PRIMARY 唯一索引 避免同一个表中某数据列中的值重复可以有多…

Graph Embeddings—随机游走基本概念

Random Walk Approaches for Node Embeddings 一、随机游走基本概念 想象一个醉汉在图中随机的行走&#xff0c;其中走过的节点路径就是一个随机游走序列。 随机行走可以采取不同的策略&#xff0c;如行走的方向、每次行走的长度等。 二、图机器学习与NLP的关系 从图与NLP的…

【计算机网络】总结篇

【C语言部分】总结篇 【操作系统】总结篇 【数据库&#xff08;SQL&#xff09;】总结篇 本文目录 1. 简述网络七层参考模型及每一层的作用2. 简述静态路由和动态路由3. 说说有哪些路由协议&#xff0c;都是如何更新的4. 简述域名解析过程&#xff0c;本机如何干预域名解析5. 简…

智能算法系列之粒子群优化算法

本博客封面由ChatGPT DALLE 2共同创作而成。 文章目录 前言1. 算法思想2. 细节梳理2.1 超参数的选择2.2 一些trick 3. 算法实现3.1 问题场景3.2 python实现 代码仓库&#xff1a;IALib[GitHub] 前言 本篇是智能算法(Python复现)专栏的第三篇文章&#xff0c;主要介绍粒子群优化…

2023年的深度学习入门指南(9) - SIMD和通用GPU编程

2023年的深度学习入门指南(9) - SIMD和通用GPU编程 深度学习从一开始就跟GPU有不解之缘&#xff0c;因为算力是深度学习不可或缺的一部分。 时至今日&#xff0c;虽然多任务编程早已经深入人心&#xff0c;但是很多同学还没有接触过CPU上的SIMD指令&#xff0c;更不用说GPGPU…

【Segment Anything Model】论文+代码实战调用SAM模型预训练权重+相关论文

上篇文章已经全局初步介绍了SAM和其功能&#xff0c;本篇作为进阶使用。 文章目录 0.前言1.SAM原论文 1️⃣名词&#xff1a;提示分割&#xff0c;分割一切模型&#xff0c;数据标注&#xff0c;零样本&#xff0c;分割一切模型的数据集 2️⃣Introduction 3️⃣Task: prompta…

【五一创作】系统集成项目管理工程师-【11 人力资源】

持续更新。。。。。。。。。。。。。。。 【第十一章】人力资源 3分11.1 项目人力资源管理的定义及有关概念11.1.1 项日人力资源管理及其过程的定义2. 人力资源管理过程【掌握】11.1.2 人力资源管理相关概念11.2 编制项目人力资源管理计划11.2.1制定人力资源管理计划的技术和工…