并查集及其实现和例题运用——2021.3.28

1.并查集的概念

并查集是用来管理元素分组情况的数据结构,是一种树型的数据结构,用于处理一些不相交集合(disjoint sets)的合并及查询问题。常常在使用中以森林来表示。

2.主要操作

  • 初始化

把每个点所在集合初始化为其自身。
通常来说,这个步骤在每次使用该数据结构时只需要执行一次,无论何种实现方式,时间复杂度均为O(N)。

  • 查找

查找元素所在的集合,即根节点。

  • 合并

将两个元素所在的集合合并为一个集合。
通常来说,合并之前,应先判断两个元素是否属于同一集合,这可用上面的“查找”操作实现。

3.并查集的实现

3.1Quick FIND

使用普通数组实现,FIND操作只需O(1)时间,这是因为数组可以在常数时间内找到任意元素的集合名。然而在执行UNION(p,q)时,需要扫描整个数组,并将p所在集合的所有元素移动到q所在集合中,这需要花费O(n)的时间。
代码如下:

package com.lanqiao.UF;

public interface UF {

	public int find(int i);
	public boolean isConnected(int p,int q);
	public void union(int p,int q);
	public int getSize();
}

package com.lanqiao.UF;

/**
 * 并查集用数组思路实现
 * @Description
 * @author mike
 * @version
 * @date 2021-3-28下午3:47:23
 *
 */
public class UnionFind1 implements UF{

	private int[] id;
	public UnionFind1(int size){
		id=new int[size];
		for (int i = 0; i < id.length; i++) {
			id[i]=i;//并查集的初始化,独立顶点
		}
	}
	
	//查看元素对应的集合编号(根节点)时间复杂度O(1),常数时间
	//FIND操作只需O(1)时间,这是因为数组可以在常数时间内找到任意元素的集合名
	public int find(int i){
		if (i<0||i>id.length) {
			throw new IllegalArgumentException("索引越界");
		}
		return id[i];
	}
	
	//查看元素p和q是否属于同一个集合
	public boolean isConnected(int p,int q){
		return id[p]==id[q];
	}
	
	//合并元素p,q所在集合
	//执行UNION(p,q)时,需要扫面整个数组,
	//并将p所在集合的所有元素移动到q所在集合中,这需要花费O(n)的时间。
	public void union(int p,int q){
		int pId=find(p);
		int qId=find(q);
		if (qId==pId) {
			return;
		}
		
		for (int i = 0; i < id.length; i++) {
			if (id[i]==pId) {
				id[i]=qId;
			}
		}
	}
	
	public int getSize(){
		return id.length;
	}
}

当数据很多时,用普通数组就比较费时间啦。尤其是union操作时。

3.2Quick UNION

并查集也可以使用树形结构实现。不过不是二叉树。每个元素对应一个节点,每个集合对应一棵树。理论上是孩子结点指向父结点的树结构,但实际上仍可以使用数组来实现,非根结点存储父结点的索引,根结点存储集合名。这样使得FIND操作退化为O(h)(h为树的高度),因为想要知道一个结点的集合名就要找到其根结点。而在进行UNION操作时不需要遍历整个数组,只需要找到两个元素的根结点,如果根结点不是同一个则让高度低的树的根结点指向高度高的树的根结点,这样UNION操作的时间复杂度优化为O(h)。
代码如下:

package com.lanqiao.UF;

/**
 * 并查集使用树形结构实现。
 * @Description
 * @author mike
 * @version
 * @date 2021-3-28下午4:19:46
 *
 */
public class UnionFind2 implements UF{

	private int[] parent;
	private int[] rank;
	
	public UnionFind2(int size){
		parent=new int[size];
		rank=new int[size];
		for (int i = 0; i < parent.length; i++) {
			parent[i]=i;//树结构的并查集初始化,每个节点的父节点指向自身
			rank[i]=1;//树的高度为1,一个树就是一个集合
		}
	}

	//查找元素p对应的集合编号(根节点),时间复杂度O(h)
	public int find(int i) {
		if (i<0||i>parent.length) {
			throw new IllegalArgumentException("索引越界");
		}
		while(i!=parent[i]){
			i=parent[i];//找它的父节点,一直找到根节点退出循环
		}
		
		return i;
	}

	@Override
	public boolean isConnected(int p, int q) {
		// TODO Auto-generated method stub
		return find(p)==find(q);
	}

	//合并元素p,q所属集合,时间复杂度为O(h)
	public void union(int p, int q) {
		int pRoot=find(p);
		int qRoot=find(q);
		if (pRoot==qRoot) {
			return;
		}
		if (rank[pRoot]<rank[qRoot]) {
			parent[pRoot]=qRoot;
		}else if (rank[pRoot]>rank[qRoot]) {
			parent[qRoot]=pRoot;
		}else{
			parent[qRoot]=pRoot;
			rank[pRoot]++;
		}
		
	}

