并查集-理论与基本代码实现

并查集的定义及运算

  在一些问题中,需要根据给出的各个元素之间的联系,将这些元素分成几个集合,每个集合中的元素直接或间接有联系,在这类问题中主要涉及的是对集合合并查找,因此将这种集合称之为并查集。

主要操作:

1、合并两个不相交集合
2、判断两个元素是否属于同一个集合
3、路径压缩
  假设有n个不同元素的集合s,这些元素被分成了不相交集合。最初假设每个元素自成一个集合。下面定义一个m次合并(union)和寻找的运算序列,每次执行合并指令后,两个不相交子集合并成一个子集。由观察可知,合并次数最多为n-1次。每个子集中,用一个特殊的元素作为集合的名字或代表。例如集合s={1,2,3,…,11}有4个子集,分别是{1,7,10,11}、{2,3,5,6}、{4,8}、{9},这些子集被标记为1,3,8,9。寻找运算返回一个包含特定元素的名字。例如运算find(11)返回1,即包含元素11的那个集合的名字1。
  在程序设计中,并查集可以用树型数据结构来记录,用树来表示每个集合,集合中的元素存贮在节点中,树中除根节点外的每个元素都指向父节p(x)的指针。根有一个空指针,用做集合的名字或集合的代表。这样就产生了一个森林,其中每棵树对应一个集合。

在这里插入图片描述

  假定元素是整数1,2,3,…,则森林可以方便地用数组a[1…n]来表示,a[j]是元素j的父节点,1 <= j <= n,空的父节点可以用0来表示,如上图所示对应的4个集合{1,7,10,11},{2,3,5,6},{4,8},{9}的4棵树,由于元素是整数,用数组表示更为优越。

运算的实现

设定义两个函数:
  find(x):寻找并返回包含元素x的集合的名字。
  union(x,y):包含元素x和y的两个集合用它们的并集替换。并集的名字或者是原来的包含元素x那个集合的名字,或者是原来包含元素y的那个集合的名字。
  对于任意假定元素x,用root(x)表示包含x的树的根。那么find(x)总是返回root(x),由于合并运算必须有两棵树的根作为它的参数,我们假定对于任意两个元素x和y,union(x,y)实际上表示union(root(x),root(y))。

1、按秩合并

  合并运算直接实现有一个明显的缺点,就是树的高度变得非常大,大到寻找的运算的时间需要o(n),在极端的情况下,树可以退化。让较小的树成为较大的树的子树。这里可以是深度、节点个数等启发函数来比较树的大小。
  举例说明:u(1,2),u(2,3),…u(n-1,n),再执行find(1),find(2)…find(n),其时间代价就为n+(n-1)+…+1=n^2。为了限制树的深度,采用秩合并措施,给每个节点存贮一个非负数作为节点的秩,记为rank,节点的秩就是它的深度。比如,执行合并运算时union(x,y)时,比较rank(x)和rank(y) :
   rank(x)
   rank(x)>rank(y),使x为y的父节点
   rank(x)=rank(y),使y为x的父节点,并rank(y)+1。

2、路径压缩

  为了进一步增强并查集寻找运算性能,路径压缩的措施被使用。我们在查找完u至根节点的路径之后,将这条路径上的所有节点的父节点都设为根节点,这样可以大大减少之后的查找次数。在find(x)运算中,找到根节点y后,我们再进行一次遍历从x到y的路径,并沿着路径改变所有节点指向父节点的指针,使它直接指向y。如下图:

在这里插入图片描述

  比如在执行find(4)运算中,找到集合名称为1。因此从4到1的路径上每个节点的父节点的指针都指向1。的确,路径的压缩增加了执行的工作量,但这种费用在随后的子序的寻找运算中得以补偿,因为我们将遍历更短的路径。
  
  可以证明,经过启发式合并和路径压缩之后的并查集,执行m次查找的复杂度为O(mα(m))。其中α(m)是Ackermann函数的某个反函数,你可以近似的认为它是小于5的。所以并查集的单次查找操作的时间复杂度也几乎是常数级的。

折叠反Ackermann函数:
  单变量反Ackermann函数(简称反Ackermann函数)α(x)定义为最大的整数m使得Ackermann(m,m)≤x。因为Ackermann函数的增长很快,所以其反函数α(x)的增长是非常慢的,对所有在实际问题中有意义的x,α(x)≤4,所以在算法时间复杂度分析等问题中,可以把α(x)看成常数。

α(x)出现在使用了按秩合并和路径压缩的并查集算法的时间复杂度中。

代码实现

int father[xx];
for(int i=1;i<=n;i++)
	father[i]=i;//初始化父节点为自身,最后只有根节点不变
int findroot(int x) {  //寻根函数非递归实现
   int root=x;
   while(father[root]!=root)   root=father[root];
   return root;
}
int findroot_recursion(int x){	//递归实现
	father[x]!=x?return findroot_recursion(father[x]):x;
void union(int x,int y){ //合并操作
	if( findroot(x)!=findroot(y) )
		father[findroot(x)]=findroot(y);//合并父节点而非子节点以降低树的深度
}
//路径压缩,让所有节点指向根
int findroot(int x){  //非递归实现
     int root=x,y;
     while(father(root]!=root) root=father[root];//寻根
     while(father[x]!=x) {
          y=x;
          x=father[x];
          father[y]=root;
    } return root;
}

int  findroot_recursion(int x){	//递归实现
     if(father[x]!=x) 
         father[x]=findroot_recursion(father[x]);
     return father[x];
}

点个赞呗

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值