
并查集(Union-Find)是一种用于处理动态连通性问题的数据结构。它通常用来处理像网络连通性、集合合并等问题。并查集操作高效,尤其适用于需要频繁进行合并和查询的情景,具有接近常数时间复杂度。
以下是对并查集的详细解释,包括它的基本概念、实现步骤,以及C++代码示例。
1.并查集的基本概念
并查集的主要功能有两个:
- 查找(Find):确定某个元素属于哪个集合。
- 合并(Union):将两个元素所在的集合合并为一个集合。
并查集通常通过树结构实现,每个元素都指向它的父节点,通过这种方式可以追溯到树的根节点。
- 父节点(Parent):并查集中每个元素有一个指向其父节点的指针。
- 根节点(Root):在并查集中,一个集合可以看作是一棵树的形式,根节点是整个树的代表元素。
优化策略
为了提高并查集操作的效率,通常会使用以下两个重要的优化技巧:
- 路径压缩(Path Compression):在执行查找操作时,将访问的节点直接连接到根节点,以减少树的深度。
- 按秩合并(Union by Rank 或 Union by Size):在合并两个集合时,将规模较小的集合连接到规模较大的集合下,避免形成深树。
2.实现步骤
1. 初始化
- 使用一个数组
parent
来表示每个元素的父节点,初始时每个元素都是自己的父节点。
- 使用一个数组
rank
来记录每棵树的高度,以便于在合并时按秩合并。
2. 查找(Find)
- 查找时,通过递归找到根节点,并在路径上将所有节点直接连接到根节点(路径压缩)。
3. 合并(Union)
- 合并时,通过比较两个集合的秩来决定将哪个集合连接到另一个集合上(按秩合并)。
C++代码实现
基础班
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| class UnionFind { public: UnionFind(int n) { parent.resize(n); for (int i = 0; i < n; ++i) { parent[i] = i; } }
int find(int x) { while (x != parent[x]) { x = parent[x]; } return x; }
void unionSets(int x, int y) { int rootX = find(x); int rootY = find(y); if (rootX != rootY) { parent[rootY] = rootX; } }
bool isConnected(int x, int y) { return find(x) == find(y); }
private: vector<int> parent; };
|
优化版
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70
| #include <iostream> #include <vector>
using namespace std;
class UnionFind { public: UnionFind(vector<int> nums) { for (int i = 0; i < nums.size(); ++i) { parent[nums[i]] = nums[i]; rank[nums[i]]=1; } }
int find(int x) { if (parent[x] != x) { parent[x] = find(parent[x]); } return parent[x]; }
void unionSets(int x, int y) { int rootX = find(x); int rootY = find(y);
if (rootX != rootY) { if (rank[rootX] > rank[rootY]) { parent[rootY] = rootX; } else if (rank[rootX] < rank[rootY]) { parent[rootX] = rootY; } else { parent[rootY] = rootX; rank[rootX]++; } } }
bool isConnected(int x, int y) { return find(x) == find(y); }
private: unordered_map<int,int> parent; unordered_map<int,int> rank; };
int main() { vector<int>nums={0,1,2,3,4,5,6,7,8,9} UnionFind uf(nums);
uf.unionSets(1, 2); uf.unionSets(2, 3); uf.unionSets(4, 5); uf.unionSets(6, 7); uf.unionSets(3, 7);
cout << "1 和 7 是否连通: " << (uf.isConnected(1, 7) ? "是" : "否") << endl; cout << "4 和 6 是否连通: " << (uf.isConnected(4, 6) ? "是" : "否") << endl;
return 0; }
|
代码讲解
- 类定义和初始化:
UnionFind(int n)
: 初始化大小为 n
的并查集,每个节点都是自己的父节点,初始秩为1。
parent
数组用于存储每个节点的父节点。
rank
数组用于存储树的高度,优化合并。
- 查找操作:
find(int x)
: 使用递归实现查找并进行路径压缩。路径压缩将每次查找的节点直接连接到根节点,显著减少后续查找操作的时间。
- 合并操作:
unionSets(int x, int y)
: 查找 x
和 y
的根节点,如果根节点不同则按秩合并。将高度较小的树连接到较大的树上,保证树的高度尽量保持较低。
- 检查连通性:
isConnected(int x, int y)
: 判断 x
和 y
是否属于同一个集合,通过比较它们的根节点是否相同来实现。
时间复杂度
- 查找(Find):通过路径压缩,Find 操作的时间复杂度接近常数,可以表示为
O(α(n))
,其中 α(n)
为反阿克曼函数,增长极为缓慢,几乎可以认为是常数。
- 合并(Union):按秩合并使得合并操作的时间复杂度也是接近常数,同样为
O(α(n))
。
因此,并查集适用于频繁的合并和查找操作场景,其操作复杂度非常低。
适用场景
- 网络连通性:检查网络中的节点是否连通。
- 最小生成树算法:例如 Kruskal 算法,用于合并不同的边并检查环。
- 动态连通性:处理一些动态添加、删除边的连通性问题。
3.相关例题
力扣128.最长连续序列