手撕并查集及其优化,三大相关模板实现详解(附图解)

相关概念及性质

定 义 \color{Green}定义

​ 并查集(Disjoint-Set)是一种可以 动 态 维 护 \textcolor{Turquoise}{动态维护} 若干个 不 重 叠 的 集 合 \color{Turquoise}{不重叠的集合} ,并支持 合 并 \color{Red}合并 查 询 \color{Red}查询 两种操作的一种 树 型 \color{Turquoise}树型 数据结构

性 质 \color{Green}性质

  • 传 递 性 \color{Orange}传递性 :比如 A 传递给 B 一个性质 或 条件,让 B 同样拥有这个性质 或 条件,这就是传递性
  • 连 通 集 合 性 \color{Orange} 连通集合性 : 和数学概念上的集合定义类似,比如 A 和 B 同属于一个集合 ,B 和 C 同属于一个集合,那么有 A,B ,C 同属于一个集合

相关操作

基 本 原 理 \color{Green}基本原理

  • ① 每个集合用一棵树来表示,树根 的编号就是整个集合的编号
  • ② 每个节点存储它的 父节点 p [   x   ] p[~x~] p[ x ] 表示   x   ~x~  x 的父节点

问题 :如何判断树根 ? ? ?

​ 既然 p [   x   ] p[~x~] p[ x ] 表示   x   ~x~  x 的父节点 ,那么 当且仅当 p [   x   ] =   x p[~x~] = ~x p[ x ]= x 时,我们可以得出 节点   x   ~x~  x  就是整棵树的树根

1. 查找 - - - 如何求 x 所属的集合编号 ???

​ 顾名思义,就是确定元素   x   ~x~  x  属于哪一个集合

实现思路

  • 自底向上,不断查找每个节点的父节点,直到找到它所属集合的根节点

1.1 BF 做法

​ 如同实现思路那样,我们直接自底向上不断查找,直到找到集合的根节点

代码实现

int p[N]; // 存储每个节点的父节点

int find(int x)
{
    if(p[x] == x)  // 如果 p[x] == x, 此时 x 即为 树根
        return x;
   	
    return find(p[x]); // 否则,自底向上继续查找当前节点的父节点
}

1.2 路径压缩优化

​ 当树的高度过高,查询元素的个数过多时, BF 做法 的时间效率显得过于吃力,此时我们需要对元素所在集合查询时的路径进行缩短优化,此时均摊的时间复杂度为 O(logn)

思路

​ 所谓 ” 路径压缩 “ ,就是一种在执行 ” 查找 “ 过程中,扁平化树的结构的方法,使得在路径上的每一个节点都可以直接连在根节点上,为以后直接或则间接引用节点的操作加速

关 键 \color{Red}关键 在搜索一遍的同时,将所有节点的父节点都置为根节点

​ 如下图所示,当我们调用 FIND 查找 元素 C 时,递归地,置此路径上的 每一个节点的父节点 直接指向 根节点,正如 元素 C 那样,将原来的 父节点 A 置为 根节点 R,这样当我们在下次查找元素 C 所属的集合时,直接以 O(1) 的时间复杂度找出根节点的编号

在这里插入图片描述

代码模板

int p[x];// 存储每个节点的父节点

int find(int x)
{
    if(p[x] != x) p[x] = find(p[x]); // 递归地查找根节点,并置路径上的所有节点的父节点为根节点
      
    return p[x];// 如果 p[x] == x, 此时 p[x] 即为 树根
}

1.3 按秩合并优化

​ 还有一种优化的方法,叫做 按秩合并,均摊时间复杂度为 O(logn)。如果题目需要维护明确的父子关系而用到了按秩合并的话,是不能用路径压缩的。一旦用了路径压缩,会破坏树的形态,因为原来的节点的父节点会直接压缩到祖先上,这样一来我们调用的时候父子关系发生了改变,造成了算法的错误

核 心 \color{Red}核心

  • ” 秩 “ :树的深度 (未压缩路径的)OR 集合的大小
  • 合并时,秩 较大 的树根 作为 秩 较小 的树根的 父节点

代码模板

  • 按 元 素 所 在 集 合 的 大 小 合 并 \color{Orange}按元素所在集合的大小合并
void uni(int x, int y)// 按 集合的大小 合并
{
    int px = find(x);
    int py = find(y);
    
    if(px != py) //  秩 较大 的树根 作为 秩 较小 的树根的父节点
    {
        if(size[px] < size[py])  // y 的树根较大 , 则 y 的树根作为 x 树根的父节点
           p[px] = py, size[py] += size[px];
        else
           p[py] = px, size[px] += size[py];
    }
}
  • 按 元 素 所 在 树 的 深 度 合 并 \color{Orange}按元素所在树的深度合并
void uni(int x, int y)
{
    int px = find(x);
    int py = find(y);
    
    if(px != py) //  秩 较大 的树根 作为 秩 较小 的树根的父节点
    {
        if(high[px] < high[py]) // y 的树根较大 , 则 y 的树根作为 x 树根的父节点
            p[px] = py;
        else // 包含  > = 的情况
        {
            p[py] = px;
            if(high[px] == high[py])
                high[px] ++; //按深度合并,只有在两树高度相等的时候才更新
        }         
        
    }
}

