在一些应用的问题中,需将n个不同的元素划分成一组不相交的集合。开始时,每个元素自成一格单元素集合,然后按一定顺序将属于同一组的元素的集合合并。其间要反复用到查询某个元素属于哪个集合的运算。适合于描述这类问题的抽象数据类型称为并查集。
并查集
并查集(Disjoint-set Union)是一种常见的数据结构,用于维护一组不相交的集合,支持合并两个集合和查询某个元素所属的集合。并查集常用于解决图论、连通性问题和动态连通性问题等,具有较高的效率和易用性。
并查集的基本操作包括:
- 初始化:将每个元素初始化为一个独立的集合,即每个元素都是该集合的根节点
- 合并:将两个不相交的集合合并成一个集合,即将其中一个集合的根节点连接到另一个集合的根节点上
- 查找:查找某个元素所属的集合,即找到该元素所在集合的根节点
并查集的优点是实现简单,可以在近乎O(1)的时间复杂度内完成合并和查找操作,因此常用于需要频繁合并和查询集合的问题中。
并查集的实现
以下是使用Java语言实现并查集的示例代码:
class UnionFind {
private int[] parent;
private int[] rank;
public UnionFind(int n) {
parent = new int[n];
rank = new int[n];
for (int i = 0; i < n; i++) {
parent[i] = i;
}
}
public int find(int x) {
if (parent[x] != x) {
parent[x] = find(parent[x]);
}
return parent[x];
}
/**
* 合并两个集合,使用平衡再优化
*/
public void union(int x, int y) {
int rootX = find(x);
int rootY = find(y);
if (rootX == rootY) {
return;
}
if (rank[rootX] > rank[rootY]) {
parent[rootY] = rootX;
rank[rootX] += rank[rootY];
} else {
parent[rootX] = rootY;
rank[rootY] += rank[rootX];
}
}
/**
* 判断两个节点是否在同一个集合中
*/
public boolean connected(int x, int y) {
return find(x) == find(y);
}
}
在这个示例代码中,我们使用两个数组
parent
和rank
来表示并查集。
- parent[i]表示节点i的父节点,初始时每个节点的父节点都是它自己
- rank[i]表示以节点i为根节点的子树的深度,初始时每个节点的深度都为0
find()
方法用于查找某个节点所在的集合,它采用路径压缩的方式来优化查找效率。在查找节点x所在的集合时,如果节点x不是根节点,就将它的父节点设置为根节点,这样下次查找时就可以直接找到- 根节点,从而加速查找效率。union()
方法用于合并两个集合,它采用按秩合并(rank)的方式来优化合并效率。在合并两个集合时,先找到它们的根节点,如果两个根节点相同,则说明它们已经在同一个集合中,直接返回;否则,- 将深度较小的根节点连接到深度较大的根节点下面。connected()
方法用于判断两个节点是否在同一个集合中,它直接调用find()方法来查找它们所在的集合,并比较两个集合的根节点是否相同
通过这样的实现,我们可以在近乎O(1)的时间复杂度内完成并查集的合并和查找操作。
Leetcode 真题
最长连续序列
解题思路:构造并查集,获取集合中节点的最大深度
class Solution {
public int longestConsecutive(int[] nums) {
Map<Integer, Integer> map = new HashMap<>();
UnionFind uf = new UnionFind(nums.length);
for (int i = 0; i < nums.length; i++) {
// 存在重复元素,跳过
if (map.containsKey(nums[i])){
continue;
}
if (map.containsKey(nums[i] - 1)) {
uf.union(i, map.get(nums[i] - 1));
}
if (map.containsKey(nums[i] + 1)) {
uf.union(i, map.get(nums[i] + 1));
}
map.put(nums[i], i);
}
return uf.getMaxConnectSize();
}
}
class UnionFind {
private int[] parent;
private int[] rank;
public UnionFind(int n) {
parent = new int[n];
rank = new int[n];
for (int i = 0; i < n; i++) {
parent[i] = i;
rank[i] = 1;
}
}
public int find(int x) {
if (parent[x] != x) {
parent[x] = find(parent[x]);
}
return parent[x];
}
public void union(int x, int y) {
int rootX = find(x);
int rootY = find(y);
if (rootX == rootY) {
return;
}
if (rank[rootX] > rank[rootY]) {
parent[rootY] = rootX;
rank[rootX] += rank[rootY];
} else {
parent[rootX] = rootY;
rank[rootY] += rank[rootX];
}
}
public int getMaxConnectSize() {
int maxSize = 0;
for (int i = 0; i < parent.length; i++) {
if (i == parent[i]) {
maxSize = Math.max(maxSize, rank[i]);
}
}
return maxSize;
}
}
省份数量
解题思路:构造并查集,获取集合中父节点为自身的节点(独立区域)
class Solution {
public int findCircleNum(int[][] isConnected) {
UnionFind uf = new UnionFind(isConnected.length);
for (int i = 0; i < isConnected.length; i++) {
for (int j = i + 1; j < isConnected[i].length; j++) {
if (isConnected[i][j] == 1) {
uf.union(i, j);
}
}
}
int count = 0;
for (int i = 0; i < isConnected.length; i++) {
if (uf.parent[i] == i) {
count++;
}
}
return count;
}
}
class UnionFind {
public int[] parent;
public int[] rank;
public UnionFind(int n) {
parent = new int[n];
rank = new int[n];
for (int i = 0; i < n; i++) {
parent[i] = i;
rank[i] = 1;
}
}
public int find(int x) {
if (parent[x] != x) {
parent[x] = find(parent[x]);
}
return parent[x];
}
public void union(int x, int y) {
int rootX = find(x);
int rootY = find(y);
if (rootX == rootY) {
return;
}
if (rank[rootX] > rank[rootY]) {
parent[rootY] = rootX;
rank[rootX] += rank[rootY];
} else {
parent[rootX] = rootY;
rank[rootY] += rank[rootX];
}
}
}
冗余连接
解题思路:
遍历每一条边,对于每一条边的两个端点:
- 如果它们已经在同一个集合中,说明在加入这条边之后会出现环,此时这条边就是需要删除的边
- 否则将这两个端点加入同一个集合中
public int[] findRedundantConnection(int[][] edges) {
int n = edges.length;
int[] parent = new int[n + 1];
for (int i = 1; i <= n; i++) {
parent[i] = i;
}
int[] result = null;
for (int[] edge : edges) {
int u = edge[0], v = edge[1];
int pu = find(parent, u), pv = find(parent, v);
if (pu == pv) {
// u和v已经在同一个集合中,说明加入这条边会出现环
result = edge;
} else {
// 将u和v加入同一个集合中
parent[pu] = pv;
}
}
return result;
}
/**
* 查找节点x所在集合的根节点(路径压缩)
*/
private int find(int[] parent, int x) {
while (parent[x] != x) {
parent[x] = parent[parent[x]];
x = parent[x];
}
return x;
}
参考资料:
- 快速并查集(Java实现)
- 并查集