并查集去解决按公因数计算最大组件大小

首先看看什么叫并查集。

并查集
并查集(Union-find Sets)是一种非常精巧而实用的数据结构,它主要用于处理一些不相交集合的合并问题。一些常见的用途有求连通子图、求最小生成树的 Kruskal 算法和求最近公共祖先(Least Common Ancestors, LCA)等。

使用并查集时,首先会存在一组不相交的动态集合 S={S1,S2,⋯,Sk},一般都会使用一个整数表示集合中的一个元素。

每个集合可能包含一个或多个元素,并选出集合中的某个元素作为代表。每个集合中具体包含了哪些元素是不关心的,具体选择哪个元素作为代表一般也是不关心的。我们关心的是,对于给定的元素,可以很快的找到这个元素所在的集合(的代表),以及合并两个元素所在的集合,而且这些操作的时间复杂度都是常数级的。

并查集的基本操作有三个:

makeSet(s):建立一个新的并查集,其中包含 s 个单元素集合。
unionSet(x, y):把元素 x 和元素 y 所在的集合合并,要求 x 和 y 所在的集合不相交,如果相交则不合并。
find(x):找到元素 x 所在的集合的代表,该操作也可以用于判断两个元素是否位于同一个集合,只要将它们各自的代表比较一下就可以了。
并查集的实现原理也比较简单,就是使用树来表示集合,树的每个节点就表示集合中的一个元素,树根对应的元素就是该集合的代表,如图 1 所示。

图 1 并查集的树表示
在这里插入图片描述
图中有两棵树,分别对应两个集合,其中第一个集合为 {a,b,c,d},代表元素是 a;第二个集合为 {e,f,g},代表元素是 e。

树的节点表示集合中的元素,指针表示指向父节点的指针,根节点的指针指向自己,表示其没有父节点。沿着每个节点的父节点不断向上查找,最终就可以找到该树的根节点,即该集合的代表元素。

现在,应该可以很容易的写出 makeSet 和 find 的代码了,假设使用一个足够长的数组来存储树节点(很类似之前讲到的静态链表),那么 makeSet 要做的就是构造出如图 2 的森林,其中每个元素都是一个单元素集合,即父节点是其自身:

在这里插入图片描述

图 2 构造并查集初始化

Find版本

public static class UnionFind1{
		//保存自己属于哪个集合
		private int[] array;
		//构造方法
		public UnionFind1(int size) {
			// TODO Auto-generated constructor stub
			array = new int[size];
			for(int i=0;i<array.length;i++) array[i]=i;
		}
		//返回数组大小
		public int size() {
			return array.length;
		}
		//返回p是属于哪个集合
		public int find(int p) {
			return array[p];
		}
		//判断两个元素是不是属于同一集合
		public boolean isConnected(int a, int b) {
			return find(a) == find(b); 
		}
		//合并两个集合,比如,下面的循环就是将和p是一个集合的合并到和q是一个集合的去了
		public void unionElements(int p, int q) {
			int pID = find(p);
			int qID = find(1);
			//他们两个本来就是相连的
			if(qID == pID) return;
			for(int i=0;i<array.length;i++) {
				if(array[i]==pID ) array[i] = qID; 
			}			
		}
	}

Union版本

public static class UnionFind2 {
		//保存根结点元素
		private int[] parents;
		//构造方法,初始化全是指向自己
		public UnionFind2(int size) {
			// TODO Auto-generated constructor stub
			parents = new int[size];
			for(int i=0;i<parents.length;i++) parents[i]=i;
		}
		//返回数组长度
		public int size() {
			return parents.length;
		}
		//判断两个元素是否在同一集合
		public boolean isConnected(int a, int b) {
			return find(a)== find(b); 
		}
		//向上找到根结点,形象起来就是一个树
		public int find(int e) {
			while(e!=parents[e]) e = parents[e];
			return e;
		}
		//合并两个
		public void unionElements(int p, int q) {
			int pRoot = find(p);
			int qRoot = find(q);
			//本身就是一个根结点,说明在同一个集合
			if(qRoot == pRoot) return;
			parents[pRoot] = qRoot;
		}
	}

测试一下Find版本和Union版本

public static double testfind(UnionFind1 UF, int m) {
		//设置开始时间
		long startTime = System.nanoTime();
		Random random = new Random();
		//合并集合
		for(int i=0;i<m;i++) {
			int p = random.nextInt(UF.size());
			int q = random.nextInt(UF.size());
			UF.unionElements(p, q);
		}
		//判断是否同一集合
		for(int i=0;i<m;i++) {
			int p = random.nextInt(UF.size());
			int q = random.nextInt(UF.size());
			UF.isConnected(p, q);
			
		}
		//结束时间
		long endtime = System.nanoTime();
		//返回时差
		return (endtime-startTime)/1000000000.0;
		
			
	}
	public static double testunion(UnionFind2 UF, int m) {
		long startTime = System.nanoTime();
		Random random = new Random();
				
		for(int i=0;i<m;i++) {
			int p = random.nextInt(UF.size());
			int q = random.nextInt(UF.size());
			UF.unionElements(p, q);
		}
		for(int i=0;i<m;i++) {
			int p = random.nextInt(UF.size());
			int q = random.nextInt(UF.size());
			UF.isConnected(p, q);
			
		}
		long endtime = System.nanoTime();
		return (endtime-startTime)/1000000000.0;
		
			
	}
