%%
# 纲要
> 主干纲要、Hint/线索/路标
# Q&A
#### 已明确
#### 待明确
> 当下仍存有的疑惑
**❓<font color="#c0504d"> 有什么问题?</font>**
# Buffer
## 闪念
> sudden idea
## 候选资料
> Read it later
%%
# 并查集
并查集是一种用于**管理 "元素所属集合"** 的**树型**数据结构,其实现为 "**一个==森林==**"——其中**每棵树代表一个集合**,**树中的节点表示 "对应==集合中的元素=="**
并查集主要支持两个基本操作:
- **==查找==**(Find): **查询某个元素所属集合**(查询对应的树的**根节点**)
- **==合并==**(Union):**合并两个元素所属集合**(合并对应的树)
<br>
## 并查集操作的时空复杂度
> 由于 `Unite` 是在 `Find` 之后执行常量级别的**父指针修改**,因此 `Union` 操作的时间复杂度同 `Find` 操作。
- 空间复杂度:$O(n)$ ,其中 $n$ 为元素数量。
- 时间复杂度:
- 同时了应用 "**路径压缩** 与 "**按秩合并**" 优化时,单次操作均为 $O(\alpha(n)) \approx O(1)$
- $\alpha()$ 为**阿克曼函数的反函数**(增长极其缓慢)[^1],单次操作的平均开销**可认为是常数 $O(1)$**。
###### 分析说明
| 优化方式 | 单次操作时间复杂度 | 最坏情况下 $m$ 次操作的时间复杂度 |
| ---------------- | -------------- | -------------------- |
| 无忧化 | $O(n)$ | $O(mn)$ |
| 路径压缩 or 按秩合并(其一) | $O(\log n)$ | $O(m\log n)$ |
| 路径压缩 + 按秩合并 | $O(\alpha(n))$ | $O(m\,\alpha(n))$ |
在没有 "**路径压缩**" 的情况下,由于**树**可能退化为 "**单链**",因此单次 `Find` 的最坏时间开销可能为 $O(n)$。,因此同为 $O(n)$。
<br>
## 并查集的应用
- **网络连通性方面**;
- **最小生成树(Kruskal 算法)**:用于判断边是否形成环。
- **动态连通性问题**:如社交网络好友关系的合并查询。
- **等式不等式系统**:如 `a == b, b == c, c != d` 类型的判断问题。
#### 网络连通性方面
- 判断**两个节点**是否在连通(是否在**同一连通分量**中)
- 先得到所有 "**连通分量**" 后,再通过 `find` 查询看两节点是否在同一集合中。
- **判断无向图的连通性**:
- 将无向图中的节点作为并查集的元素,若两个节点之间有边相连,则**将它们所属的集合合并**。
- 最终,**若所有节点都属于同一个集合,则图是连通的**;否则,图是非连通的。
- **获取无向图中的所有连通分量**(求连通分量数量)
- **获取有向图中的所有强连通分量**
- 将有向图中的节点作为并查集的元素,从**任意节点**开始进行 DFS,将**已访问节点的父节点**指向 **==同一个根节点==**(可选入口节点所在树的根节点)。
- 搜索结束后,**每个根节点所代表的节点集合就是一个强连通分量**。
- **无向图的环路检测**
- Kruskal 算法中(判断**边加入后是否会成环**)
<br><br>
# 并查集的实现
主要涉及三个部分,其余如 "**删除**" 与 "**移动**" 操作参见[^1]
- (1)**初始化**
- 初始时, **每个元素**均作为**一个==独立集合==** (即对应一棵独立的树, 只有 **==根节点==**)
- 用一个数组记录 "**==每个元素的父节点==**",初始时记录**各节点的父节点为其自身**。
- (2)**查找**
- 操作:**从给定节点沿着树向上移动**,直至**根节点**,返回根节点。
- 优化:**路径压缩(Path Compression)**——在 `Find` 操作时,将访问路径上的所有节点直接连接到根节点,减少后续查询的层数。
- (3)**合并**
- 操作:合并两棵树,**将一棵树的根节点连接至另一棵树**。
- 优化:**按秩合并(Union by Rank)**——在 `Union` 操作时,总是让 **高度较小的树** 挂到 **高度较大的树** 上,以保持树的高度尽可能小。
> [!caution] "**按秩合并**" 只是 "**尝试合并出最矮的树**" ,并不保证一定最矮。
>
> - **秩**有两种定义:**树的深度**、**树的节点**,取其一即可。
> - 秩代表的 "==**深度**==" 是 "**==深度上界==**" 而不是准确值,因为在路径压缩过程中,树的深度可能会被改变
实现:
```cpp
class UnionFind {
public:
// 初始化并查集
// 初始时, 每个元素均作为一个独立集合(对应一棵独立的树, 只有根节点), 初始化父节点为其自身.
UnionFind(size_t n) : parent(n), rank(n, 1) {
std::iota(parent.begin(), parent.end(), 0); // parent[i] = i;
}
// 查找 (带路径压缩): 自给定节点起向上移动, 直至根节点.
// 路径压缩: 查询过程中经过的每个元素都属于该集合, 将其直连至根节点, 加快后续查询
size_t find(size_t x) {
if (parent[x] != x) {
parent[x] = find(parent[x]); // 路径压缩.
}
return parent[x];
}
// 合并
// 按秩合并: 将"高度较小的树"挂到"高度较大的树"
void unite(size_t x, size_t y) {
x = find(x), y = find(y);
if (x == y) return;
if (rank[x] < rank[y]) std::swap(x, y); // 令y是小树,将y挂到x.
parent[y] = x;
++rank[y];
}
// 判断两元素是否属于同一集合
bool connected(size_t x, size_t y) {
return find(x) == find(y);
}
private:
std::vector<size_t> parent; // 记录每个元素的父节点
std::vector<size_t> rank; // 记录树的秩(深度), 用于按秩合并.
};
```
<br><br>
# 参考资料
- [01. 并查集知识 \| 算法通关手册(LeetCode)](https://algo.itcharge.cn/07.Tree/05.Union-Find/01.Union-Find/#_6-2-%E7%9C%81%E4%BB%BD%E6%95%B0%E9%87%8F)
- [并查集 - OI Wiki](https://oi-wiki.org/ds/dsu/)
- [leetcode.cn/circle/discuss/qmjuMW/](https://leetcode.cn/circle/discuss/qmjuMW/)
# Footnotes
[^1]: [并查集 - OI Wiki](https://oi-wiki.org/ds/dsu/)