Union-Find算法

声明

本文章的内容来源于《abuladong的算法小抄》,为了加强自己的记忆,写本文章梳理学习内容。

算法简介

本算法为通常说的并查集算法,主要解决图论中“动态连通性”问题。数据结构(清华版)课本上有一节专门讲了这个算法。

问题

一个图(无向图)有5个节点(1~5),他们不是相互连通的,那么我们认为连通分量为5个。
在此我们可以实现Union-Find算法的API:

class UnionFind{
public:
    // 将p节点和q节点连通
    void union(int p, int q);
    //判断p节点和q节点是否连通
    bool connect(int p, int q);
    //返回图中有多少个连通分量
    int count();
}

这里得连通是建立在无向图之上的。
如果现在调用union(1,2),那么节点0和节点1被连通,连通分量就变成4个。
再调用union(2,3),那么这时1,2,3节点都被连通,再调用connected(1,3)也会返回true,连通分量变成8个。
由此可见,本算法的关键在于union和connected函数的效率。

基本思路

模型:使用森林来表示图的动态连通性
数据结构:数组实现这棵树
表示连通性:设定数的每个节点用一个指针指向其父节点,如果是根节点,那么其父节点就是他自己。
代码实现:

#include<iostream>
#include<vector>
using namespace std;

class UnionFind{
private:
    //记录连通分量
    int count;
    //记录其父节点
    vector<int> parent;
public:
    //构造函数
    UnionFind(int n){
        //开始都不连通
        this->count = n;
        //初始化父节点都是指向自己
        this->parent.resize(n);
        
        for(int i = 0; i < n; i++) this->parent[i] = i;
    }

    void Union(int p, int q){
        //找根节点
        int rootp = find(p);
        int rootq = find(q);
        //如果根节点相同 说明两个节点是连通的
        if(rootq == rootp) return;

        
        //跟节点连接起来
        // 下面两种连接都可以
        // this->parent[rootq] = rootp;
        this->parent[rootp] = rootq;
        this->cout--;
    }
    //找根节点
    int find(int x){
        while(x != this->parent[x]) x = this->parent[x];
        return x;
    }
    //返回连通分量
    int count(){
        return this->count;
    }

    bool Connected(int p, int q){
        int rootq = find(q);
        int rootp = find(p);
        //根节点相同 说明是连通的
        return rootp == rootq;
    }
};

如果两个节点被连通,则让其中一个节点的根节点接到另一个节点上。
如果两个节点拥有相同的根节点,说明这两个节点是连通的。

时间复杂度:find,union,connected的最坏时间复杂度是O(N)
如果数据量巨大,这种复杂度是不可以接受的,主要的原因是我们构建的树模型存在不平衡现象

平衡优化

出现不平衡问题主要出现在union现象
我们只是简单粗暴的将一个树的根节点接到另一棵树的根节点下面,这样会产生极度不平衡现象。
将小一些的树接到大一些树的下面,这样就可以更加平衡一些。我们定一个变量,记录每一颗树的节点数目。

class UnionFind{
private:
    //记录连通分量
    int count;
    //记录其父节点
    vector<int> parent;

    //增加数组,记录树包含的节点数
    vector<int> size;
public:
    //构造函数
    UnionFind(int n){
        //开始都不连通
        this->count = n;
        //初始化父节点都是指向自己
        this->parent.resize(n);
        this->size.resize(n);
        for(int i = 0; i < n; i++) {
            this->parent[i] = i;
            this->size[i] = 1;
        }
    }

    void Union(int p, int q){
        //找根节点
        int rootp = find(p);
        int rootq = find(q);
        //如果根节点相同 说明两个节点是连通的
        if(rootq == rootp) return;

        
        //跟节点连接起来
        // 下面两种连接都可以
        // this->parent[rootq] = rootp;  //q树接到p树上 ***注意
        // this->parent[rootp] = rootq;
        //小树接到大树的下面
        if(this->size[rootp] > this->size[rootq]){
            //p树的节点多,为大树, q树接到p树上 ***注意
            this->parent[rootq] = rootp;
            this->size[rootp] += this->size[rootq]
        }
        else{
            this->parent[rootp] = rootq;
            this->size[rootq] += this->size[rootp]
        }
        this->cout--;
    }
 //其他函数不变
}

此时 find、union、connected的时间复杂度标成了O(Nlog(N))
注意 this->parent[rootq] = rootp; 是q树接到p树上,在写次博客的时候差点把自己弄晕了。

路径压缩

我们可以进行路径压缩,让树的高度保持常数,这样find就可以用O(1)的时间复杂度找到某个根节点,相应的connected和union函数也降到了O(1)

    int find(int x){
        // while(x != this->parent[x]) x = this->parent[x];
        while(parent[x] != x){
        	// 只需要加入这一样即可
            parent[x] = parent[parent[x]];
            x = parent[x];
        }
        return x;
    }

有了压缩路径,那么记录树节点个数的size数组还要么?
可以不要,但是加上会效率高一些,可以让树更平衡一些。所以,路径压缩和重量平衡都用上是最好的选择。

完成代码

#include<iostream>
#include<vector>
using namespace std;

class UnionFind{
private:
    //记录连通分量
    int count;
    //记录其父节点
    vector<int> parent;

    //增加数组,记录树包含的节点数
    vector<int> size;
public:
    //构造函数
    UnionFind(int n){
        //开始都不连通
        this->count = n;
        //初始化父节点都是指向自己
        this->parent.resize(n);
        this->size.resize(n);
        for(int i = 0; i < n; i++) {
            this->parent[i] = i;
            this->size[i] = 1;
        }
    }

    void Union(int p, int q){
        //找根节点
        int rootp = find(p);
        int rootq = find(q);
        //如果根节点相同 说明两个节点是连通的
        if(rootq == rootp) return;

        
        //跟节点连接起来
        // 下面两种连接都可以
        // this->parent[rootq] = rootp;  //q树接到p树上 ***注意
        // this->parent[rootp] = rootq;
        //小树接到大树的下面
        if(this->size[rootp] > this->size[rootq]){
            //p树的节点多,为大树, q树接到p树上 ***注意
            this->parent[rootq] = rootp;
            this->size[rootp] += this->size[rootq]
        }
        else{
            this->parent[rootp] = rootq;
            this->size[rootq] += this->size[rootp]
        }
        this->cout--;
    }
    //找根节点
    int find(int x){
        // while(x != this->parent[x]) x = this->parent[x];
        while(parent[x] != x){
            parent[x] = parent[parent[x]];
            x = parent[x];
        }
        return x;
    }
    //返回连通分量
    int count(){
        return this->count;
    }

    bool Connected(int p, int q){
        int rootq = find(q);
        int rootp = find(p);
        //根节点相同 说明是连通的
        return rootp == rootq;
    }
};

int main(){

    UnionFind A = UnionFind(10);
    return 0;
}

除了构造函数的时间复杂度是O(N),其他的时间复杂度都是O(1)

结束

如果有读者需要图来解释的话,等我忙完这段时间再补上。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值