文章目录
相关概念及性质
定 义 \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) 的偏移量
以上内容尚未完全,随着今后学习的推进,我会继续对其进行补充与完善。另外,大家如果觉得我写的还行的话,还请赠予我一个可爱的赞,你的赞对于我是莫大的支持