並查集算法:
並查集算法(union-find)是一種用於快速判斷兩節點是否連通的算法.
1.連通性:
並查集算法中所處理的連通性具有如下性質:
- 自反性:節點p與其自身是連通的
- 對稱性:如果p與q是連通的,則q與p也是連通的.
- 傳遞性:如果節點p是節點q是連通的,節點q與節點r是連通的,則節點p與節點r是連通的.
2.連通性判斷的問題形式:
在連通性判斷問題中,通常首先給定多個連通的節點對(i,j)
,然後要你判斷給定的兩節點r,s是否是連通的.
3. 基於並查集的連通性問題判斷:
由於連通性的傳遞性,我們需要基於給定的連通節點對(i,j)
,構建起各個連通分量.例如,給定(a, b)
, (b,c)
連通點對,則當前(a,b,c)
三個節點位於同一連通分量內,同一連通分量內的節點間是互相連通的.只要我們構建了連通分量,則判斷節點,是否是連通的,就轉變爲判斷節點,是否位於同一連通分量內.而並查集算法核心即是這兩個步驟.1.構建連通分量;2.返回給定節點所在的連通分量編號.
4.並查集算法:
我們假定N個節點使用整數0~N-1表示.基於此,我們可以使用整數數組實現並查集算法.如果節點是其他類型對象,則可以使用鍵值對數據結構實現同樣的並查集算法.
此處整理三種並查集算法.
在並查集算法中,每一連通分量都可以使用當前連通分量中的一個節點值來代表,稱爲當前連通分量的根節點.例如假設當前連通分量中包括節點{2, 3, 11}
,我們可以使用任意一個節點來表示當前連通分量的根節點.假設2
爲該連通分量的根節點,則我們說2
所在的連通分量根節點爲2
,3
所在的連通分量根節點爲2
,如果當前兩個節點所在連通分量根節點相同,則當前兩個節點位於同一連通分量中,這兩個節點是連通的.
並查集算法API:
並查集算法包括三個方法:
void union(int p, int q)
:如果節點p,q是連通的,則我們使用union
方法將兩個節點相連接,注意到,當p,q連通後,p所在的連通分量與q所在的連通分量也連接爲一個更大的連通分量.
int find(int p)
:find
方法返回節點p所在連通分量的根節點
boolean connected(int p, int q)
:connected
方法判斷節點p與q是否連通,該方法實現非常簡單:return find(p) == find(q)
.
4.1 quick-find算法:
我們可以使用整數數組id[]
保存節點所在連通分量的根節點,id[i]
爲第i
個節點所在連通分量的根節點.在初始情況下,每一個節點相獨立,因此id[i] = i
.基於此,並查集算法實現如下:
public int find(int p){
return id[p];
}
public void union(int p, int q){
int pID = find(p);
int qID = find(q);
if(pID == qID)
return;
//將節點p的連通分量合併到節點q的連通分量中
for(int i = 0; i < id.length; i++)
if(id[i] == pID])
id[i] == qID;
}
算法時間複雜度分析:
不能發現,quick-find算法中,find方法的時間複雜度爲O(1),union方法的時間複雜度爲O(N);
4.2 quick-union算法:
爲了提高union方法的效率,我們涉及quick-union算法.在該算法中,id[]
數組含義不同於quick-find算法.我們使用id[i]
來存在節點i的父節點,如果當前節點i爲所在連通分量的根節點,則其無父節點,id[i] = i
.基於這種表示方法,一個連通分量可以被理解爲一棵多叉樹.例如,若某連通分量中包括節點{2, 3, 5, 7, 12}
,此時id[2] = 3, id[3] = 5, id[5] = 5, id[7] = 5, id[12] = 5
.則當前連通分量可以用如下圖結構表示:
假設另一連通分量爲:
其對應的id[]
數組爲id[4] = 4, id[6] = 4, id[8] = 6, id[9] = 4, id[11] = 9
.
則只需要將其中一個連通分量的根節點添加到另一連通分量根節點上即刻實現連通分量合併.我們只需要更改id[4] = 5
即可完成.
實際上,我們可以發現quick-find方法相當於一棵樹高爲2的多叉樹,其除過根節點外只有一層葉節點.對於連通分量{4,6,8,9,11}
,其根節點爲4,則使用quick-find方法的表示,其樹結構如下:
且quick-find方法要求合併後的兩顆樹仍然爲高度爲2的樹,因此兩顆樹的合併過程較爲麻煩,需要將其中一棵樹的所有節點均連接到另一棵樹的根節點.
通過對以上關於樹形的瞭解,我們不難發現,find
方法相當於從當前節點出發向上尋找根節點,在quick-find方法中,由於樹高爲1,因此我們可以在O(1)時間複雜度實現find
方法.而對於quick-union方法,find
方法最壞情況下的時間複雜度即爲該樹的高度.
public int find(p){
//從當前節點起,沿父節點向上查找到根節點
while(p != id[p])
p = id[p];
return p;
}
public void union(int p, int q){
int pRoot = find(p);
int qRoot = find(q);
if(pRoot == qRoot)
return;
id[pRoot] = qRoot;
}
算法時間複雜度分析:
該方法中,find方法所需時間與當前節點到樹根部路徑長度成正比,union方法需要調用兩次find方法查找連通分量根節點,因此其時間複雜度與find方法時間複雜度相同.
4.3 加權quick-union方法
在分析quick-union方法時,我們指出,find方法時間複雜度與樹的高度有關.最壞情況下,其時間複雜度可能與當前連通分量中的節點個數成正比(每個節點只有一個子節點).那我們應該如何儘可能地降低樹的高度呢?分析算法不難發現,我們唯一可能作出的改進在於union
方法中兩顆子樹的連接,在quick-union方法中,我們隨意的將一棵樹連接到另一棵樹上.爲了儘可能降低數的高度,我們應在每一步連接時,將高度較低的樹連接到高度較高的樹上.後面推導我們會證明,在每一步我們只需要比較兩棵樹的節點個數,將節點數少的樹合併到節點樹多的樹上即可.
假設有如下兩個連通分量,我們使用樹形表示:
如果我們將左邊的連通分量連接到右邊的連通分量中,則合併後連通分量樹形爲:
由於我們將更高的樹連接到較低的樹上,此時樹的高度進一步增加1.
如果我們將較低樹連接到較高樹,此時連通分量樹形如下:
此時樹高並未增加.
加權quick-union算法實現如下:
public class WeightQuickUnionUF{
private int[]id; //紀錄當前節點所在連通分量的根節點
private int[] sz;//紀錄sz[]對應樹形的節點個數.
private int count; //紀錄當前連通分量個數
public WeightQuickUnionUF(int N){
id = new int[N];
count = 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 int find(int p){
while(p != id[p])
p = id[p];
return p;
}
public void union(int p, int q){
int pID = find(p);
int qID = find(q);
if(pID == qID)
return;
if(sz[pID] < sz[qID]){
id[pID] = qID;
sz[qID] += sz[pID];
}
else{
id[qID] = pID;
sz[pID] += sz[qID];
}
count--; //合併後,連通分量減1
}
}
加權quick-union方法中,對於節點爲N的連通分量,find方法的時間複雜度爲O(lgN);即當前連通分量所在樹形結構其樹高不超過lg2N;
證明:我們使用歸納法證明該命題.
對於大小爲k的樹,其樹高最多爲lg2k;
- 當k=1時,樹高爲1 = lg2; 命題成立
- 假設大小爲i的樹,其樹高最多爲lg2i,當我們將大小爲i與另一棵大小爲j的樹進行合併時,假設,則將i合併到j, 合併後樹節點個數爲,合併後大小爲i的樹節點的高度+1, 而大小爲j的樹中節點高度未變,因此合併後節點數爲k的樹中各節點高度不超過;