	@Override
	public int getSize() {
		// TODO Auto-generated method stub
		return parent.length;
	}

}

3.3路径压缩优化Find操作

我们已经知道了前面的实现方法中FIND和UNION操作的时间复杂度均为O(h),所以我们的优化思路就是要尽可能的降低树的高度。路径压缩就是一个很好的优化方法,具体过程是:FIND操作遍历当前结点到根结点路径上的一系列结点,通过将这些结点的每个父结点指向根结点,可以想象最后的树高度相当低,可以使FIND操作更高效。
代码如下:

package com.lanqiao.UF;

/**
 * 路径压缩优化find操作
 * @Description
 * @author mike
 * @version
 * @date 2021-3-28下午4:20:29
 *
 */
public class UnionFind3 implements UF{
	
	private int[] parent;
	private int[] rank;
	
	public UnionFind3(int size){
		parent=new int[size];
		rank=new int[size];
		
		for (int i = 0; i < parent.length; i++) {
			parent[i]=i;
			rank[i]=1;
		}
	}

	@Override
	public int find(int i) {//查找i的根节点,查找的过程中,把每个节点的父节点指向根节点。
		if (i<0||i>parent.length) {
			throw new IllegalArgumentException("索引越界");
		}
//		while(i!=parent[i]){
//			//路径压缩
//			parent[i]=parent[parent[i]];//这一句并非把每个节点的父节点都指向父节点
//			i=parent[i];//找它的父节点,一直找到根节点退出循环
//		}
		
		if (parent[i]==i) {//递归出口,i的父节点为本身,i为根节点
			return i;
		}
		
		//路径压缩
		//将i到根节点路径上的所有点的父节点设为根节点。
		return parent[i]=find(parent[i]);//此代码相当于先找到根结点 rootx,然后parent[x]=rootx 
	}

	@Override
	public boolean isConnected(int p, int q) {
		// TODO Auto-generated method stub
		return find(p)==find(q);
	}

	@Override
	public void union(int p, int q) {
		int pRoot=find(p);
		int qRoot=find(q);
		if (pRoot==qRoot) {
			return;
		}
		if (rank[pRoot]<rank[qRoot]) {
			parent[pRoot]=qRoot;
		}else if (rank[pRoot]>rank[qRoot]) {
			parent[qRoot]=pRoot;
		}else{
			parent[qRoot]=pRoot;
			rank[pRoot]++;
		}
		
	}

	@Override
	public int getSize() {
		// TODO Auto-generated method stub
		return parent.length;
	}

}

package com.lanqiao.UF;

import java.util.Random;

public class Main {
	
	public static double testUF(UF uf,int m){
		int size=uf.getSize();
		Random random=new Random();
		
		long start=System.currentTimeMillis();
		
		for (int i = 0; i < m; i++) {
			int a=random.nextInt(size);
			int b=random.nextInt(size);
			uf.union(a, b);
		}
		
		for (int i = 0; i < m; i++) {
			int a=random.nextInt(size);
			int b=random.nextInt(size);
			uf.isConnected(a, b);
		}
		long end=System.currentTimeMillis();
		return end-start;
	}

	public static void main(String[] args) {
		int size=10000000;
		int m=10000000;
		
//		UnionFind1 uf1=new UnionFind1(size);
//		System.out.println("UnionFind1:"+testUF(uf1, m)+"ms");
		
		UnionFind2 uf2=new UnionFind2(size);
		System.out.println("UnionFind2:"+testUF(uf2, m)+"ms");
		
		UnionFind3 uf3=new UnionFind3(size);
		System.out.println("UnionFind3:"+testUF(uf3, m)+"ms");
	}

}

测试情况:
在这里插入图片描述
可以看到用普通数组实现的并查集性能远远差于用树结构实现的并查集。

历届试题 合根植物

问题描述
w星球的一个种植园,被分成 m * n 个小格子(东西方向m行,南北方向n列)。每个格子里种了一株合根植物。
这种植物有个特点,它的根可能会沿着南北或东西方向伸展,从而与另一个格子的植物合成为一体。
如果我们告诉你哪些小格子间出现了连根现象,你能说出这个园中一共有多少株合根植物吗?

输入格式
第一行,两个整数m,n,用空格分开,表示格子的行数、列数(1<m,n<1000)。
接下来一行,一个整数k,表示下面还有k行数据(0<k<100000)
接下来k行,第2+k行两个整数a,b,表示编号为a的小格子和编号为b的小格子合根了。