main函数调用上面的test
public static void main(String[] args) {
		int size = 100000;//元素个数
		int m = 10000;//操作次数(合并或者比较是否同一集合)
		System.out.println(testfind(new UnionFind1(size), m));
		System.out.println(testunion(new UnionFind2(size), m));
	}
测试1
size=100000,m=10000

在这里插入图片描述

size=100000 , m=100000

在这里插入图片描述

由上面测试结果可以看出,操作次数增加哀乐,我们的Union版本明显变慢了。因为元素个数太多,我们Union版本的树的深度也就高了,要知道Union版本不管是合并还是查找是否同一集合时间复杂度都是O(h):h是树高度。而Find版本明显就是查找是否相等的时间复杂度是O(1),而合并复杂度是O(N)

对Union版本进行优化

基于size优化
我们上面的union版本其实有个问题:就是合并的时候没有注意高度的变化,看这样子:

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

可以看到,那么不断合并,树的高度越来越高,而基于size就是让节点个数少的往节点个数高的合并

在这里插入图片描述
这样多次合并还是只有高度2,不会增加那么快。

public static class UnionFind3 {
	private int[] parents;
	private int[] sz;//记录每棵树的节点个数
	public UnionFind3(int size) {
		// TODO Auto-generated constructor stub
		parents = new int[size];
		sz = new int[size];
		for(int i=0;i<parents.length;i++) {
			parents[i]=i;
			sz[i]=1;//每个根结点的一开始都只有一个节点
		}
		
	}
	public int size() {
		return parents.length;
	}
	public boolean isConnected(int a, int b) {
		return find(a)== find(b); 
	}
	//向上找到根结点
	public int find(int e) {
		while(e!=parents[e]) e = parents[e];
		return e;
	}
	
	public void unionElements(int p, int q) {
		int pRoot = find(p);
		int qRoot = find(q);
		//本身就是一个根结点,说明在同一个集合
		if(qRoot == pRoot) return;
		
		//节点数少的合并到节点数多的。
		if(sz[pRoot]<sz[qRoot]) {
			parents[pRoot] = qRoot;
			sz[qRoot] += sz[pRoot];
		}else {
			parents[qRoot]= pRoot;
			sz[pRoot] += sz[qRoot];
		}
	}
}
基于rank树的高度优化
上面基于size方法看似很优秀,但是也有缺点,它只注意size的数值,并没有真正从树的高度去下手,比如

在这里插入图片描述
但是基于rank就是这样:
在这里插入图片描述
那么我们的层级明显就要比基于size的要少一些,所以查找就方便很多了。

public static class UnionFind4 {
	private int[] parents;
	//rank[i]表示i为根的集合所表示的树的层数,而不是上面的基于元素个数大小
			private int[] rank;
	public UnionFind4(int size) {
		// TODO Auto-generated constructor stub
		parents = new int[size];
		 rank = new int[size];
		for(int i=0;i<parents.length;i++) {parents[i]=i; rank[i]=1;}
	}
	public int size() {
		return parents.length;
	}
	public boolean isConnected(int a, int b) {
		return find(a)== find(b); 
	}
	//向上找到根结点
	public int find(int e) {
		while(e!=parents[e]) e = parents[e];
		return e;
	}
	
	public void unionElements(int p, int q) {
		int pRoot = find(p);
		int qRoot = find(q);
		//本身就是一个根结点,说明在同一个集合
		if(qRoot == pRoot) return;
		//根据根结点树的高度来判断合并方向
		//层级矮的树往层级高的树合并并不需要维护rank
		if(rank[pRoot] <rank[qRoot]) {
			parents[pRoot] = qRoot;
		}else if(rank[pRoot]>rank[qRoot]) {
			parents[qRoot] = pRoot;
		}else {
			//这时候就会高度+1,比如最开始1,2两个元素合并,就是1<-2,高度为2,如果有1<-2,3<-4合并,就是1<-2,1<-3<-4高度就是3
			parents[pRoot] = qRoot;
			rank[qRoot] +=1;
		}
	}
}

测试一下上面四个版本

在这里插入图片描述

结果
可见基于size和基于rank优化结果都挺不错的。

在这里插入图片描述

可以看到,在十万级别的数据量,rank可能还略微差一点,但是当你到百万级别,就rank好一些了,而且我当前数据合并是顺序合并,并不是扰乱合并,所以实验结果会有些差别。

下面基于rank继续优化:路径压缩

直接修改find方法,就是如果当前节点的父亲节点,仍然不是根结点,那么就可能祖父节点可能是根结点,干脆直接让他直接指向祖父节点也不会影响对吧。

在这里插入图片描述

private int find(int p) {
    while (p != parents[p]) {
        parents[p] = parents[parents[p]];
        p = parents[p];
    }
    return p;
}
先将测试数据跳到百万级别

