動態聯通性問題--union-find算法

連通性問題

在給定的一張節點網絡(也就是圖)中,判斷兩個節點之是否可達的問題就是連通性問題。java

場景:判斷兩個用戶之間是否存在間接社交關係;判斷兩臺計算機之間是否創建鏈接等。算法

定義數據結構

使用最基本的數組做爲該算法的數據結構。數組下標 i 表明當前節點編號,id[i]的值表示與該節點連通的某一個節點。每一個節點id[i]的值初始化爲 i。數組

定義輸入

輸入是一系列整數對,一對整數(p, q)表明p和q是相連的。網絡

例如:輸入(3, 4)、(1,3)、(2, 5),那麼3-四、1-三、1-四、2-5是連通的。數據結構

定義union-find算法API

public class UF{優化

      void union(int p,int q)                   //在p和q之間創建鏈接ui

      int find(int p)                                 //p所在的份量的標識符spa

      boolean connected(int p,int q)     //p和q同在一個份量中則爲truecode

}對象

初始狀態,每一個節點都是一個份量。兩點之間創建鏈接後,union()方法會將兩個份量合併。一個份量中各觸點都相互鏈接。find()方法返回給定觸點所在連通份量的標識符。

connected()方法即return find(p)==find(q);  因此關鍵是實現find()方法和union方法。

1  quick-find算法

quick-find算法保證在同一連通份量中全部觸點id[]中的值必須相同。這種實現狀況下:

  • union()必須遍歷數組,將一個連通份量中的id[]值變爲另外一個連通份量的id[]值
  • find方法只需return id[p]
  • connected()方法只需判斷find[p]==find[q]便可

換一種思路,其實quick-find算法的核心是將每個連通份量以揹包的形式存放。

算法實現:

//union()方法用於合併兩個連通份量
public void qf_union(int p,int q) {
	int pID = qk_find(p);
	int qID = qk_find(q);
	if(pID == qID) return;
    //由於不知道p所在的連通份量的全部節點,須要全掃描節點數組
	for(int i = 0;i<id.length;i++)
		if(id[i] == pID) id[i] = qID;
}

//find()方法實現簡單,直接返回數組值
public int qf_find(int p) { return id[p]; }

//connected()方法返回是否相等
public boolean connected(int p, int q){ return find(p) == find(q); }

算法分析:

該算法的特色是union慢,find快。在quick-find算法中,每次find()調用訪問一次數組,常數級別O(1)。歸併兩個份量的union()操做訪問數組次數時間複雜度O(N)。

2  quick-union算法

quick-union算法中每一個觸點所對應的id[]元素都是另外一個觸點的名稱(也多是本身,若是是本身的畫說明是根節點),觸點之間循環這種關係直到到達根觸點。當且僅當兩個觸點開始這個過程打到同一個根觸點說明它們存在於一個連通份量中。這種實現狀況下:

  • find()方法就是沿着這條路徑找到根節點
  • union()方法只需將一個根節點連接到另外一個上面就可實現合併份量
  • connected()方法只需判斷find[p]==find[q]便可

換一種思路,quick-union算法的核心是將連通份量以多叉樹的形式存放。

算法實現:

//find()方法須要沿着多叉樹向上找到本身的根節點
public int qu_find(int p) {
	while(p!=id[p]) p=id[p];
	return p;
}

//union()方法只須要將一個根節點鏈接到另外一個根節點便可
public void qu_union(int p,int q) {
	int pRoot = qu_find(p);
	int qRoot = qu_find(q);
	if(pRoot == qRoot) return;
	id[pRoot] = qRoot;
}

//connected()方法簡單比較find(p)和find(q)
public boolean connected(int p, int q){ return find(p) == find(q); }

算法分析:

該算法的特色是union快,find慢。find()方法訪問數組次數是1+觸點所在樹的高度*2,即時間複雜度是O(logN);union()方法和connected()方法時間複雜度是常數級別O(1)。

3  加權quick-union算法:

對quick-union算法的改進,保證小的樹連接在大樹上。即給每個連通份量添加權重,在須要將兩個連通份量合併時,將權重小的連通份量鏈接到大的連通份量上。

算法實現:

在類中新建一個數組保存各根節點的權重。注意該數組中只有根結點對象的下標中的數據有效。而後修改union方法以下:

public void union(int p, int q) {
    int rootP = find(p);
    int rootQ = find(q);
    if (rootP == rootQ) return;
    //將權重小的連通份量鏈接到權重大的上面
    if (size[rootP] < size[rootQ]) {
        parent[rootP] = rootQ;
        size[rootQ] += size[rootP];
    }else {
        parent[rootQ] = rootP;
        size[rootP] += size[rootQ];
    }
}

算法分析:

加權quick-union算法能夠有效地下降生成的連通份量的樹的高度,從而提升算法執行效率。固然這是一種用空間換時間的方法,由於使用了輔助數組保存節點權重,因此它的額外空間複雜度爲O(N)。

4  路徑壓縮的加權quick-union算法:

加權quick-union算法在大部分整數對都是直接鏈接的狀況下,生成的樹依舊會比較高。因此能夠進一步優化:每次計算某個節點的根結點時,將沿路檢查的結點也指向根結點。儘量的展平樹,這樣將大大減小find()方法遍歷的結點數目。

算法實現:

//union()方法
public void union(int p, int q) {
    int rootP = find(p);
    int rootQ = find(q);
    if (rootP == rootQ) return;
    if (rank[rootP] < rank[rootQ]) parent[rootP] = rootQ;
    else if (rank[rootP] > rank[rootQ]) parent[rootQ] = rootP;
    else {
        parent[rootQ] = rootP;
        rank[rootP]++;
    }
}

//find()方法
public int find(int p) {
    while (p != parent[p]) {
        parent[p] = parent[parent[p]];    //路徑壓縮減半
        p = parent[p];
    }
    return p;
}

//connected()方法
public boolean connected(int p, int q) { return find(p) == find(q); }

算法分析:

路徑壓縮後基本上連通份量樹的高度爲2, 因此find()方法的時間複雜度接近O(1),union()方法的時間複雜度接近O(1)。

附:union-find算法和圖的可達性問題

圖的可達性問題通常採用深度優先遍歷的思想來實現。理論上,深度優先算法解決圖的可達性比union-find快,由於它可以保證所需時間是線性的。

但實際上,union-find算法更快,由於它不須要完整的構造並表示一張圖。更重要的是union-find算法是一種動態算法,咱們在任什麼時候候都能用接近常數的時間檢查兩個頂點是否連通,甚至在添加一條邊的時候,但深度優先算法必須對圖進行預處理。

相關文章
相關標籤/搜索