格子的编号一行一行,从上到下,从左到右编号。
比如:5 * 4 的小格子,编号:
1 2 3 4
5 6 7 8
9 10 11 12
13 14 15 16
17 18 19 20

样例输入
5 4
16
2 3
1 5
5 9
4 8
7 8
9 10
10 11
11 12
10 14
12 16
14 18
17 18
15 19
19 20
9 13
13 17

样例说明:其合根情况参考下图(注意:6也是一个连通子集)
在这里插入图片描述
样例输出
5
————————————————
版权声明:合根植物为CSDN博主「酱懵静」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/the_ZED/article/details/101766228

解析

这个题目的意思需要仔细读。给我的感觉就是又臭又长。
首先看题目中给出的一些关键词,合根植物,连根现象。这实际上就是在暗示整个图中点与点之间的关系——要么同属于某个点集合,要么各自属于其他的某个点集合。这样一来,便能把题目的任务转换为“统计有多少个点集”。采用并查集思想。
代码如下:

package com.lanqiao.mike;

import java.util.Scanner;

/**
 * 
 * @Description
 * @author mike
 * @version
 * @date 2021-3-28下午7:56:08
 *
 */
public class Class_合根 {

	static int MAX=1000000;//不到1000行1000列,图中点个数的最大值
	static int m,n;
	static int[] parent=new int[MAX];
	static int[] rank=new int[MAX];
	
	static void init(int p){//初始化并查集
		for (int i = 1; i <= p; i++) {
			parent[i]=i;//每个点自成一个集合,它们自身是该集合的代表元
			rank[i]=1;//每个点为一棵树,高度为1
		}
	}
	
	static int find(int i){//寻找i的根节点
		if (parent[i]==i) {
			return i;//此节点i的父节点等于自身,说明它是根节点。
		}
		//i不是根节点的条件下,先查找到根节点,然后将i节点到根节点路径上的点的父节点设为根节点
		return parent[i]=find(parent[i]);
	}
	
	static void union(int x,int y){//合并x,y节点所属的集合
		int rootx=find(x);
		int rooty=find(y);
		if (rootx!=rooty) {
			parent[rootx]=rooty;//最坏的情况会形成单分支树,导致find效率低
		}
//		//这个函数还可以优化,加一个rank[]数组,表征树的高度,高度小的树其父节点是高度大的树
//		if (rootx==rooty) {
//			return;
//		}
//		if (rank[rootx]<rank[rooty]) {
//			parent[rootx]=rooty;
//		}else if (rank[rootx]>rank[rooty]) {
//			parent[rooty]=rootx;
//		}else{
//			parent[rootx]=rooty;
//			rank[rooty]++;
//		}
	}
	
	public static void main(String[] args) {
		Scanner scanner=new Scanner(System.in);
		m=scanner.nextInt();
		n=scanner.nextInt();
		int k=scanner.nextInt();
		init(m*n);
		for (int i = 0; i < k; i++) {
			int x=scanner.nextInt();
			int y=scanner.nextInt();
			//每录入一对点,就将这两个点联合,直到录入完毕,就能将所有点所在的集合统计出
			//(表现在并查集中,就是每个点都找到了能代表当前所在集合的代表点)
			union(x, y);//编号为x的小格子和编号为y的小格子合根
		}
		
		//通过一层循环来遍历每个点,对于每个点,我们都标记其代表点的索引为1。
		int ans=0;//记录点集的数量,即索引1的个数
		int[] a=new int[m*n+1];
		for (int i = 1; i <= m*n; i++) {
			a[find(i)]=1;//把i的根节点的索引置为1
		}
		//统计数组a中有几个1
		for (int i = 1; i <= m*n; i++) {
			if (a[i]==1) {
				ans++;
			}
		}
		System.out.println(ans);

	}

}

测试用例:

5 4
16
2 3
1 5
5 9
4 8
7 8
9 10
10 11
11 12
10 14
12 16
14 18
17 18
15 19
19 20
9 13
13 17
5            //5个点集,即5个合根

历届试题 国王的烦恼

问题描述
C国由n个小岛组成,为了方便小岛之间联络,C国在小岛间建立了m座大桥,每座大桥连接两座小岛。两个小岛间可能存在多座桥连接。然而,由于海水冲刷,有一些大桥面临着不能使用的危险。

如果两个小岛间的所有大桥都不能使用,则这两座小岛就不能直接到达了。然而,只要这两座小岛的居民能通过其他的桥或者其他的小岛互相到达,他们就会安然无事。但是,如果前一天两个小岛之间还有方法可以到达,后一天却不能到达了,居民们就会一起抗议。

现在C国的国王已经知道了每座桥能使用的天数,超过这个天数就不能使用了。现在他想知道居民们会有多少天进行抗议。

