专题:并查集
内容来源:《挑战程序设计竞赛》(第2版)
一、引入
在一些有N个元素的集合应用问题中,我们通常是在开始时让每个元素构成一个单元素的集合,然后按一定顺序将属于同一组的元素所在的集合合并,其间要反复查找一个元素在哪哪个集合中。
该问题看似并不复杂,但数据量极大,若用正常的数据结构来描述的话,往往超过了空间的限制,计算机无法承受;而且复杂度较高,实现过程较复杂。因此,只能采用一种特殊数据结构——并查集来描述。
二、定义
并查集是一种用于分离集合操作(管理元素分组情况)的抽象数据类型。它所处理的是“集合”之间的关系,即动态地维护和处理集合元素之间复杂的关系。
当给出两个元素的一个无序对(a,b)时,需要快速“合并”a和b分别所在的集合,这其间需要反复“查找”某元素所在的集合。“并”、“查”和“集”三字由此而来。在这种数据类型中,n个不同的元素被分为若干组。每组是一个集合,这种集合叫做分离集合。
三、结构
使用树形结构实现,不过不是二叉树。
集合中的每个元素对应一个节点,每个集合对应一棵树。在并查集中,哪个节点是哪个节点的父亲以及树的形状等信息无需多加关注,整体组成一个树形结构才是重要的。
四、基本操作
并查集支持查找一个元素所属的集合 以及 两个元素各自所属的集合的合并 两种操作。
1. 初始化:
使用n个节点表示n个元素,最开始时没有边。
2. 合并:
相当于将两个集合合并为一个集合(即求并集的过程),假定在此操作前两个集合是分离的。
3. 查询:
查询两个节点是否属于同一集合(即他们是否有相同的根节点),我们需要沿着树向上走,来查询包含这个元素的树的根是谁。如果两个节点走到了同一个根,则可说明它们属于同一集合。
举例:
五、优化
1. 当树形结构发生“退化”,即趋于线性结构时,对并查集的基本操作的复杂度就会非常高。因此,有必要优化存储结构,想办法避免“退化”的发生。
2. 考虑高度合并的方法:对每棵树,记录树的高度rank;合并时,如果两棵树的rank不同,那么从rank小的向rank大的连边。
3. 并查集的路径压缩(一个重要且典型的方法)
(1)路径压缩实际上是在找完根结点之后,在递归回来的时候顺便把路径上元素的父亲指针都指向根结点。
(2)以上图为例,我们在“合并5和3”的时候,不是简单地将5的父亲指向3,而是直接指向根节点1,如图:
(3)在使用这种简化的方法时,为简便起见,即使树的高度发生了变化,也不修改rank的值。
4. 复杂度分析:O(ɑ(n)),是一个”均摊复杂度”,比O(log(n))还要快。
六、代码实现(以数组实现为例)
#include <stdio.h>
#define maxn 100005
int N,M,Q; //建立含N个元素的集合(编号1~N),进行M次合并、Q次查找
int r1,r2,x,y; //将x和y节点所属集合合并
int parent[maxn]; //parent[i]表示元素i的父亲节点
int rank[maxn]; //树的高度
void Init(int n) //初始化并查集
{
int i;
for(i=1;i<=n;i++)//初始状态:每个节点自身为根节点,且高度为0
{
parent[i]=i;
rank[i]=0;
}
}
/*
int Find(int x) //查询树的根(非递归实现)
{
while(parent[x]!=x)
x=parent[x];
return x; //找到返回根节点
}
*/
int Find(int x) //查询树的根(递归实现)
{
if(parent[x]==x)//x是根节点,找到,直接返回
return x;
else //x不是根节点,则从x的父亲节点开始,继续查找
return parent[x]=Find(parent[x]);
}
void Union(int x,int y) //合并节点x和y所属的集合
{
x=Find