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)。