输入格式
输入的第一行包含两个整数n, m,分别表示小岛的个数和桥的数量。
接下来m行,每行三个整数a, b, t,分别表示该座桥连接a号和b号两个小岛,能使用t天。小岛的编号从1开始递增。

输出格式
输出一个整数,表示居民们会抗议的天数。

样例输入
4 4
1 2 2
1 3 2
2 3 1
3 4 3

样例输出
2

样例说明
第一天后2和3之间的桥不能使用,不影响。
第二天后1和2之间,以及1和3之间的桥不能使用,居民们会抗议。
第三天后3和4之间的桥不能使用,居民们会抗议。

数据规模和约定
对于30%的数据,1<=n<=20,1<=m<=100;
对于50%的数据,1<=n<=500,1<=m<=10000;
对于100%的数据,1<=n<=10000,1<=m<=100000,1<=a, b<=n, 1<=t<=100000。
————————————————
版权声明:国王烦恼为CSDN博主「酱懵静」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/the_ZED/article/details/90407435

解析:

解析的详细过程请看CSDN博主「酱懵静」的原创文章,链接地址:https://blog.csdn.net/the_ZED/article/details/90407435解题思路讲的很详细。
下面附上我的代码,依据他的思路写的

package com.lanqiao.mike;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Scanner;

public class Class_国王 {

	static int N=10010;
	static int M=100010;
	public static class Bridge implements Comparable<Bridge>{//桥
		int x,y;//桥连接的两个小岛的编号
		int day;//桥的可用天数
		public Bridge(int x,int y,int day){
			this.x=x;
			this.y=y;
			this.day=day;
		}
		@Override
		public int compareTo(Bridge o) {
			// TODO Auto-generated method stub
//			return this.day-o.day;//升序
			return o.day-this.day;//降序
		}
	}
	
	static int[] pre=new int[N];//存储每个小岛的上级
	static List<Bridge> bridges=new ArrayList<>();
	
	static void init(int p){
		for (int i = 1; i <= p; i++) {
			pre[i]=i;
		}
	}
	
	static int find(int p){
		if (pre[p]==p) {
			return p;
		}
		return pre[p]=find(pre[p]);
	}
	
	static boolean union(int x,int y){
		int rootx=find(x);
		int rooty=find(y);
		if (rootx!=rooty) {
			pre[rootx]=rooty;
			return true;
		}
		return false;
	}
	
	public static void main(String[] args) {
		Scanner scanner=new Scanner(System.in);
		int n=scanner.nextInt();
		int m=scanner.nextInt();
		init(n);
		for (int i = 1; i <= m; i++) {
			int a=scanner.nextInt();
			int b=scanner.nextInt();
			int c=scanner.nextInt();
			bridges.add(new Bridge(a, b, c));
		}
		//对bridges降序排序
		Collections.sort(bridges);
		int ans=0;//记录抗议的天数
		//通过一个lastDay变量即可,这个变量的作用是记录前一次某个桥的使用天数,如果在循环中,
		//检测到当前桥的使用天数和lastDay不相等,并且将当前桥连接的两个小岛进行Unite操作后
		//其确实使得这两个岛的代表元发生了改变,就说明此时需要执行ans++,否则一律不执行。
		int lastDay=0;//
		for (int i = 0; i <m; i++) {
			//若为真,当前这两个岛未连通
			Bridge bridge=bridges.get(i);
			boolean flag=union(bridge.x, bridge.y);
			if (flag&&bridge.day!=lastDay) {//未连通,且此桥天数是第一次出现,那么增加抗议的天数
				ans++;
				lastDay=bridge.day;//为了避免有桥的使用天数是一样的,导致重复计数
			}
		}
		System.out.println(ans);
	}

}

测试用例:

4 4
1 2 2
1 3 2
2 3 1
3 4 3
2     
4 4
1 2 3
1 3 2 
2 3 5
3 4 1
3

总结:并查集实现起来其实并不复杂,关键在于要会题目中,加以运用。这两个题目意思说的都比较隐晦,需要多读几遍,总觉得会理解有偏差。依我个人的感觉这就是考察我们的语文素养~( ̄▽ ̄)"。

参考文献

有关并查集的内容参考文献如下:
————————————————
版权声明:本文为CSDN博主「无意呢」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qq_41900081/article/details/86713156
————————————————
版权声明:合根植物为CSDN博主「酱懵静」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/the_ZED/article/details/101766228
————————————————
版权声明:国王烦恼为CSDN博主「酱懵静」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/the_ZED/article/details/90407435
————————————————
版权声明:本文为CSDN博主「酱懵静」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。这个文献讲并查集很形象,大白话讲的。
原文链接:https://blog.csdn.net/the_ZED/article/details/105126583

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值