1. 问题描述
可以想象一张地图上有很多点,有些点之间是有道路相互联通的,而有些点则没有。如果我们现在要从点 A 走向点 B,那么一个关键的问题就是判断我们能否从 A 走到 B 呢?换句话说,A 和 B 是否是连通的。这是动态连通性最基本的诉求。现在给出一组数据,其中每个元素都是一对“点”,代表这对点之间是联通的,我们需要设计一个算法,让计算机依次读取这些数据,最后判断出其中任意两点是否连通。
2. quick-find
保证当且仅当 id[p] 等于 id[q] 时 p 和 q 是连通的。即在同一个分量中的所有触点在 id[ ] 中的值必须全部相同。
import java.util.Scanner;
public class UF {
private int[] id;//分量 id(以触点为索引)
private int count;//分量数量
public UF(int N){
//初始化分量 id 数组
count = N;
id = new int[N];
for(int i = 0;i < N;i++){
id[i] = i;
}
}
public int count(){
return count;
}
public boolean connected(int p,int q){
return find(p) == find(q);
}
public int find(int p){
return id[p];
}
public void union(int p,int q){
//将q和p归并到相同的分量中
int pID = find(p);
int qID = find(q);
//如果p和q已经在相同的分量之中则不需要采取任何行动
if(pID == qID) return;
//将p的分量重命名为q的名称
for(int i = 0;i < id.length;i++){
if(id[i] == pID) id[i] = qID;
}
count--;
}
//解决动态连通性问题
/*测试数据:
10
4 3
3 8
6 5
9 4
2 1
5 0
7 2
6 1
1 0
6 7
输出: 2 components */
public static void main(String[] args) {
Scanner in = new Scanner(System.in);
int N = in.nextInt();
UF uf = new UF(N);
for(int i = 0;i < N;i++){
int p = in.nextInt();
int q = in.nextInt();
if(uf.connected(p,q)) continue;//已经连通则忽略
uf.union(p,q);//归并分量
}
System.out.println(uf.count + " components");
}
}
时间复杂度:find() 为 O(1),union() 为 O(N)。
3. quick-union
每个触点所对应的 id[ ] 元素都是同一个分量中的另一个触点的名称(也可能是他自己)。在实现 find() 方法时,从给定触点开始,由它链接得到另一个触点,再由这个触点的链接到达第三个触点,如此继续直到到达一个根触点,即链接指向自己的触点。
package fundamentals;
import java.util.Scanner;
public class UF {
private int[] id;//分量 id(以触点为索引)
private int count;//分量数量
public UF(int N){
//初始化分量 id 数组
count = N;
id = new int[N];
for(int i = 0;i < N;i++){
id[i] = i;
}
}
public int count(){
return count;
}
public boolean connected(int p,int q){
return find(p) == find(q);
}
private int find(int p){
//找出分量的名称
while(p != id[p]) p = id[p];
return p;
}
private void union(int p,int q){
//将p和q的根节点统一
int pRoot = find(p);
int qRoot = find(q);
if(pRoot == qRoot) return;
id[pRoot] = qRoot;
count--;
}
//解决动态连通性问题
/*测试数据:
10
4 3
3 8
6 5
9 4
2 1
5 0
7 2
6 1
1 0
6 7
输出: 2 components */
public static void main(String[] args) {
Scanner in = new Scanner(System.in);
int N = in.nextInt();
UF uf = new UF(N);
for(int i = 0;i < N;i++){
int p = in.nextInt();
int q = in.nextInt();
if(uf.connected(p,q)) continue;//已经连通则忽略
uf.union(p,q);//归并分量
}
System.out.println(uf.count + " components");
}
}
时间复杂度取决于形成的树的高度。
4. 优化的 quick-union
与其在 union() 中随意将一棵树连接到另一颗树,现在记录每一颗树的大小并总是将较小的树连接到较大的树上。
import java.util.Scanner;
public class WeightedQuickUnionUF {
private int[] id;//分量 id(以触点为索引)
private int[] sz;//各个根节点所对应的分量的大小
private int count;//分量数量
public WeightedQuickUnionUF(int N){
//初始化分量 id 数组
count = N;
id = new int[N];
for(int i = 0;i < N;i++){
id[i] = i;
}
sz = new int[N];
for(int i = 0;i < N;i++){
sz[i] = 1;
}
}
public int count(){
return count;
}
public boolean connected(int p,int q){
return find(p) == find(q);
}
private int find(int p){
//找出分量的名称
while(p != id[p]) p = id[p];
return p;
}
private void union(int p,int q){
//将p和q的根节点统一
int i = find(p);
int j = find(q);
if(i == j) return;
//将小数的根节点连接到大树的根节点
if(sz[i] < sz[j]){
id[i] = j;
sz[j] += sz[i];
}else {
id[j] = i;
sz[i] += sz[j];
}
count--;
}
//解决动态连通性问题
/*测试数据:
10
4 3
3 8
6 5
9 4
2 1
5 0
7 2
6 1
1 0
6 7
输出: 2 components */
public static void main(String[] args) {
Scanner in = new Scanner(System.in);
int N = in.nextInt();
WeightedQuickUnionUF uf = new WeightedQuickUnionUF(N);
for(int i = 0;i < N;i++){
int p = in.nextInt();
int q = in.nextInt();
if(uf.connected(p,q)) continue;//已经连通则忽略
uf.union(p,q);//归并分量
}
System.out.println(uf.count + " components");
}
}
时间复杂度:find() 和 union() 都为 O(logN)。