並查集的相關演算法,是我見過的,最為之有趣的演算法之一。並查集是一種樹型的資料結構,用於處理一些不相交集合(disjoint sets)的合併及查詢問題。其相關的實現**較為簡短,實現思想也簡單易懂,處理問題的效率也高,解決的問題範圍也較廣。
為了實現並查集的相關演算法,我們規定將物件稱之為觸點,將整數對稱之為連線,將兩兩之間彼此互不相連的各個集合的分布(也就是其相關的等價類)稱之為連通分量,也稱為分量。同時定義了如下的api用來封裝其所需的基本操作:
public class uf
相關api
說明uf(int n)
以整數標識(0到n-1)初始化n個觸點
void union(int p,int q)
在p和q之間新增一條連線,也就是連線p和q
int find(int p)
p(0到n-1)所在的分量的標識
boolean connected(int p,int q)
如果p和q存在同乙個分量中則返回true
int count()
連通分量的數量
下圖1.1用以說明觸點,連線,和連通分量:
我們用整數int來表示相關的觸點以及分量。為此,我們可以用乙個以觸點為索引(也就是下標)的陣列id來表示所有的分量。我們將使用分量中的某個觸點的名稱作為分量的識別符號,因此,你可以認為每個分量都是由它的觸點之一來進行表示的。一開始我們有n個分量,每個觸點都構成了觸點i,我們將用find()方法用來判定它所在的分量所需的資訊儲存在id[i]之中。connected(int p,int q)方法的實現只用到了一條語句find(p)==find(q),它將返回布林值。find(int p)方法和union(int p,int q)方法的實現是整個union-find相關演算法的關鍵。我們在實現時,採用int陣列id用於表示相關的連通分量的資訊,乙個整型變數count用於記錄連通分量的個數。
以下**為相關api中的部分api的實現
package algorithm;
/** * 用於演示並查集(union-find)演算法
* @author 學徒
* */
public abstract class uf
@override
public int find(int p)
@override
public void union(int p, int q)
@override
public int find(int p)
@override
public void union(int p, int q)
}
其union過程如下示意圖1.2所示:
由於每個觸點初始化之前均指向自身,為此,當每次union操作時,均會消除掉乙個指向自身的觸點,使得其中乙個觸點指向另乙個指向自身的觸點。我們可以將每個指向自身的觸點的分量看成是乙個整體,通過歸納,可以得知,無論從該連通分量的哪個觸點出發進行find()操作,沿著其相關的鏈結進行查詢。到最後總是可以找到其指向自身的乙個觸點,即為根觸點,也就是該連通分量的標記。
分析:
quick-union演算法看起來比quick-find演算法快很多,因為它並不需要為每對輸入遍歷整個陣列。在最好的情況,find()只需要訪問陣列一次就能夠得到乙個觸點所在的連通分量的標記符;而在最壞的情況下,需要o(n)次訪問(由於在初始時,每個觸點均指向自身,若從開始到結束,在每次union的時候,將多個觸點的連通分量鏈結到只有乙個觸點的連通分量的時,會出現連通分量合併成為一條「鏈」的情況,如圖1.3所示)。為此,當使用quick-union演算法對問題進行處理並最終只得到乙個連通分量的時候,時間複雜度在最壞情況下為o(n^2)。出現這一種情況的根本原因在於進行union處理的時候,每次都將深度較深的連通分量所表示的樹,鏈結到深度較淺的連通分量所代表的樹中。為此,提出了如下的「加權quick-union」演算法
加權quick-union演算法是在quick-union演算法的基礎上進行改進的。其在quick-union的基礎上做出的改變的是,與其在union()中隨意的將一棵樹連線到另外的一棵樹,不如記錄每一棵樹的大小並總是將較小的樹連線到較大的樹上。這項改動需要新增乙個額外的陣列和一些**來記錄數中的節點數。
相關**:
class weightquick_union extends uf
{ private int sz;//(由觸點索引的)各個根節點所對應的分量的大小
public weightquick_union(int n)
{super(n);
sz=new int[n];
for(int i=0;i分析:
加權quick-union演算法能夠保證對數級別的效能。因為對於n個觸點,加權quick-union演算法構造的森林中的任意節點的深度最多為lgn。其證明過程如下:
證明:
可以通過歸納法證明乙個更強的命題,即森林中大小為k的樹的高度最多為lgk。在原始情況下,當k等於1時樹的高度為0.根據歸納法,我們假設大小為i的樹的高度最多為lgi,其中i < k。設i <= j且i+j=k,當我們將大小為i和大小為j的樹歸併時,小樹中的所有節點的深度都增加了1,但它們現在所在的樹的大小為i+j=k,而1+lgi=lg(i+i)<=lg(i+j)=lgk,性質成立。
雖然,加權quick-union演算法能夠保證在lgn的時間複雜度內解決問題,但是,仍然存在著一種比加權quick-union演算法時間複雜度更低的演算法,那就是使用路徑壓縮的quick-union演算法。理想情況下,我們希望每個觸點都直接的連線到其根節點上,但我們又不想像quick-find演算法那樣通過修改大量的連線而達到目的。於是,出現了一種接近這種理想狀態的方法,那就是在檢查觸點的同時,把他直接連線到根節點上。該方法只需要在find(int p)中為其新增上乙個迴圈,將在路徑上遇到的所有節點都直接鏈結到其根節點上,這樣我們所得到的結果是幾乎完全扁平化的樹。
相關**:
class pathcompressweightquick_union extends uf
{ private int sz;//(由觸點索引的)各個根節點所對應的分量的大小
public pathcompressweightquick_union(int n)
{super(n);
sz=new int[n];
for(int i=0;i分析:
使用路徑壓縮的加權quick-union演算法,其均攤後的成本非常非常的接近但仍沒能達到常數級別(o(1))。
在學到乙個知識點的時候,我們可能會常常思考乙個問題,那就是它有什麼用?並查集也不例外,為了說明並查集的相關作用。此處,舉了一些簡單的例子,用以說明並查集的使用場景及其相關的作用,在舉例的時候,使用整數來表示問題中的集合的每個單位。
網路:輸入的整數表示的是大型計算機網路中的計算機,而整數對則表示網路中的連線。使用並查集能夠幫助我們判定在兩台計算機p和q之間是否需要架設一條新的連線才能夠進行通訊,或者是我們可以通過已有的連線在兩者之間建立通訊線路;或者這些整數表示的可能是電子電路中的觸點,而整數對表示的是連接觸點之間電路;或者這些整數表示的可能是社交網路中的人,而整數對表示的是朋友關係,使用並查集,我們可以知道社交網路上的任意兩個人之間,是否可以通過已知的連線發生聯絡。
變數名等價性:某些程式設計環境允許宣告兩個等價的變數名(指向同乙個物件的多個引用)。在一系列這樣的宣告之後,系統需要判斷兩個給定的變數名是否等價。這種較早出現的應用(如fortran語言)推動了並查集相關的演算法的發展。
數學集合:在更高的抽象層次上,可以將輸入的所有整數看做屬於不同的數學集合。在處理乙個整數對p和q時,我們是在判斷它們是否屬於相同的集合。如果不是,我們會將p所屬的集合和q所屬的集合歸併到同乙個集合中。該方式也是個人認為的,用於思考乙個問題是否可以通過使用並查集來解決的最後的方式,先把問題進行抽象化,當問題抽象化成判斷兩個集合是否同屬於乙個集合的問題的時候,可以採用並查集來進行解決。
回到目錄|·(工)·)
並查集演算法
所謂並查集,它是乙個集合,這個集合的元素也是集合,他支援三種操作 makeset x 建立乙個只有乙個元素x的集合x0,將這個集合放入並查集中 findset x 在並查集中尋找乙個元素s 注意並查集的元素s也是集合 滿足 x屬於s union x,y 將並查集中的元素s1,s2合併,其中x屬於s1...
並查集演算法
並查集是一種樹型的資料結構,用於處理一些不相交集合 disjoint sets 的合併及查詢問題。常常在使用中以森林來表示。讓每個元素構成乙個單元素的集合,也就是按一定順序將屬於同一組的元素所在的集合合併。1 makeset s 建立乙個新的並查集,包含s個單元素集合。2 union x,y 把x ...
並查集演算法
includeint pre 10 int find int x 查詢祖先節點 int i x,j while i r 壓縮路徑 return r void join int x,int y int main 2 食物鏈問題 description 動物王國中有三類動物a,b,c,這三類動物的食物鏈...