并查集(Union-Find),又称不相交集合(Disjoint Set),是一种高效处理集合合并与元素归属查询问题的数据结构。它在图论、网络连接分析、动态连通性判断等领域有广泛应用。
一、核心问题与基本功能
并查集主要解决以下两类问题:
- 合并(Union):将两个集合合并成一个集合。
- 查询(Find):判断两个元素是否属于同一个集合。
这类问题常见于:
- 判断图中两个节点是否连通
- Kruskal算法中避免生成环
- 社交网络中判断两人是否在同一个“朋友圈”
- 迷宫连通性判断
二、基本原理
1. 数据结构思想
并查集使用森林(多棵树)来表示多个集合,每棵树代表一个集合。每个节点存储其父节点的索引。
- 根节点:代表整个集合,其父节点指向自己。
- 查找操作:从一个节点出发,不断向上找父节点,直到找到根节点。
- 合并操作:将一棵树的根节点连接到另一棵树的根节点上。
2. 初始状态
假设有n个元素(编号1~n),初始时每个元素自成一个集合:
for (int i = 1; i <= n; i++) {
parent[i] = i; // 每个节点的父节点是自己
}
三、核心优化技术
1. 路径压缩(Path Compression)
在find操作过程中,将沿途经过的所有节点直接连接到根节点,从而“压缩”路径。
作用:显著降低树的高度,使后续查询接近O(1)。
int find(int x) {
if (parent[x] != x) {
parent[x] = find(parent[x]); // 递归压缩路径
}
return parent[x];
}
2. 按秩合并(Union by Rank)
在合并时,将秩较小的树合并到秩较大的树上。秩(rank)可以理解为树高度的上界。
作用:防止树退化为链表,保持结构平衡。
void unionElements(int x, int y) {
int rootX = find(x);
int rootY = find(y);
if (rootX == rootY) return;
if (rank[rootX] < rank[rootY]) {
parent[rootX] = rootY;
} else if (rank[rootX] > rank[rootY]) {
parent[rootY] = rootX;
} else {
parent[rootY] = rootX;
rank[rootX]++;
}
}
四、完整C++实现
#include <iostream>
#include <vector>
using namespace std;
class UnionFind {
private:
vector<int> parent; // 父节点数组
vector<int> rank; // 秩数组
int count; // 当前集合数量
public:
// 构造函数:初始化n个独立集合
UnionFind(int n) {
count = n;
parent.resize(n);
rank.resize(n, 0);
for (int i = 0; i < n; i++) {
parent[i] = i; // 每个元素自成一集合
}
}
// 查找根节点(带路径压缩)
int find(int x) {
if (parent[x] != x) {
parent[x] = find(parent[x]); // 路径压缩
}
return parent[x];
}
// 合并两个集合(按秩合并)
void unionElements(int x, int y) {
int rootX = find(x);
int rootY = find(y);
if (rootX == rootY) return; // 已在同一集合
// 按秩合并
if (rank[rootX] < rank[rootY]) {
parent[rootX] = rootY;
} else if (rank[rootX] > rank[rootY]) {
parent[rootY] = rootX;
} else {
parent[rootY] = rootX;
rank[rootX]++;
}
count--;
}
// 判断两个元素是否在同一集合
bool isConnected(int x, int y) {
return find(x) == find(y);
}
// 获取当前集合数量
int getCount() const {
return count;
}
};
// 使用示例
int main() {
UnionFind uf(5); // 5个元素:0,1,2,3,4
uf.unionElements(0, 1);
uf.unionElements(1, 2);
uf.unionElements(3, 4);
cout << "0和2连通: " << (uf.isConnected(0, 2) ? "Yes" : "No") << endl; // Yes
cout << "0和3连通: " << (uf.isConnected(0, 3) ? "Yes" : "No") << endl; // No
cout << "当前集合数: " << uf.getCount() << endl; // 2
return 0;
}
五、扩展功能:维护集合大小
有时需要查询某个集合中元素的数量,可通过维护size数组实现:
class UnionFindWithSize {
private:
vector<int> parent;
vector<int> size; // 记录每个集合的元素数量
public:
UnionFindWithSize(int n) {
parent.resize(n);
size.resize(n, 1); // 初始每个集合大小为1
for (int i = 0; i < n; i++) parent[i] = i;
}
int find(int x) {
if (parent[x] != x) {
parent[x] = find(parent[x]);
}
return parent[x];
}
void unionElements(int x, int y) {
int rootX = find(x);
int rootY = find(y);
if (rootX == rootY) return;
// 将较小集合合并到较大集合(按大小合并)
if (size[rootX] < size[rootY]) {
parent[rootX] = rootY;
size[rootY] += size[rootX];
} else {
parent[rootY] = rootX;
size[rootX] += size[rootY];
}
}
int getSize(int x) {
return size[find(x)];
}
};
六、时间复杂度分析
- 单次操作时间复杂度:接近 O(α(n)),其中 α(n) 是反阿克曼函数。
- 对于实际应用中的 n(如 n ≤ 1e9),α(n) ≤ 5,因此可视为常数时间。
七、典型应用场景
- Kruskal最小生成树算法:判断加边是否形成环
- 图的连通分量计算
- 动态连通性问题
- 朋友圈问题(LeetCode 547)
- 岛屿数量问题(并查集解法)
并查集以其实现简洁、效率极高的特点,成为算法竞赛和工程实践中不可或缺的工具之一。
1856

被折叠的 条评论
为什么被折叠?