为 什 么 合 并 的 两 棵 树 深 度 相 同 时 , 新 的 根 节 点 的 深 度 才 要 + 1    ? ? ? \color{Yellow}为什么合并的两棵树深度相同时,新的根节点的深度才 要 + 1~~ ??? +1  

  • 深度有差异时
          ~~~~~      由于我们在合并时,将 深度较大 的树根作为 深度较小 的树根的父节点,因为深度较小的树并不能影响合并后的树的深度,故当两棵树高度有差异时,我们不做处理

  • 深度相等时
          ~~~~~      如下图所示,我们有两个深度均为 2 的树,现在要合并这两棵树,这里把R1 的父节点置为R2,显然在合并后,树的高度便增加 了 1

在这里插入图片描述


2. 合并 - - - 如何合并两个集合 ???

​ 当我们查找出两个元素所属的集合编号后,进一步就可以确定两个元素是否同属于一个集合

实现思路

  • ① 若 属于 同一个子集,我们什么都不做
  • ②若 不属于 同一个子集,我们 置 一 个 节 点 的 祖 宗 节 点 ( 根 节 点 ) 的 父 节 点 , 为 另 一 个 节 点 的 祖 宗 节 点 \color{Orange}置一个节点的祖宗节点(根节点)的父节点,为另一个节点的祖宗节点

​ 如下图所示,当我们合并 元素 A元素 B 所在的集合时,我们找到其中一个元素(这里我取 元素A)所属集合的根节点R1,以及另一个元素(这里我取 元素B)所属集合的根结点R2,及将R1的 父节点 置为R2 ,即 p [   f i n d ( A )   ] = f i n d ( B ) \textcolor{Yellow}{p[~find(A)~] = find(B)} p[ find(A) ]=find(B)

在这里插入图片描述

模板理解及实现

​ 以下代码模板 在此 均采用路径压缩优化

朴素并查集

​ 正如名字所示那样,我们只实现并查集的基本功能,即查找和合并,不维护其他的信息

代码模板

int p[N];// 储存每个节点的祖宗节点

// 返回 x 的祖宗节点
int find(int x)
{
    if(p[x] != x) return p[x] = find(p[x]);
    
    return p[x];
}

//  初始化, 假定 节点编号是 1 ~ n
for(int i = 1; i <= n; i ++ )
{
    p[i] = i;// 初始化时,我们将各个节点的父节点置为本身
}

// 合并 元素 x 和 元素 y 所在的两个集合
p[find(x)] = find(y);

维护 Size 的并查集

思路

​ 如下图所示,当我们合并 元素 x元素 y所在集合时,将元素 x 的所在集合的祖宗节点的父节点置为 元素 y 所在集合的祖宗节点,并使 元素y所在集合的大小 加上 元素 x所在集合的大小,即 s i z e [ f i n d ( x ) ] + = s i z e [ f i n d ( x ) ] \color{Yellow}size[find(x)] += size[find(x)] size[find(x)]+=size[find(x)]

代码模板

int p[N]; // p[] 存储每个节点的祖宗节点
int size[N]; // size[] 只有祖宗节点的有意义,表示祖宗节点所在集合中的节点的数量

// 返回 x 的祖宗节点
int find(int x)
{
    if(p[x] != x)  p[x] = find(p[x]);
    
    return p[x];
}

// 初始化,假定节点的编号是 1 ~ n
for(int i = 1;i <= n; i ++ )
{
    p[i] = i; // 初始化时,我们将各个节点的父节点置为本身
    size[i] = 1; // 只有 i 本身一个元素
}

//合并 元素 x 和元素 y 所在的两个集合
p[find(x)] = find(y);
size[find(x)] += size[find(y)];

维护到祖宗节点距离的并查集

思路

​ 如下图所示,当进行路径压缩过程中,我们将 元素x 的父节点置为 祖宗节点的同时,将 元素x 到父节点的距离d[x] 置为 d[x] + d[px] ,即 d [ x ] + = d [ p [ x ] ] \color{Yellow}d[x] += d[p[x]] d[x]+=d[p[x]]

在这里插入图片描述

代码模板

int p[N];// p[] 存储每个节点的祖宗节点
int d[N];// d[x] 储存 x 到 其父节点 p[x] 的距离

//返回 x 的祖宗节点
int find(int x)
{
    if(p[x] != x) 
    {
        int u = find(p[x]);
        d[x] += d[p[x]];
        p[x] = u;
        
    }
    
    return p[x];
   
}

// 初始化,假定节点的编号是 1 ~ n
for(int i = 1;i <= n; i ++ )
{
    p[i] = i; // 初始化时,我们将各个节点的父节点置为本身
    size[i] = 1; // 只有 i 本身一个元素
}

//合并 元素 x 和元素 y 所在的两个集合
p[find(x)] = find(y]);
d[find(x)] = distance; // 根据具体问题而定,初始化find(x) 的偏移量

以上内容尚未完全,随着今后学习的推进,我会继续对其进行补充与完善。另外,大家如果觉得我写的还行的话,还请赠予我一个可爱的赞,你的赞对于我是莫大的支持

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值