并查集¶
概述¶
并查集(Disjoint Set Union,DSU)是一种树形数据结构,专门用于处理不相交集合的合并与查询问题。它支持高效判断元素是否属于同一集合,是解决动态连通性问题的利器。
核心思想:将每个集合表示为一棵树,树的根节点作为集合的唯一标识。通过查找根节点判断元素所属集合,通过连接两棵树的根实现集合合并。
并查集特点¶
| 特性 | 说明 |
|---|---|
| 集合表示 | 每个集合用一棵树表示,根节点为集合代表 |
| 查找操作 | 找到元素所在集合的根(代表元素) |
| 合并操作 | 将两个集合合并为一个集合 |
| 路径压缩 | 查找时将路径上所有节点直接连到根 |
| 按秩合并 | 合并时将矮树挂到高树下 |
数据结构可视化¶
初始状态¶
每个元素自成一个集合,父指针指向自己:
数组表示:
| 索引 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
|---|---|---|---|---|---|---|---|---|
| parent | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
| rank | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
树形结构:每个元素独立,自成一棵单节点树
合并操作可视化¶
执行合并操作 unite(0, 1), unite(2, 3), unite(4, 5):
graph TB
subgraph 集合A
n0((0)) --> n1((1))
end
subgraph 集合B
n2((2)) --> n3((3))
end
subgraph 集合C
n4((4)) --> n5((5))
end
subgraph 独立集合
n6((6))
n7((7))
end
style n0 fill:#4CAF50,color:#fff
style n2 fill:#2196F3,color:#fff
style n4 fill:#FF9800,color:#fff
style n6 fill:#9E9E9E,color:#fff
style n7 fill:#9E9E9E,color:#fff
路径压缩原理¶
查找元素时,将路径上所有节点直接连接到根节点:
graph TB
subgraph 压缩前
r1((0)) --> a1((1))
a1 --> b1((2))
b1 --> c1((3))
c1 --> d1((4))
end
subgraph 压缩后_find_4
r2((0)) --> a2((1))
r2 --> b2((2))
r2 --> c2((3))
r2 --> d2((4))
end
style r1 fill:#4CAF50,color:#fff
style r2 fill:#4CAF50,color:#fff
style d1 fill:#FF5722,color:#fff
style d2 fill:#FF5722,color:#fff
路径压缩效果:查找节点 4 后,节点 1、2、3、4 都直接连接到根节点 0,后续查找这些节点的时间复杂度降为 O(1)。
按秩合并原理¶
合并时将秩(树高)较小的树挂到秩较大的树下:
graph TB
subgraph 合并前
subgraph 树A_秩为2
rootA((0)) --> a1((1))
rootA --> a2((2))
end
subgraph 树B_秩为1
rootB((3)) --> b1((4))
end
end
subgraph 合并后
root((0)) --> c1((1))
root --> c2((2))
root --> c3((3))
c3 --> c4((4))
end
style rootA fill:#4CAF50,color:#fff
style rootB fill:#2196F3,color:#fff
style root fill:#4CAF50,color:#fff
基本实现¶
数据结构定义¶
| C | |
|---|---|
初始化¶
| C | |
|---|---|
基本查找(无优化)¶
| C | |
|---|---|
基本合并¶
| C | |
|---|---|
连通性判断¶
路径压缩优化¶
递归实现¶
| C | |
|---|---|
执行过程示例(查找节点 4):
初始状态:parent = [0, 0, 1, 2, 3] (链状结构 0←1←2←3←4)
递归调用栈:
findCompressed(4)
→ findCompressed(3) // parent[4]=3
→ findCompressed(2) // parent[3]=2
→ findCompressed(1) // parent[2]=1
→ findCompressed(0) // parent[1]=0
→ 返回 0
→ parent[1] = 0, 返回 0
→ parent[2] = 0, 返回 0
→ parent[3] = 0, 返回 0
→ parent[4] = 0, 返回 0
最终状态:parent = [0, 0, 0, 0, 0]
所有节点直接连到根,路径压缩完成
迭代实现(两次遍历)¶
| C | |
|---|---|
按秩合并优化¶
按秩合并过程示例:
初始:两个秩为1的树
0 3 | | 1 4
合并 uniteByRank(0, 3):
• rank[0] = 1, rank[3] = 1 (秩相等)
• parent[3] = 0 (将树3挂到树0下)
• rank[0]++ = 2 (根的秩加1)
结果:
0
/ \
1 3
|
4
C++ 实现¶
操作过程可视化¶
完整操作流程¶
flowchart TD
A[初始状态: 8个独立元素] --> B[unite 0,1]
B --> C[unite 2,3]
C --> D[unite 4,5]
D --> E[unite 6,7]
E --> F[unite 0,2]
F --> G[unite 4,6]
G --> H[unite 0,4]
style A fill:#E3F2FD
style H fill:#E8F5E9
操作后数据状态¶
操作序列:unite(0,1), unite(2,3), unite(0,2), unite(4,5), unite(0,4)
最终状态:
| 数组 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
|---|---|---|---|---|---|---|---|---|
| parent | 0 | 0 | 0 | 2 | 0 | 4 | 4 | 6 |
| rank | 2 | 0 | 1 | 0 | 1 | 0 | 0 | 0 |
树形结构:
0
/ | \
1 2 4
| |
3 5
说明:
• 节点 0 是根,秩为 2
• 节点 1, 2, 4 直接连接到 0
• 节点 3 连接到 2,节点 5 连接到 4
应用场景¶
1. 连通分量计数¶
| C | |
|---|---|
连通分量可视化:
graph TB
subgraph 连通分量1
a((0)) --- b((1))
b --- c((2))
end
subgraph 连通分量2
d((3)) --- e((4))
end
subgraph 连通分量3
f((5))
end
style a fill:#4CAF50,color:#fff
style d fill:#2196F3,color:#fff
style f fill:#FF9800,color:#fff
2. 环检测¶
| C | |
|---|---|
环检测过程:
flowchart LR
A[边 0-1] --> B[合并 0,1]
B --> C[边 1-2]
C --> D[合并 1,2]
D --> E[边 2-0]
E --> F{find 0 == find 2?}
F -->|是| G[检测到环!]
style G fill:#FF5722,color:#fff
3. Kruskal 最小生成树¶
Kruskal算法过程:
flowchart TD
A[按边权排序] --> B[初始化并查集]
B --> C{遍历每条边}
C --> D{边的两端点连通?}
D -->|否| E[加入MST]
E --> F[合并两个集合]
F --> G{边数 = n-1?}
D -->|是| H[跳过该边]
H --> C
G -->|否| C
G -->|是| I[输出MST]
style E fill:#4CAF50,color:#fff
style I fill:#E8F5E9
4. 带权并查集¶
用于处理节点间存在权值关系的问题(如食物链、差分约束):
时间复杂度分析¶
| 操作 | 无优化 | 仅路径压缩 | 仅按秩合并 | 路径压缩+按秩合并 |
|---|---|---|---|---|
| 查找 | O(n) | O(log n) | O(log n) | O(α(n)) |
| 合并 | O(n) | O(log n) | O(log n) | O(α(n)) |
| 初始化 | O(n) | O(n) | O(n) | O(n) |
反阿克曼函数 α(n):增长极其缓慢的函数,对于实际应用中任何可能的 n 值,α(n) ≤ 4。因此,同时使用路径压缩和按秩合并的并查集,其操作时间复杂度在实际应用中可视为常数 O(1)。
复杂度对比¶
假设:操作次数 m = 10⁶, 元素个数 n = 10⁶
| 优化策略 | 时间复杂度 | 实际耗时 | 评估 |
|---|---|---|---|
| 无优化 | O(m × n) | 10¹² | 超时 |
| 仅路径压缩 | O(m × log n) | ≈ 2×10⁷ | 可接受 |
| 路径压缩+按秩合并 | O(m × α(n)) | ≈ 4×10⁶ | 极快 |
空间复杂度¶
| 实现 | 空间复杂度 | 说明 |
|---|---|---|
| 基本实现 | O(n) | 仅需 parent 数组 |
| 按秩合并 | O(n) | 额外需要 rank 数组 |
| 带权并查集 | O(n) | 额外需要 weight 数组 |
优化策略对比¶
graph TB
A[并查集优化策略]
A --> B[路径压缩]
A --> C[按秩合并]
B --> B1[查找时优化]
B --> B2[将路径节点连到根]
B --> B3[摊还分析有效]
C --> C1[合并时优化]
C --> C2[矮树挂到高树下]
C --> C3[保持树平衡]
style A fill:#2196F3,color:#fff
style B fill:#4CAF50,color:#fff
style C fill:#FF9800,color:#fff
最佳实践:同时使用路径压缩和按秩合并,可以达到近乎线性的时间复杂度 O(m·α(n)),这是并查集的标准高效实现。
典型应用场景总结¶
| 应用场景 | 描述 | 时间复杂度 |
|---|---|---|
| 连通分量 | 图的连通分量计数 | O(E·α(V)) |
| 最小生成树 | Kruskal 算法 | O(E·logE) |
| 环检测 | 判断无向图是否有环 | O(E·α(V)) |
| 等价类 | 划分等价关系 | O(n·α(n)) |
| 社交网络 | 朋友圈、群组划分 | O(n·α(n)) |
| 电网连接 | 动态连通性维护 | O(Q·α(n)) |
| 食物链问题 | 带权并查集应用 | O(n·α(n)) |
实际问题示例¶
LeetCode 547. 省份数量¶
| C++ | |
|---|---|
LeetCode 200. 岛屿数量¶
参考资料¶
- 《算法导论》第21章:用于不相交集合的数据结构
- 《数据结构与算法分析》第8章:不相交集合
- Tarjan, R. E. (1975). Efficiency of a Good But Not Linear Set Union Algorithm