zoukankan      html  css  js  c++  java
  • (数据结构)并查集

    引言

    并查集(Union-Find Set),顾名思义,是实现快速合并集合与查询元素所在集合的数据结构。
    它在包括但不限于最小生成树Kruskal算法的众多算法中有很好的使用效果。

    结构

    形象地看,并查集是一个类似于森林的结构,同一个集合的元素都连在同一棵树上,查询操作即寻找树的根节点(唯一编码),合并操作即将一个集合的根连在另一棵树上。
    抽象地看,这是一个记录父节点的数组(优化算法还包括这个集合的元素数量)。

    性质

    • 该数组记录父节点编号。
    • 每个集合的根节点是一个具有特殊标记(一般其父节点指向其本身)的点。

    操作

    1. 查询元素所在集合
      根据性质,只需要依照沿着这个集合的树向上爬即可。
    2. 合并两个元素所在的集合
      根据操作1找到两个根节点后连接即可。

    代码实现

    基础版

    //////////////////////////////////////////////////////////////////////
    //Target: To implement the Union-Find Set
    //@Author: Pisceskkk
    //Date: 2019-2-24
    //////////////////////////////////////////////////////////////////////
    
    #define Max_N (int)1e6
    
    int n,f[Max_N];
    
    void init(int n){
        for(int i=1;i<=n;i++){
            f[i] = i;
        }
    }
    
    int find(int x){
        return (f[x) == x)?x:find(f[x]);
    }
    
    bool mege(int x,int y){
        x = find(x);
        y = find(y);
        if(x == y) return 0;
        f[x] = y;
        return 1;
    }
    

    优化

    路径压缩

    问题来了:在特殊情况下,这棵树可能是一条长长的链。设链的最后一个结点为x,则
    每次执行find(x)都会遍历整条链,效率十分低下。看上去是个很棘手的问题,其实改进方法
    很简单。既然每棵树表示的只是一个集合,因此树的形态是无关紧要的,并不需要在“查
    找”操作之后保持树的形态不变,只要顺便把遍历过的结点都改成树根的子结点,下次查找
    就会快很多了。
    ——《算法竞赛入门经典》刘汝佳

    正如刘犇所言,我们可以通过路径压缩来解决这一个特殊情况。

    int find(int x){
        return f[x] = (f[x] == x ? x : find(f[x]));
    }
    

    另一个版本是非递归版。虽然个人认为,在添加路径压缩之后,这个递归版函数应该不会出现递归层数过深而导致爆栈的情况。

    int find(int x){
        int fx = f[x];
        while(fx != f[fx]){
            fx = f[fx];
        }
        while(x != fx){
            int temp = f[x];
            f[x] = fx;
            x = temp;
        }
        return fx;
    }
    

    启发式合并

    路径压缩虽然好处多多,但是也有一个显而易见的缺陷——丢失大量信息,尤其是在一些要求解连接时间的题目(e.g. BZOJ 4668 - 冷战 )中,路径压缩就不再是最好的优化方法了。
    那么我们要如何才能有效解决上述问题,同时又不致丢失数据呢?
    思考这样一个问题,查找操作的时间复杂度取决于什么?
    那当然是树的高度啦。那么我们降低复杂度的方法就是让树高尽量小。
    考虑两棵树合并,一颗树高为6,另一棵为2,那么合并之后树高可能为几?7或者6。那么合并方案也就出现了,往树高大的树上合并,这也就是启发式合并

    #define Max_N (int)1e6
    
    int n,f[Max_N],height[N];
    
    void init(int n){
        for(int i=1;i<=n;i++){
            f[i] = i;
            height[i] = 1;
        }
    }
    
    int find(int x){
        while(f[x] != x)x=f[x];
        return x;
    }
    
    bool mege(int x,int y){
        x = find(x);
        y = find(y);
        if(x == y) return 0;
        if(height[x] > height[y]){
            f[y] = x;
        }
        else if(height[x] < height[y]){
            f[x] = y;
        }
        else {
            f[y] = x;
            height[x]++;
        }
        return 1;
    }
    

    除了以高度为标准进行启发式合并,刘犇还提到了一个按秩合并的方法(个人感觉这是一个和高度差不太多的量)。

    一个优化是:把小的树合并到大树中,这样会让深度不太大。这个优化称为启发式合并。(rank[i]) 表示 (i) 的秩,并用秩来代替深度做刚才提到的启发式合并。

    技巧

    标记

    既然数组(f[i])只负责标记,那么我们也就不必局限于f[i] == i这一种标记。

    1. 初始化0
      简单的memset(f,0,sizeof(f))也是可以满足需求的。
    2. 与height(或 rank)数组合并
      (f[i])数组设为负数,其绝对值等于树高或者树的秩也是个不错的选择。

    非路径压缩

    当没有路径压缩时,可以通过删除最后一次合并来达到集合拆分的操作。

    代码实现暂略。
    如果还有其他技巧将会及时更新。

    我思故我在
  • 相关阅读:
    Pixar 故事公式
    你想住在中国哪里
    tar.gz方式安装nacos设置使用systemct进行service方式的管理并设置开机自启动
    记一个nginx server_name配置多个时的坑
    linux软链接的创建、修改和删除
    阿里云SLB的健康检查配置
    (转载)bullet安装之——windows下的安装与VS开发
    [译] 找到ndarray中的重复行
    [译] 对dataframe数据按照某列值进行分组,分组后连接字符串
    [译] 如何将列表嵌套列表的情况转化成一维列表
  • 原文地址:https://www.cnblogs.com/pisceskkk/p/10424444.html
Copyright © 2011-2022 走看看