文章目录
- 题型:并查集(Union-Find)
-
- [1. 核心思路](#1. 核心思路)
-
- [1.1 适用场景](#1.1 适用场景)
- [1.2 核心思想](#1.2 核心思想)
- [1.3 优化策略:路径压缩](#1.3 优化策略:路径压缩)
- [2. 模板](#2. 模板)
-
- [2.1 简化版本(函数式)](#2.1 简化版本(函数式))
- [2.2 时间复杂度](#2.2 时间复杂度)
- [3. 常见变形](#3. 常见变形)
-
- [3.1 统计连通分量数量](#3.1 统计连通分量数量)
- [3.2 按秩合并(可选优化)](#3.2 按秩合并(可选优化))
- [3.3 带权并查集](#3.3 带权并查集)
- [4. 典型应用场景](#4. 典型应用场景)
- [5. 并查集 vs DFS/BFS](#5. 并查集 vs DFS/BFS)
题型:并查集(Union-Find)
1. 核心思路
并查集是一种树型的数据结构,用于处理一些不交集的合并及查询问题。
1.1 适用场景
- 判断两个元素是否在同一个集合中
- 将两个元素合并到同一个集合中
- 统计连通分量的数量
1.2 核心思想
- 每个集合用一棵树表示,树的根节点作为该集合的代表
- 通过查找根节点来判断两个元素是否属于同一集合
- 通过合并根节点来合并两个集合
示例:
初始状态:每个元素都是独立的集合
A, B, C, D, E
合并A和B: 合并C和D:
A C
| |
B D
查找过程:A和B的根都是A → 在同一集合
C和D的根都是C → 在同一集合
E的根是E → 独立集合
1.3 优化策略:路径压缩
- 问题:树可能退化成链,查找根节点需要O(n)时间
- 解决:在查找过程中,将所有节点直接连接到根节点
- 效果:第一次查找O(logn),后续查找接近O(1)
路径压缩示例:
压缩前(链状结构): 压缩后(扁平结构):
A A
| / | \
B B C D
|
C
|
D
2. 模板
cpp
class UnionFind {
private:
vector<int> father; // 父节点数组
public:
// 初始化:每个节点的父节点都是自己
UnionFind(int n) {
father.resize(n);
for (int i = 0; i < n; i++) {
father[i] = i;
}
}
// 查找根节点(带路径压缩)
int find(int u) {
// 如果u的父节点不是自己,说明u不是根节点
// 递归查找根节点,并将路径上的所有节点直接连接到根节点
return father[u] == u ? u : father[u] = find(father[u]);
}
// 判断两个节点是否在同一集合
bool isSame(int u, int v) {
return find(u) == find(v);
}
// 合并两个节点所在的集合
void join(int u, int v) {
u = find(u); // 找到u的根节点
v = find(v); // 找到v的根节点
if (u == v) return; // 如果根相同,说明已经在同一集合,直接返回
father[v] = u; // 将v的根节点指向u的根节点
}
};
2.1 简化版本(函数式)
cpp
int n = 1000;
vector<int> father(n);
// 初始化
void init() {
for (int i = 0; i < n; i++) {
father[i] = i;
}
}
// 查找根节点(路径压缩)
int find(int u) {
return father[u] == u ? u : father[u] = find(father[u]);
}
// 判断是否在同一集合
bool isSame(int u, int v) {
return find(u) == find(v);
}
// 合并集合
void join(int u, int v) {
u = find(u);
v = find(v);
if (u == v) return;
father[v] = u;
}
2.2 时间复杂度
- 初始化:O(n)
- 查找(路径压缩后):接近O(1),实际为O(α(n)),α是阿克曼函数的反函数
- 合并:接近O(1)
- 总体:对于n次操作,时间复杂度接近O(n)
3. 常见变形
3.1 统计连通分量数量
cpp
int countComponents(int n, vector<vector<int>>& edges) {
UnionFind uf(n);
for (auto& edge : edges) {
uf.join(edge[0], edge[1]);
}
// 统计根节点数量(根节点的father[i] == i)
int count = 0;
for (int i = 0; i < n; i++) {
if (uf.find(i) == i) {
count++;
}
}
return count;
}
3.2 按秩合并(可选优化)
在路径压缩的基础上,还可以使用按秩合并来进一步优化:
cpp
class UnionFind {
private:
vector<int> father;
vector<int> rank; // 记录树的深度(秩)
public:
UnionFind(int n) {
father.resize(n);
rank.resize(n, 0);
for (int i = 0; i < n; i++) {
father[i] = i;
}
}
int find(int u) {
return father[u] == u ? u : father[u] = find(father[u]);
}
void join(int u, int v) {
u = find(u);
v = find(v);
if (u == v) return;
// 按秩合并:将秩小的树合并到秩大的树下
if (rank[u] < rank[v]) {
father[u] = v;
} else if (rank[u] > rank[v]) {
father[v] = u;
} else {
father[v] = u;
rank[u]++; // 秩相同时,合并后深度+1
}
}
};
3.3 带权并查集
记录节点到根节点的距离或关系:
cpp
class WeightedUnionFind {
private:
vector<int> father;
vector<int> weight; // 记录到根节点的权重
public:
WeightedUnionFind(int n) {
father.resize(n);
weight.resize(n, 0);
for (int i = 0; i < n; i++) {
father[i] = i;
}
}
int find(int u) {
if (father[u] != u) {
int root = find(father[u]);
weight[u] += weight[father[u]]; // 更新权重
father[u] = root;
}
return father[u];
}
void join(int u, int v, int w) {
int rootU = find(u);
int rootV = find(v);
if (rootU == rootV) return;
father[rootV] = rootU;
weight[rootV] = weight[u] - weight[v] + w;
}
};
4. 典型应用场景
-
连通性问题
- LeetCode 547. 省份数量
- LeetCode 200. 岛屿数量(也可用DFS/BFS)
- LeetCode 130. 被围绕的区域
-
朋友圈/社交网络
- 判断两个人是否在同一个朋友圈
- 统计朋友圈数量
-
最小生成树(Kruskal算法)
- 使用并查集判断边是否会形成环
- LeetCode 1584. 连接所有点的最小费用
-
等价关系判断
- 判断两个变量是否等价
- 字符串相似性判断
-
动态连通性
- 实时添加边,判断连通性
- 网络连接问题
5. 并查集 vs DFS/BFS
| 比较项 | 并查集 | DFS/BFS |
|---|---|---|
| 适用场景 | 动态合并集合、判断连通性 | 遍历图、查找路径 |
| 时间复杂度 | 接近O(1) | O(V+E) |
| 空间复杂度 | O(V) | O(V) |
| 优势 | 合并和查询操作高效 | 可以遍历所有节点、找路径 |
| 劣势 | 不能遍历图结构 | 每次查询需要重新遍历 |
选择建议:
- 需要频繁合并集合、判断连通性 → 并查集
- 需要遍历图、找路径、统计连通块 → DFS/BFS