一文搞定并查集
并查集是什么?
并查集是一种树型的数据结构,用于处理一些不相交集合的合并及查询问题,常常在使用中以森林来表示。并查集通常用来解决管理若干元素的分组问题
并查集可以高效完成下列操作 (并与查的功能) :
- 并:合并元素a和元素b所在的组
- 查:查询元素a和元素b是否属于同一组
并查集的结构
并查集使用树形结构形成,实际上可以看作是森林
如图所示,我们可以将左侧的分组以右侧的森林的形式表示。其中每个元素对应树中的一个节点,每一个组对应着森林中的一颗树,在并查集中,通常我们不在意父节点与子节点的顺序,实际上树的形状也无关紧要,只需要令同一组的元素对应到同一个树上即可。
并查集的逻辑实现
-
初始化
准备n个节点表示n个元素,在初始化时,彼此之间无关联,即均单独成树
-
合并
合并即为合并两个树,由于每个树表示一个集合(即相同类别,无顺序之分),故只需要让一个树的根节点指向另一个树即可。合并的例子:
-
查询
查询即查询两个元素是否是同一集合中,即查询对应的两个节点是否在同一个树上,因此只需要查询两个节点所对应的树的根节点是否一致即可。
分析该图,在合并前,4对应的根节点是3,而7对应的根节点是6,故不在同一树中,不属于同一集合。当合并后,4对应的根节点是6,7对应的根节点也是6,故二者根节点相同,说明它们在同一集合中。
并查集逻辑实现的优化
主要优化思路为尽可能使树的高度降低,从而尽可能减少查询的时间复杂度
-
当进行合并时,尽可能使高树的根节点作为合并后的根节点。通常方法是记录每个树的高度rank,当合并时,比较两个树的rank,将rank值大的树的根作为合并后的根节点。这样可以避免树的复杂度过高
上图是两种合并方式,显然,如果以6为合并后的根节点,则树的高度为3,反之则为4。故当合并时,如果考虑到两个树的高度,将使得最终树的高度尽可能小
-
进行路径压缩,使得并查集更高效。对于每个节点,一旦向上走到了一次根节点,就把这个节点的父亲节点直接指向根节点。即若一个节点的根节点已经计算出,则直接将其指向根节点,尽可能避免重复计算
如果已计算得到8的根节点为6,则直接将8指向6即可,从而降低树的高度实际上,由于查询过程是自下而上的,因此可以将查询过程的路径上遇到的所有节点均直接指向最终的根节点,也就意味着实际上每次查询均附带着实现了路径压缩
查询节点10的根节点的过程中,实际上将得到节点10、节点9、节点8、节点7对应的树的根节点均为节点 6,因此可以直接将其指向节点6,实现路径压缩。为了简化起见,路径压缩过程通常不去修改树的高度值
并查集的复杂度
易知,进行优化后,并查集的效率将非常高,比O(log n) 还快
并查集的实现(C++)
在并查集的实现过程中,通常我们用一个数组即可实现,数组的下标表示所对应的节点,数组的值代表该节点所在的树的根节点标号
par数组表示节点,存储内容为根节点的序号,rank数组表示树的高度,不需做非常精确的计算
//并查集的实现
#include<iostream>
using namespace std;
const int MAX_N=10000;
int par[MAX_N]; //父节点, 存储该节点对应的根节点的位置
int rank[MAX_N]; //树的高度,存储该树的位置
void init(int n) //初始化n个元素,使得该n个元素最初的根节点均为自身
{
for(int i=0;i<n;i++)
{
par[i]=i;
rank[i]=0;
}
}
int find (int x) //查询第x个节点所在树的根
{
if(par[x]==x)
return x;
else
return par[x]=find(par[x]); //这一步很巧妙,既通过递归找到根节点,又同时完成了路径的压缩
}
void unite(int x,int y) //将x和y所在的集合合并
{
//合并时主要考虑两个根即可
x=find(x);
y=find(y);
if(x==y) return ; //在同一集合,不需操作
if(rank[x]<rank[y]) //rank大的节点作为合并后的根节点
par[x]=y;
else
{
if(rank[x]==rank[y]) rank[x]++; //如果两个树高度一样,则合并后树的高度加1
par[y]=x;
}
}
bool same(int x,int y) //判断x和y是否是同一个集合
{
return find(x)==find(y);
}
//在main函数中做一些简单的测试:
int main()
{
int n=10;
init(n); //初始化10个节点
for(int i=0;i<n;i++)
cout<<par[i]<<' ';
cout<<endl;
unite(1,2);
unite(2,3);
unite(3,5); //1,2,3,5在同一集合
cout<<par[1]<<' '<<par[2]<<' '<<par[3]<< ' '<<par[5]<<endl;//same number
unite(4,6);
unite(6,7); //4,6,7在同一集合
cout<<same(4,5)<<' '<<same(4,6)<<endl; //0 1
unite(4,5);
cout<<same(5,6)<<endl; //1
}
掌握并查集的实现的几个函数即可轻松应付大多数并查集题目