并查集主要是为了解决图的动态连通性的问题,主要需要实现union、find的操作,基本的C++实现如下:
class UnionFind{
private:
int count; //记录连通分量的个数
vector<int> parent;
public:
UnionFind(int n){
count = n;
parent = vector<int>(n,0);
for(int i = 0; i < n; i++){
parent[i] = i; //自己指向自己
}
}
//将p和q进行合并
void Union(int p, int q){
int rootP = Find(p);
int rootQ = Find(q);
if(rootP == rootQ){
return;
}
//简单的将p的根指向q的根
parent[rootP] = rootQ;
count--;
}
//找到p的根
int Find(int p){
while(parent[p] != p){
p = parent[p];
}
return p;
}
bool isConnected(int p, int q){
return Find(p) == Find(q);
}
int Count(){
return count;
}
};
上述的实现在union的时候,只是简单的将一个根指向另一个根,会造成树的深度过深,在find操作的时候效率低下,在并查集讲解中有更详尽的介绍。一般有两个优化union的方法,一是将元素少的集合的根节点指向元素多的集合的根节点,如下图将9指向8,而不是将8指向9。具体实现如下所示:
void Union(int p, int q){
int rootP = Find(p);
int rootQ = Find(q);
if(rootP == rootQ)
return;
//进行优化 --->将元素少的集合并到元素多的集合
if(size[rootP] < size[rootQ]){
parent[rootP] = rootQ;
size[rootQ] += size[rootP];
}else{
parent[rootQ] = rootP;
size[rootP] += size[rootQ];
}
//两个分量合二为一
count--;
}
如果按照上面的方法对下图进行合并,则4的根8指向了2的根7,深度为4。但是更好的做法是将2的根7指向4的根8,深度为3。因此另一种优化是将集合中深度小的根指向集合中深度大的根。具体实现如下:
void Union(int p, int q){
int rootP = Find(p);
int rootQ = Find(q);
if(rootP == rootQ)
return;
//进行优化 --->将深度小的指向深度大的
if(rank[rootP] < rank[rootQ]){
parent[rootP] = rootQ;
}else if(rank[rootP] > rank[rootQ]){
parent[rootQ] = rootP;
}else{
parent[rootP] = rootQ;
rank[rootQ] += 1; //当层数相同的时候,更新rank
}
//两个分量合二为一
count--;
}
刚才我们关注点在union操作上,其实find操作也可以优化,之前的实现就是一直往上遍历进行寻找。因为我们关注点是找到根,因此可以跳着找,具体实现如下:
int Find(int p){
while(parent[p] != p){
//进行路径压缩
parent[p] = parent[parent[p]];
p = parent[p];
}
return p;
}
还有一种find的优化方式:将每个元素直接指向根,在查找的时候就只查找一次就可以了。
//返回p的根节点
int Find(int p){
while(parent[p] != p){
//更进一步进行路径压缩
parent[p] = Find(parent[p]);
}
return parent[p];
}