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

251 阅读6分钟

相关概念及性质

定义\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~] 表示  x ~x~的父节点

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

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

1. 查找 - - - 如何求 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{Green}为什么合并的两棵树深度相同时,新的根节点的深度才 要 + 1~~ ???

  • 深度有差异时

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

  • 深度相等时

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

在这里插入图片描述


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

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

实现思路

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

​ 如下图所示,当我们合并 元素 A元素 B 所在的集合时,我们找到其中一个元素(这里我取 元素A)所属集合的根节点R1,以及另一个元素(这里我取 元素B)所属集合的根结点R2,及将R1的 父节点 置为R2 ,即 p[ find(A) ]=find(B)\textcolor{Orange}{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所在集合的大小,即 size[find(x)]+=size[find(x)]\color{Orange}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{Orange}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) 的偏移量

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