在这里插入图片描述

只测试基于size,基于rank和基于rank的路径压缩

结果这么叼,百万级别的基于路径压缩结果都这么好。
在这里插入图片描述

在这里插入图片描述

看个题目吧。

按公因数计算最大组件大小

给定一个由不同正整数的组成的非空数组 A,考虑下面的图:

  • 有 A.length 个节点,按从 A[0] 到 A[A.length - 1] 标记;
  • 只有当 A[i] 和 A[j] 共用一个大于 1 的公因数时,A[i] 和 A[j] 之间才有一条边。
    返回图中最大连通组件的大小。

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

考虑这个题目,明显就是一个并查集的问题
首先我们要知道,找公因数方法,很多人一对于这种东西,马上就要去找性能最好,比如辗转相除法,但是呢,经过我的实验,它的计算会超时,因为它要不断重复相除,有许多重复计算,这里就直接采用质因数分解
这里的话,我们先找到最大的数W,那么我们计算它的公因数最多循环到sqrt(W)即可。而且,因为数组中每个数肯定小于W,那么公因数遍历最多到sqrt(x)即可。
那么呢,我们提取了公因数之后,就将他们合并,然后找出最大的。
这样我们仔细看代码,时间复杂度是多少,首先要遍历数组个数A,然后遍历到他们的sqrt(x),这里为了取上限,最多是sqrt(w),所以时间复杂度就是O(A*sqrt(w)),空间复杂度就是O(A)
package ByteDance;
/*
 * 给定一个由不同正整数的组成的非空数组 A,考虑下面的图:

有 A.length 个节点,按从 A[0] 到 A[A.length - 1] 标记;
只有当 A[i] 和 A[j] 共用一个大于 1 的公因数时,A[i] 和 A[j] 之间才有一条边。
返回图中最大连通组件的大小。

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/largest-component-size-by-common-factor
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
 * */
public class p13 {
	public static void main(String[] args) {
		p13 paP13 = new p13();
		paP13.largestComponentSize(new int[] {4,6,15,35});
	}
	public int largestComponentSize(int[] A) {
		 int maxVal = 0;
	        for (int num : A) {
	            maxVal = Math.max(maxVal, num);
	        }

	        // 0 位置不使用,因此需要 + 1
	        UnionFind unionFind = new UnionFind(maxVal + 1);

	        for (int num : A) {
	            double upBound = Math.sqrt(num);
	            for (int i = 2; i <= upBound; i++) {
	                if (num % i == 0) {
	                    unionFind.unionElements(num, i);
	                    unionFind.unionElements(num, num / i);
	                }
	            }
	        }

	        // 将候选数组映射成代表元,统计代表元出现的次数,找出最大者
	        int[] cnt = new int[maxVal + 1];
	        int res = 0;
	        for (int num : A) {
	            int root = unionFind.find(num);
	            cnt[root]++;
	            res = Math.max(res, cnt[root]);
	        }
	        return res;
    }
	public int zhan(int a,int b) {
//		System.out.print(a+":"+b+":::");
		int r = -1;
		if(a<b) {r=a;a=b;b=r;} 
		while(r!=0) {
			r = a%b;
			a=b;
			b=r;
		}
//		System.out.println(a);
		return a;
	}
	public static class UnionFind {
		private int[] parents;
		//rank[i]表示i为根的集合所表示的树的层数,而不是上面的基于元素个数大小
				private int[] rank;
				private int[] size;
		public UnionFind(int size) {
			// TODO Auto-generated constructor stub
			parents = new int[size];
			 rank = new int[size];
			 this.size = new int[size];
			for(int i=0;i<parents.length;i++) {parents[i]=i; rank[i]=1; this.size[i]=1;}
		}
		public int size() {
			return parents.length;
		}
		public boolean isConnected(int a, int b) {
			return find(a)	== find(b); 
		}
		//向上找到根结点
		public int find(int e) {
			while(e!=parents[e]) 
				{
				parents[e] = parents[parents[e]];
				e = parents[e];
					
				}
			return e;
		}
		
		public int unionElements(int p, int q) {
			int pRoot = find(p);
			int qRoot = find(q);
			//本身就是一个根结点,说明在同一个集合
			if(qRoot == pRoot) return size[qRoot];
			//根据根结点树的高度来判断合并方向
			//层级矮的树往层级高的树合并并不需要维护rank
			if(rank[pRoot] <rank[qRoot]) {
				parents[pRoot] = qRoot;
				size[qRoot] += size[pRoot];
				return this.size[qRoot];
			}else if(rank[pRoot]>rank[qRoot]) {
				parents[qRoot] = pRoot;
				size[pRoot]+= size[qRoot];
				return this.size[pRoot];
			}else {
				//这时候就会高度+1,比如最开始1,2两个元素合并,就是1<-2,高度为2,如果有1<-2,3<-4合并,就是1<-2,1<-3<-4高度就是3
				parents[pRoot] = qRoot;
				size[qRoot] += size[pRoot];
				rank[qRoot] +=1;
				return this.size[qRoot];
			}
		}
	}
	
}

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小满锅lock

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值