一文详解并查集:从基础原理到高级应用
一文详解并查集:从基础原理到高级应用
- 前言
- 一、基本概念
- 1.1 定义与作用
- 1.2 直观理解
- 二、并查集的基本实现
- 2.1 数据结构定义
- 2.2 查找操作实现
- 2.3 合并操作实现
- 三、经典优化策略
- 3.1 路径压缩(Path Compression)
- 3.2 按秩合并(Union by Rank)
- 四、经典应用案例
- 4.1 岛屿数量问题(LeetCode 200)
- 4.2 冗余连接问题(LeetCode 684)
- 4.3 Kruskal 算法求最小生成树
- 五、并查集扩展与高级应用
- 5.1 带权并查集
- 5.2 扩展域并查集
- 5.3 动态维护连通性
- 总结
前言
处理集合相关的问题时,我们常常需要高效地执行合并集合与查询元素所属集合的操作。并查集(Disjoint Set Union,DSU)作为一种专门为此设计的数据结构,以其简洁的实现和出色的性能,在图论算法、动态连通性问题、最小生成树算法等众多场景中发挥着关键作用。本文我将带你深入探讨并查集的基本概念、实现原理、优化策略、经典应用案例,并结合丰富的代码示例,全面剖析这一强大的数据结构。
一、基本概念
1.1 定义与作用
并查集是一种用于管理一系列不相交集合的数据结构,支持两种核心操作:
-
合并(Union):将两个不相交的集合合并为一个集合。
-
查找(Find):查询某个元素属于哪个集合,即找到该元素所在集合的代表元素(通常称为根节点)。
并查集的主要作用在于高效地处理动态连通性问题。例如,在社交网络中,判断两个用户是否属于同一个朋友圈;在地图导航中,确定不同地点之间是否存在连通路径等。
1.2 直观理解
可以将并查集想象成一片由多棵树组成的森林,每棵树代表一个集合,树中的节点对应集合中的元素,树的根节点作为集合的代表。查找操作就是找到某个节点所在树的根节点,合并操作则是将两棵树合并为一棵。例如,有三个集合 {1, 2}
、{3}
、{4, 5}
,分别对应三棵树,当执行 Union(2, 3)
操作后,{1, 2}
和 {3}
两个集合合并,对应的两棵树也合并成一棵新树。
二、并查集的基本实现
2.1 数据结构定义
最基础的并查集可以使用数组来实现,数组的下标表示元素,数组对应位置的值表示该元素的父节点。如果一个元素的父节点是它自身,那么该元素就是所在集合的根节点。以 C++ 代码为例:
#include <vector>
using namespace std;class UnionFind {
private:vector<int> parent;
public:UnionFind(int n) {parent.resize(n);for (int i = 0; i < n; ++i) {parent[i] = i; // 初始化时,每个元素自成一个集合,父节点为自身}}
};
2.2 查找操作实现
查找操作的目的是找到某个元素所在集合的根节点,通过不断向上追溯父节点,直到找到根节点(即父节点为自身的节点)。
int find(int x) {while (x != parent[x]) {x = parent[x];}return x;
}
上述代码中,find
函数通过一个循环,不断将 x
更新为其父节点,直到 x
等于 parent[x]
,此时 x
即为根节点。
2.3 合并操作实现
合并操作是将两个不同集合合并为一个集合,通常的做法是将其中一个集合的根节点作为另一个集合根节点的子节点。
void unionSet(int x, int y) {int rootX = find(x);int rootY = find(y);if (rootX != rootY) {parent[rootX] = rootY; // 将 rootX 所在的树连接到 rootY 所在的树}
}
在 unionSet
函数中,先分别找到 x
和 y
所在集合的根节点 rootX
和 rootY
,如果它们不相同,就将 rootX
的父节点设置为 rootY
,从而实现两个集合的合并。
三、经典优化策略
3.1 路径压缩(Path Compression)
路径压缩的目的是在查找操作时,将路径上的所有节点直接连接到根节点,从而降低树的高度,提高后续查找操作的效率。优化后的查找函数如下:
int find(int x) {if (x != parent[x]) {parent[x] = find(parent[x]); // 递归查找并压缩路径}return parent[x];
}
在这个版本的 find
函数中,当 x
不是根节点时,通过递归调用 find
函数,将 x
的父节点更新为根节点,这样下次查找 x
或其路径上的节点时,就能直接找到根节点,大大减少查找时间。
3.2 按秩合并(Union by Rank)
按秩合并是指在合并操作时,将秩(可以理解为树的高度或节点数量)较小的树合并到秩较大的树下面,以避免树的高度过快增长。为了实现按秩合并,需要额外维护一个数组 rank
来记录每个根节点所在树的秩。
class UnionFind {
private:vector<int> parent;vector<int> rank;
public:UnionFind(int n) {parent.resize(n);rank.resize(n, 1); // 初始化时,每个元素所在树的秩为 1for (int i = 0; i < n; ++i) {parent[i] = i;}}int find(int x) {if (x != parent[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) {if (rank[rootX] < rank[rootY]) {parent[rootX] = rootY;} else if (rank[rootX] > rank[rootY]) {parent[rootY] = rootX;} else {parent[rootY] = rootX;rank[rootX]++; // 当两棵树秩相同时,合并后根节点的秩加 1}}}
};
通过路径压缩和按秩合并这两种优化策略,在均摊情况下,并查集的查找和合并操作的时间复杂度几乎可以视为常数级别,极大地提高了并查集的性能。
四、经典应用案例
4.1 岛屿数量问题(LeetCode 200)
问题描述:给定一个由 '1'
(陆地)和 '0'
(水)组成的二维网格地图 grid
,计算岛屿的数量。岛屿被水包围,并且通过水平或垂直方向上相邻的陆地连接而成。你可以假设网格的四个边均被水包围。
解决方案:可以将每个陆地单元格视为一个元素,使用并查集来合并相邻的陆地单元格。遍历网格,对于每个陆地单元格,如果其相邻单元格也是陆地,则将它们合并到同一个集合中。最后,不同集合的数量就是岛屿的数量。
class UnionFind:def __init__(self, n):self.parent = list(range(n))self.rank = [1] * ndef find(self, x):if self.parent[x] != x:self.parent[x] = self.find(self.parent[x])return self.parent[x]def unionSet(self, x, y):rootX, rootY = self.find(x), self.find(y)if rootX != rootY:if self.rank[rootX] < self.rank[rootY]:self.parent[rootX] = rootYelif self.rank[rootX] > self.rank[rootY]:self.parent[rootY] = rootXelse:self.parent[rootY] = rootXself.rank[rootX] += 1return rootX != rootYdef numIslands(grid):if not grid:return 0m, n = len(grid), len(grid[0])uf = UnionFind(m * n)for i in range(m):for j in range(n):if grid[i][j] == '1':index = i * n + jif i > 0 and grid[i - 1][j] == '1':uf.unionSet(index - n, index)if j > 0 and grid[i][j - 1] == '1':uf.unionSet(index - 1, index)islandSet = set()for i in range(m):for j in range(n):if grid[i][j] == '1':islandSet.add(uf.find(i * n + j))return len(islandSet)
4.2 冗余连接问题(LeetCode 684)
问题描述:在本问题中,树指的是一个连通且无环的无向图。给定一个有 n
个节点的树,用二维数组 edges
表示树中所有的边,其中 edges[i] = [uᵢ, vᵢ]
表示节点 uᵢ
和 vᵢ
之间有一条无向边。由于数据错误,其中有一条边是冗余的,它使得树变成了一个连通图,但包含了一个环。请找出这条冗余的边。
解决方案:使用并查集来判断添加边时是否会形成环。遍历 edges
数组,对于每条边 [u, v]
,检查 u
和 v
是否已经在同一个集合中,如果是,则说明这条边是冗余的;如果不是,则将它们合并到同一个集合中。
class UnionFind {
private:vector<int> parent;vector<int> rank;
public:UnionFind(int n) {parent.resize(n);rank.resize(n, 1);for (int i = 0; i < n; ++i) {parent[i] = i;}}int find(int x) {if (x != parent[x]) {parent[x] = find(parent[x]);}return parent[x];}bool unionSet(int x, int y) {int rootX = find(x);int rootY = find(y);if (rootX == rootY) {return false; // 已经在同一个集合中,说明添加这条边会形成环}if (rank[rootX] < rank[rootY]) {parent[rootX] = rootY;} else if (rank[rootX] > rank[rootY]) {parent[rootY] = rootX;} else {parent[rootY] = rootX;rank[rootX]++;}return true;}
};vector<int> findRedundantConnection(vector<vector<int>>& edges) {int n = edges.size();UnionFind uf(n + 1);for (const auto& edge : edges) {if (!uf.unionSet(edge[0], edge[1])) {return edge;}}return {};
}
4.3 Kruskal 算法求最小生成树
问题描述:在一个无向连通图中,最小生成树是指连接所有节点且边权之和最小的树。Kruskal 算法是一种用于求解最小生成树的经典算法,其中并查集发挥了重要作用。
解决方案:Kruskal 算法首先将所有边按照边权从小到大排序,然后依次遍历每条边。对于当前边,如果它连接的两个节点不在同一个集合中(通过并查集判断),则将这条边加入最小生成树,并合并这两个节点所在的集合;如果在同一个集合中,则说明加入这条边会形成环,跳过该边。重复上述过程,直到所有节点都在同一个集合中,此时得到的就是最小生成树。
struct Edge {int from, to, weight;Edge(int f, int t, int w) : from(f), to(t), weight(w) {}
};bool compareEdges(const Edge& a, const Edge& b) {return a.weight < b.weight;
}int kruskalMST(vector<Edge>& edges, int n) {UnionFind uf(n);int mstWeight = 0;int edgesAdded = 0;sort(edges.begin(), edges.end(), compareEdges);for (const auto& edge : edges) {if (uf.unionSet(edge.from, edge.to)) {mstWeight += edge.weight;edgesAdded++;if (edgesAdded == n - 1) {break;}}}return mstWeight;
}
五、并查集扩展与高级应用
5.1 带权并查集
在一些问题中,并查集的节点可能带有权值信息,例如在计算两个节点之间的距离、代价等场景下。带权并查集需要在查找和合并操作时,同时处理权值的更新。例如,在某些网络流问题中,可以使用带权并查集来记录节点之间的流量损耗。
5.2 扩展域并查集
扩展域并查集通过将一个元素拆分成多个域(例如正域和反域),来处理更复杂的逻辑关系。在一些逻辑推理、黑白染色等问题中,扩展域并查集能够巧妙地解决元素之间的多种约束关系。
5.3 动态维护连通性
在一些动态场景中,图的结构会不断变化,例如节点的添加、边的增删等。并查集可以用于动态维护图的连通性,实时判断节点之间的连通状态,在网络拓扑管理、游戏场景中角色关系管理等方面有广泛应用。
总结
并查集作为一种高效处理集合合并与查询的数据结构,凭借简洁的实现和强大的功能,在众多领域发挥着不可替代的作用。从基础的数组实现到优化策略的引入,从经典的算法问题到复杂的实际应用场景,深入理解并查集的原理和使用方法,能够帮助我们高效地解决动态连通性、图论算法等相关问题。
That’s all, thanks for reading!
觉得有用就点个赞
、收进收藏夹
吧!关注
我,获取更多干货~