用于解決動態連通圖的連接性問題
問題描述
給定由N個對象構成的集合,并告知哪些對象之間是連通的,由此判斷某兩個對象之間是否連通。
注:連通代表有路可達,而不一定直接相連
抽象建模
- 將所有對象用序號標識,如0,1,2,3...
- Union 將告知的連通對象之間連通
- Find 判斷某兩個對象之間是否連通
算法描述
基本思想是,將連通圖分割成N個最大連通子集,即每個子集內部的對象相互連通,而子集之間的對象互不連通,這樣只需判斷某兩個對象是否在同一個子集中,便可判斷其是否連通。
具體而言,根據不同的實現方式,可衍生出以下幾種算法。
1. Quick-Find
顧名思義,這是一個注重快速判斷的算法。它將屬于同一個子集的對象用同一個標號標識,而不同子集用不同標號標識,這時判斷對象是否連通就等價于判斷對象標號是否相等。
實現思路
- 整形N維數組id[],存儲N個對象所屬的子集標號,下標為對象標識
- Union 合并分別包含p和q的子集,需將所有標號等于id[p]的都置為id[q]
- Find 判斷p和q是否連通,即判斷是否滿足id[p]==id[q]
偽代碼
// 初始化 function(int N)
id = new int[N];
for (int i = 0; i < N; i++)
id[i] = i; // 每個對象在初始時各不連通,各成一個子集,用其下標標識
// Union function(int p, int q)
int pid = id[p];
int qid = id[q];
for (int i = 0; i < N; i++)
{
if (id[i] == pid)
id[i] = qid; // 將所有子集標識等于id[p]的改為id[q]
}
// Find function(int p, int q)
return id[p] == id[q]; // 判斷子集標識是否相同
這種算法雖然Find很快,但Union卻非常慢,每次需要判斷和改動的位置很多。一般來說,Union的次數要比Find的次數多很多,所以注重Union效率的提高會更實際一些。
2. Quick-Union
這即是一個注重快速合并的算法, 不同于Quick-Find將子集內所有對象都用同一個標識表示,它將子集中的對象看成是樹狀結構,每個子集形成一棵樹,子集中的對象都有父對象,根對象的父對象是其自身。
此時,在N維數組中存儲各對象的父對象標識,則對象i所在子集樹的根為id[id[id[...id[i]...]]],同一個根對應的所有對象是連通的。這樣,Find時需判斷對象的根是否相同,而Union時只需將一個子集樹的根變為另一個子集樹的孩子。可見,Union時只需要變更一個位置,而Find則取決于樹的高度。
偽代碼
// Root function(int i)
while (i != id[i]) i = id[i]; // 尋找根節點
return i;
// Union function(int p, int q)
int i = root(p);
int j = root(q);
id[i] = j; // 將一顆子集樹掛在另一顆子集樹根上
// Find function(int p, int q)
return root(p) == root(q); // 判斷對象根節點是否相同
雖然這種算法的Union速度加快,但其速度仍取決于root的速度,即樹的高度,因此可優化的地方還有很多。
3. Weighted Quick-Union
這是一個基于QU的加權算法,它可有效降低樹的高度,使root速度變快。
其思想是,在每次Union合并子集樹時,總是將小樹的根掛在大樹的根上,而不像QU中總是將前一個樹的根掛在后一個樹的根上。
這就需要另外設置一個N維整形數組,用于存儲以每個對象為根的樹大小,這樣在Union時便可比對子集樹的大小。
偽代碼
// 初始化 function(int N)
sz = new int[N]; // 樹大小數組
for (int i = 0; i < N; i++)
sz[i] = 1; // 初始時各對象自成一樹,每棵樹的大小為1
// Union function(int p, int q)
int i = root(p);
int j = root(q);
if (sz[i] < sz[j])
{
id[i] = j;
sz[j] += sz[i];
}
else
{
id[j] = i;
sz[i] += sz[j];
}
這種算法帶來的壓縮樹高度的效果是非常顯著的,尤其是在對象個數較多時。
4. Quick-Union with Path Compression
這是一種基于QU的路徑壓縮優化算法,目的也是壓縮樹的高度。
一種思想是,在每次尋找對象的根節點時,都將通往根節點的沿途各個節點直接掛在根節點下。代碼實現可設置兩個循環,先找根節點,再掛各節點。
另一種思想是,在每次尋找對象的根節點時,都將對象掛在其祖父對象上,再從祖父對象依次重復上述過程。則每次root會大致將路徑長度壓縮一半。
偽代碼
// Root function(int i)
while (i != id[i])
{
id[i] = id[id[i]];
i = id[i];
}
return i;
當然也可以將Weighted與Path Compression結合使用。
5. Weighted Quick-Union with Path Compression
Weighted是在Union階段,而Path Compression是在Root階段,兩者互不干擾,可結合使用,效果更好。
偽代碼(完整)
/* Weighted Quick-Union with Path Compression */
// 初始化 function(int N)
id = new int[N]; // 父對象數組
sz = new int[N]; // 樹大小數組
for (int i = 0; i < N; i++)
{
id[i] = i;
sz[i] = 1;
}
// Root function(int i)
while (i != id[i])
{
id[i] = id[id[i]]; // 采用Path Compression第二種思想
i = id[i];
}
return i;
// Union function(int p, int q)
int i = root(p);
int j = root(q);
if (sz[i] < sz[j]) // 采用Weighted思想
{
id[i] = j;
sz[j] += sz[i];
}
else
{
id[j] = i;
sz[i] += sz[j];
}
// Find function(int p, int q)
return root(p) == root(q);
算法分析
現分析各算法的時間性能。
Quick-Find
UnionO(N),FindO(1)Quick-Union
UnionO(N),FindO(N)
主要耗時在root上Weighted-QU
UnionO(logN),FindO(logN)
樹的高度不會超過logN(以2為底),考慮一個對象,其最開始樹大小為1,每次其高度+1發生在其作為小樹成員掛到大樹根上,此時合并樹的大小>=2倍小樹大小,而小樹最多不會翻倍logN次,所以高度最高不會超過logNQU + Path Compression
UnionO(logN),FindO(logN)
猜想:采用第二種思想時,每次可將路徑壓縮一半,則最終樹的高度<=logN?Weighted-QU + Path Compression
<= c(N+MlogN) 接近于線性
N表示對象個數,M表示Union次數
logN表示N經過多少次log能到達1,如log16=3, log65536=4
Algorithm | Initialize | Union | Find | Worst-Case Time |
---|---|---|---|---|
Quick-Find | N | N | 1 | MN |
Quick-Union | N | N | N | MN |
Weighted-QU | N | logN | logN | N + MlogN |
QU + Path Compression | N | logN | logN | N + MlogN |
Weighted-QU + Path Compression | N | log*N | log*N | N + Mlog*N |
注:Union和 Find列均指一次操作的最壞時間復雜度, Worst-Case Time表示 M次Union操作的最壞時間復雜度。
應用舉例
Union-Find 的一個典型應用是解決“滲透問題(Percolation)”
N*N的網格,網格中每一塊都以概率p開啟(白色)或關閉(黑色),判斷從頂層能否滲透到底層(可假想從頂層注水,只能流經白色區域,水能否到達底層,或者導體通電問題等)
將每個網格看成是一個對象,這類問題明顯可轉化為動態連通圖連接性問題,但要注意幾個問題。
- 為避免循環,可將所有頂層節點與一個虛擬頂層節點相連,而將所有底層節點都與一個虛擬底層節點相連,這樣只需判斷兩個虛擬節點是否連通。
- 開啟一個網格時,應連通它與周圍四個節點中所有開啟的節點。
- Backwash問題,在將所有底層節點與虛擬底層節點相連后,如果要判斷一個節點(不一定是底層節點)是否與頂層節點相連時,它可能會通過虛擬底層節點向上尋找到路徑,這顯然不符合常理。
可有兩種解決方式,一是取消使用底層虛擬節點,犧牲時間;二是帶虛擬底層幾點與不帶虛擬底層節點兩種模型結合使用,犧牲空間。
結束語
動態連通圖問題可以是很多問題的抽象建模,如網絡中計算機連接、社交中的朋友關系、數學集合中的元素等等,掌握Union-Find的各算法是非常有用的。