本篇在基础并查集上添加了合并的优化以及路径压缩,复杂度小于O(logn),如果有同学认为难以理解,可以去理解一下初级并查集。
合并的优化:
在合并元素x和y时先搜到它们的根结点,然后在合并这两个根结点,即把一个根结点的即改成另一个根结点。这两个根结点的高度不同,如果把高度较小的集合并到较大的集上,能减少树的高度。下面是优化后的代码,在初始化时用height[i]定义元素i的高度,在合并时更改。
代码如下:
void union_set(int x,int y)
{
x = find_set(x);
y = find_set(y);
if(height[x] ==height[y]){
height[x] = height[x] + 1;
people[y] = x;
}
else{
if(height[x]<height[y]) people[x] = y;
else people[y] = x;
}
}
查询的优化——路径压缩
在上面的查询程序find_set()中,查询元素i所属的集需要搜索路径找到根结点,返回的结果是根结点。这条搜索路径可能很长。如果在返回的时候顺便把i所属的集改成根结点,那么下次再搜的时候就能在O(1)的时间内得到结果。
代码如下:
int find_set(int x){
if(x!=people[x])people[x] = find_set(people[x]);
return people[x];
}
这个方法称为路径压缩,因为在递归过程中,整个搜索路径上的元素(从元素i到根结点的所有元素)所属的集都被改为根结点。路径压缩不仅优化了下次查询,而且优化了合并,因为在合并时也用到了查询。
上面的代码用递归实现,如果数据规模太大,担心爆栈,可以使用下面的非递归代码:
int find_set(int x)
{
int r = x;
while(people[r]!=r)r = people[r];//找到根结点
int i=x,j;
while(i!=r){
j = people[i];//用临时变量j记录
people = r;//把路径上元素的集改为根结点
i = j;
}
return r;
}
完整代码:
#include<iostream>
using namespace std;
int people[1010];
int height[1010];
int find_set(int x)
{
if(x!=people[x])people[x] = find_set(people[x]);//路径压缩
return people[x];
}
void union_set(int x,int y)//优化合并操作
{
x = find_set(x);
y = find_set(y);
if(height[x] == height[y]){
height[x] = height[x] + 1;//合并,树的高度加一
people[y] = x;
}
else{//把矮树并到高树上,高树的高度保持不变
if(height[x]<height[y])people[x] = y;
else people[y] = x;
}
}
int main()
{
int t;
cin>>t;
while(t--){
int n,m;
int a,b;
cin>>n>>m;
for(int i=1;i<=n;i++){
people[i] = i;
height[i] = 0;
}
for(int i=1;i<=m;i++){
cin>>a>>b;
union_set(a,b);
}
int ans = 0;
for(int i=1;i<=n;i++)
if(people[i] == i)
ans++;
cout<<ans<<endl;
}
system("pause");
return 0;
}