并查集(Union-Find)算法详解
一、并查集的应用场景
🎯 主要应用领域
- 动态连通性问题:判断两个元素是否连通
- 集合合并与查询:高效的集合操作
- 网络连接检测:计算机网络、社交网络
- 图论算法:Kruskal最小生成树算法
- 图像处理:连通区域标记
💡 典型问题
- 朋友圈问题
- 家庭关系合并
- 网络连通性检测
- 岛屿数量问题
- 最小生成树
二、并查集的原理
🔍 核心思想
树形结构表示集合:每个集合用一棵树表示,树根作为集合的代表元素。
📐 算法特性
- 使用父指针数组表示树结构
- 支持两种核心操作:查找(Find)和合并(Union)
- 通过优化达到接近常数时间复杂度
三、通用模板
// 并查集数据结构
class UnionFind {
private:
vector<int> parent; // 父节点数组
vector<int> rank; // 秩数组(用于按秩合并)
int count; // 连通分量数量
UnionFind(int n) {
count = n;
parent.resize(n);
rank.resize(n, 0); // 初始秩都为0
// 每个元素的父节点指向自己
for (int i = 0; i < n; i++) {
parent[i] = i;
}
}
// 2. 查找操作:找到元素x的根节点(带路径压缩)
int find(int x) {
// 2.1 如果x不是根节点,递归查找并压缩路径
if (parent[x] != x) {
parent[x] = find(parent[x]); // 路径压缩
}
// 2.2 返回根节点
return parent[x];
}
// 2. 查找操作,循环搜索
int find(int x) {
while(x != father[x]) {
x = father[x];
}
return x;
}
// 3. 合并操作:将元素x和y所在的集合合并(按秩合并)
void unionSet(int x, int y) {
// 3.1 找到x和y的根节点
int rootX = find(x);
int rootY = find(y);
// 3.2 如果已经在同一集合,直接返回
if (rootX == rootY) return;
// 3.3 按秩合并:将秩小的树合并到秩大的树下
if (rank[rootX] < rank[rootY]) {
parent[rootX] = rootY;
} else if (rank[rootX] > rank[rootY]) {
parent[rootY] = rootX;
} else {
// 秩相等时,任意合并,并增加秩
parent[rootY] = rootX;
rank[rootX]++;
}
// 3.4 连通分量数量减1
count--;
}
// 4. 判断连通性:检查x和y是否属于同一集合
bool connected(int x, int y) {
return find(x) == find(y);
}
// 5. 获取连通分量数量
int getCount() {
return count;
}
};
// 使用示例:
void main() {
// 初始化并查集
UnionFind uf(n);
// 处理连接关系
for (每条边 (u, v)) {
if (!uf.connected(u, v)) {
uf.unionSet(u, v);
}
}
// 获取结果
int components = uf.getCount();
}
四、并查集的实现步骤
🚀 详细步骤
- 初始化:每个元素独立成集合,父指针指向自己
- 查找操作:
- 递归找到根节点
- 路径压缩优化
- 合并操作:
- 找到两个元素的根节点
- 按秩合并优化
- 连通性判断:比较根节点是否相同
📝 关键要点
- 路径压缩:让查找路径上的节点直接指向根节点
- 按秩合并:将矮树合并到高树下,保持树平衡
- 代表元素:每个集合用最小元素或任意元素作为代表
五、代码示例(C++实现)
示例1:基础并查集
#include <iostream>
#include <vector>
using namespace std;
class UnionFind {
private:
vector<int> parent;
public:
UnionFind(int n) : parent(n) {
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 unionSet(int x, int y) {
int rootX = find(x);
int rootY = find(y);
if (rootX != rootY) {
parent[rootY] = rootX;
}
}
bool connected(int x, int y) {
return find(x) == find(y);
}
};
示例2:统计集合数量
int countComponents(int n, vector<vector<int>>& edges) {
UnionFind uf(n);
for (auto& edge : edges) {
uf.unionSet(edge[0], edge[1]);
}
// 统计根节点数量
int count = 0;
for (int i = 0; i < n; i++) {
if (uf.find(i) == i) {
count++;
}
}
return count;
}
六、时间复杂度
| 操作 | 基础版本 | 优化版本 |
|---|---|---|
| 初始化 | O(n) | O(n) |
| 查找(Find) | O(h) | O(α(n)) |
| 合并(Union) | O(h) | O(α(n)) |
其中:
- h是树的高度
- α(n)是反阿克曼函数,增长极慢,可视为常数
七、并查集与BFS、DFS的对比
| 特性 | 并查集 | BFS | DFS |
|---|---|---|---|
| 主要用途 | 动态连通性 | 最短路径、遍历 | 路径搜索、回溯 |
| 数据结构 | 父指针数组 | 队列 | 栈/递归 |
| 时间复杂度 | O(α(n)) | O(V+E) | O(V+E) |
| 空间复杂度 | O(n) | O(V) | O(h) |
| 适用场景 | 集合操作 | 层次遍历 | 深度搜索 |
八、总结
✨ 核心优势
- 高效性:接近常数时间的操作
- 简洁性:代码实现简单
- 实用性:解决多种连通性问题
🎯 适用场景选择
- 使用并查集:动态连通性、集合合并
- 使用BFS:最短路径、层次遍历
- 使用DFS:路径存在性、回溯问题
💡 学习建议
- 掌握基础版本,理解核心思想
- 学习优化技术,理解其原理
- 多做练习,熟悉各种应用场景
- 对比不同算法,选择合适的解决方案
并查集是解决连通性问题的利器,在算法竞赛和工程实践中都有广泛应用!
21万+

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



