C++并查集&按大小合并&按秩合并&路径压缩

3,018 阅读3分钟

等价关系

关系R定义在集合S上。对于S中的a,b元素,如果aRb为true,那么就说ab有关系。 等价关系是满足下面三个条件的关系:

  • 自反性aRa
  • 对称性aRb当且仅当bRa
  • 传递性若aRb,bRc则aRc。 例如≤不是等价关系,因为他不满足对称性; 导线连通则是一个等价关系。

动态等价行问题

给定一个等价关系~,考虑对于任意的a,b,是否有a~b?

一个元素的等价类是S的一个子集,它包含所有与a有(等价)关系的元素。 有了这个定义,我们只需要确定a,b是否在一个等价类中。 那么如何形成等价类?

  • find,判断a,b的等价类名字是否相同
  • union,检查a,b是否在一个等价类中,如果没有添加关系a~b

那么最直观的做法就是,把集合映射为一个int型数组,find(x)返回x的根,union(x,y)操作始终把x作为y的根。

class UF
{
private:
    vector<int> s;   
public:
    int find(int x);
    void union(int root1,int root2);
    UF(int n):s(n, -1){};
};

int UF::find(int x) const{
    if(s[x]<0)
        return x;
    else
        return find(s[x]);
}

void UF::union(int root1,root2){
    s[root2] = root1; 
}

但是这种算法是会建立深度为N-1的树,执行N次find操作的时间复杂度是O(N)。那么M次混合操作(union,find)的时间复杂度就是O(MN)。 那么我们如何减小树的深度,以获得更灵巧的union算法?

* 按大小合并
* 按秩合并

按大小合并

我们总是让较小的树成为较大的树的子树。 实现起来的话我们让根节点保存节点数目的负值,然后每次合并之前比较大小。初始时只有一个节点,所以初始值设为-1。

void UF::unionBySize(int root1,root2){
    if(s[root2] > s[root1])  //因为是负值,所以说明root1更大
        s[root1] += s[root2];
        s[root2] = root1;
    else {
        s[root2] += s[root1];
        s[root1] = root2;
    }
}

按秩合并

此时我们追踪每棵树的高度而不是大小,并总让浅的树成为深的树的子树。可以看出只有两颗树的高度相同时树的高度才会增加。当然我们实际上也是存储树的高度的负值。

void UF::unionByRank(int root1,root2){
    if(s[root2] < s[root1])  //因为是负值,说明root2更深
        s[root1] = root2;
    else {
        if(s[root1] == s[root2])
            --s[root1];  //高度加1
        s[root2] = root1;  
    }
}

这样M次操作的时间平均下来就是线性的O(M),但是最坏的情况O(MlogN)依然可能发发生,例如把集合放到队列里并让前两个元素反复出队入队。这样无论如何都又可能生成最坏的树,也就是说union算法没有更多改进的空间了。

所以我们考虑改进find方法。

路径压缩(改进find)

就是每次执行find(x)操作都让他的父节点变成他祖父节点,也就是离根更近一次。

int UF::find(int x) {
    if(s[x]<0)
        return x;
    else
        return s[x] = find(s[x]);
}

下面我们来看一个我写的例子:畅通工程问题