并查集

1.定义

并查集,就是支持合并和查找操作的集合。实际中,都是用树结构(森林)来表示。

2.实现

最简单的实现方法就是使用数组,int father[n];。其中father[i]表示元素i的父亲结点,而父亲结点本身也是集合内的元素。有两种表示根节点的方式:
(1) father[i]=-1;
(2) father[i]=i;
以下算法均使用第一种方式。

3.基本操作

1.初始化

memset(father,-1,sizeof(father));

2.查找
查找操作就是查找元素所在集合,用根节点唯一标识。
(1)递推方式

int find(int x){
	while(father[x]!=-1){
		x=father[x]; //获得自己的父亲结点 
	}
	return x; //满足father[x]==-1的根节点 
} 

(2)递归方式

int find(int x){
	if(father[x]==-1) return x;
	else return find(father[x]);
} 

3.合并
合并指把两个集合合并成一个集合,把其中一个集合的根节点作为另一个集合根节点的子结点即可。

void Union(int a,int b){
	int faA=find(a);
	int faB=find(b);
	if(faA!=faB) father[faA]=faB; //faA!=faB表示A,B的集合的根节点不同
}

4.优化算法

上述算法是没有经过优化的原始而朴素的算法,在极端情况下效率极低。当并查集退化为一条链时效率极低。比如下面的情况下,总共有105个元素,那么要进行105次查询,每次查询都查询最后面的结点的根节点,时间复杂度为O(n2)
在这里插入图片描述

4.1 路径压缩

路径压缩是并查集最常用的一个优化之一,他可以优化查询的速度。我们发现,如果我们要询问一个元素所属哪个集合,我们只关心集合的根节点是谁,而并不关心树的形态。那么我们可以在每次执行往上找根节点的时候,把路径上所有的元素的父亲全部指向树根(也就是通过改变树结构,降低了树的高度,增快了我们的查询速度),这样的均摊复杂度为O(logN)

1.递归实现

int find(int x){
	if(father[x]==-1) return x;
	else return x=find(father[x]);	
}

2.递推实现
由于递归调用系统栈,比较耗时,且数据较大时会出现爆栈的现象,所以虽然递归实现更简单,但是非递归实现用得更多。

int find(int x){
	/*step1: 寻找根结点 */
	int root=x;
	while(father[root]!=-1){
		root=father[root];
	}
	/*step2: 将路径上所有结点的父结点改为根结点*/
	while(father[x]!=-1){
		int t=x; //x将被father[x]覆盖,t保存x的值 
		x=father[x];
		father[t]=root;
	}
	return root; 	
}

4.1 按秩归并

按秩合并是一种启发式合并,主要思想是合并的时候把小的树合并到大的树以减少工作量。
并查集的“秩”有两种定义的方法:

  1. 树的高度
  2. 树的节点数

在路径压缩之后一般采用第二种,因为第一种在路径压缩之后就已经失去意义了,按照第二种合并可以减少一定的路径压缩的工作量。(但其实也不会太多,所以一般来说路径压缩就够用了)

单独采用按秩合并的话平摊查询时间复杂度同样为O(logN)。如果我们把路径压缩和按秩合并合起来一起使用的话可以把查询复杂度下降到O(α(n)),其中α(n)为反阿克曼函数。阿克曼函数是一个增长极其迅速的函数,而相对的反阿克曼函数是一个增长极其缓慢的函数。对所有在实际问题中有意义的x,α(x)≤4,所以在算法时间复杂度分析等问题中,可以把α(x)看成常数。证明过程比较繁琐,大可不必去纠结,感兴趣可以看《算法导论》

Ackermann函数定义如下:
(1)若m=0,返回n+1。
(2)若m>0且n=0,返回Ackermann(m-1,1)。
(3)若m>0且n>0,返回Ackermann(m-1,Ackermann(m,n-1))。

按秩(结点数)归并代码:

void Union(int a, int b) {
	int fa = find(a), fb = find(b);
	if (fa != fb) {
		//father[fa]、father[fb]都是负数,father[fa] < father[fb]表示fa表示的集合的结点多于fb表示的集合
		if (father[fa] < father[fb]) { 
			father[fa] += father[fb]; //元素个数累加(用根节点的father[]绝对值表示集合元素个数)
			father[fb] = fa;
		}
		else {
			father[fb] += father[fa];
			father[fa] = fb;
		}
	}
}

对比

两种表示根节点的方式(1) father[i]=-1;(2) father[i]=i;有什么区别呢?
从我做题来看,第一种方式会更方便:
(1)归并之后更方便确定根节点。第一种方式在归并之后,满足father[i]<0就是根节点。而第二种方法路径压缩之后,集合中所有结点的father[i]都等于根节点,还需另设数组表示哪些结点是根节点。
(2)方便统计集合中结点个数

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
查集(Disjoint Set)是一种数据结构,用于解决集合的合并和查找问题。在Python中可以使用类来实现并查集。引用展示了一个简单的并查集类的代码实现,其中包括了初始化集合、查找集合、合并集合和判断两个元素是否在同一个集合中的方法。另外,引用和展示了对并查集代码的优化,包括路径压缩和按秩合并等技巧,以提高并查集的效率。 在Python中使用并查集可以解决一些实际问题,如求解岛屿个数、朋友圈等。通过将问题转化为集合的合并和查找操作,可以使用并查集来高效地解决这些问题。 所以,如果你需要在Python中实现并查集,可以参考以上的代码实现和优化方法。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* [python 数据结构与算法——并查集](https://blog.csdn.net/itnerd/article/details/103916115)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT0_1"}}] [.reference_item style="max-width: 50%"] - *2* *3* [并查集Python版](https://blog.csdn.net/XZ2585458279/article/details/127274576)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT0_1"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值