1. 前言
并查集(Disjoint Set)是一种树形的数据结构,用于处理一些不交集的合并及查询问题。它在图算法、计算机科学和编程竞赛中非常常见。并查集主要用于解决动态连通性问题,即在一个过程中维护一组不相交的集合,并支持两种基本操作:
- 查找(Find):确定一个元素属于哪个集合。通常实现为查找该元素所在集合的代表元或根节点。
- 合并(Union):将两个集合合并成一个集合。
2. 基本操作
1. 初始化
- 对于每个元素 `x`,创建一个新的只包含自身的集合。这可以通过设置每个元素的父节点为自己来实现。
2. 查找 (Find)
- 给定一个元素 `x`,通过沿着父节点链向上追溯,直到找到根节点。根节点的特征是它的父节点是指向自己的。
- 为了提高效率,可以使用“路径压缩”技术:在查找过程中,将沿途的所有节点直接连接到根节点上。这样下一次查找时会更快。
3. 合并 (Union)
- 给定两个元素 `x` 和 `y`,先找到它们各自所在的集合的根节点 `root_x` 和 `root_y`。
- 如果 `root_x` 和 `root_y` 不相同,则将其中一个根节点的父节点设为另一个,从而将两个集合合并成一个。
3. 实现细节
并查集可以通过数组或者更复杂的数据结构来实现。最常见的实现方式是使用数组来存储每个元素的父节点信息。例如,对于一个包含 `n` 个元素的集合,可以定义一个长度为 `n` 的数组 `parent[]`,其中 `parent[i]` 存储了第 `i` 个元素的父节点的索引。
所以下面用数组来实现并查集。
4. 代码
初始时顶点集合:
public class DisjointSet {
// 用数组来实现并查集
// 牢大数组,存储着第i个元素在其集合中的牢大
//当然,初始的时候每个元素的牢大都是它自己
int[] parent;
// 为union方法所用,记录集合的元素个数
int[] size;
public DisjointSet(int size) {
parent = new int[size];
this.size = new int[size];
for(int i = 0; i < size; i++) {
parent[i] = i;
// 初始化时每个顶点都当做是一个集合,此时每个集合的长度当然都是1
this.size[i] = 1;
}
}
// 查找牢大的方法
public int find(int x) {
// 如果x与元素值相同,说明x就是牢大
// 比如[0, 0, 1, 2],x=0时,x==parent[0],0是顶点集合0, 1的牢大
if(x == parent[x]){
return x;
} else {
// 路径压缩,将沿途的所有顶点都连接到集合中的牢大顶点上
return parent[x] = find(parent[x]);
}
}
// union是让两个集合"相交", 即选出新的牢大,x, y是原来牢大索引
public void union(int x, int y) {
// 让元素更多的集合中的牢大,继续成为牢大
// 少的集合中的牢大成为新集合的小弟
if(size[x] < size[y]){
parent[x] = y;
size[y] = size[x] + size[y];
} else {
parent[y] = x;
size[x] = size[x] + size[y];
}
}
public static void main(String[] args) {
DisjointSet set = new DisjointSet(7);
set.union(0, 1);
set.union(1, 2);
set.union(2, 3);
set.union(3, 4);
set.union(4, 5);
set.union(5, 6);
set.find(6);
// set.find(6)对全路径做了路径压缩,使得其他所有节点的都直接指向了牢大节点0
System.out.println(set);
//[0, 0, 0, 0, 0, 0, 0]
}
@Override
public String toString() {
return Arrays.toString(parent);
}
}
调用set.find(6)之前顶点图:
调用之后: