并查集 (Union-Find)
问题现象
并查集算法主要是用来解决动态连通性问题
动态连通性(Dynamic Connectivity)
根据《Algorithms4》可知,动态连通性即给定N个对象的集合,可以做以下操作:
- Union操作:连接两个对象
- Find/connected查询: 两个对象是否连接?
其中,连通具有以下性质:
- Reflexive: p和p是相连的,又称自反性;
- Symmetric: 如果p和q是相连的,那么q和p也是相连的,又称对称性;
- Transitive: 如果p和q相连且q和r相连,那么p和r相连,又称传递性。
问题建模
API
public class UnionFind {
int[] parent; // 存储某个对象的父节点,大小为N,初始化为对应的数组下标,代表独立分量,且是树根,也即初始化为parent[i] = i;
UnionFind(int N) { // 构造函数
}
public void union(int p, int q) { // 连通 q, p分量
}
private int find(int x) { // 获取x的根节点
}
public boolean connected(int p, int q) { // 判断q, p是否连通
}
public int count() { // 获取连通分量个数
}
}
find方法
private int find(int x) { // 获取x的根节点
while (x != parent[x]) { // 根节点的数组值就是自身的下标,也就是parent[x] = x
x = parent[x];
}
return x;
}
union方法
public void union(int p, int q) { // 连通 q, p分量
int rootQ = find(q);
int rootP = find(p);
if (rootP == rootQ) { // 若根节点相同,则说明两个分量已相连,直接返回
return;
}
parent[rootQ] = rootP; // 将Q的根节点连接到P的根节点上,也可以parent[rootP] = rootQ
}
优化
从上述代码可知,union方法是直接将某棵树的根节点连接在另一棵树上,在多次执行此操作后,树可能会很高,甚至退化成链表。而find方法是从某一个节点往上依次遍历到根节点,若树的高度很高,则会导致查找效率低下。
find方法优化
- 隔代压缩:把路径上的每个节点的父节点指向其祖父节点
private int find(int x) { // 获取x的根节点
while (x != parent[x]) { // 根节点的数组值就是自身的下标,也就是parent[x] = x
parent[x] = parent[parent[x]];
x = parent[x];
}
return x;
}
- 彻底压缩:先找到当前结点的根结点,然后把沿途上所有的节点都指向根节点
private int find(int x) { // 获取x的根节点
if (x != parent[x]) { // 根节点的数组值就是自身的下标,也就是parent[x] = x
parent[x] = find[parent[x]];
}
return x;
}
union方法优化
增加一个数组,大小为N,初始化为对应的数组下标,也即初始化为size[i] = i,记录每棵树的高度,在查找的时候比较树高,高度小的树合并到高度搞的树上,相等的话两者均可作为根节点,并把高度加一;
public void union(int p, int q) { // 连通 q, p分量
int rootQ = find(q);
int rootP = find(p);
if (rootP == rootQ) { // 若根节点相同,则说明两个分量已相连,直接返回
return;
}
if (size[rootP] > size[rootQ]) {
parent[rootQ] = rootP;
size[rootP] += size[rootQ];
} else {
parent[rootP] = rootQ;
size[rootQ] = size[rootP];
}
count--;
}
完整算法
public class UnionFind {
int count;
int[] parent;
int[] size;
UnionFind(int N) {
this.count = N;
this.parent = new int[N];
this.size = new int[N];
for (int i = 0; i < N; ++i) {
parent[i] = i;
size[i] = 1;
}
}
public void union(int p, int q) { // 连通 q, p分量
int rootQ = find(q);
int rootP = find(p);
if (rootP == rootQ) {
return;
}
if (size[rootP] > size[rootQ]) {
parent[rootQ] = rootP;
size[rootP] += size[rootQ];
} else {
parent[rootP] = rootQ;
size[rootQ] = size[rootP];
}
count--;
}
private int find(int x) { // 获取x的父节点
while (x != parent[x]) {
parent[x] = parent[parent[x]];
x = parent[x];
}
return x;
}
public boolean connected(int p, int q) { // 判断q p是否连通
int rootQ = find(q);
int rootP = find(p);
return rootP == rootQ;
}
public int count() { // 获取联通分量个数
return count;
}
}