並查集初學(1)

2021-07-11 16:17:42 字數 3392 閱讀 3600

出處:

github:

並查集(union-find sets)是一種非常精巧而實用的資料結構,它主要用於處理一些不相交集合的合併問題。一些常見的用途有求連通子圖、求最小生成樹的 kruskal 演算法和求最近公共祖先(least common ancestors, lca)等。

使用並查集時,首先會存在一組不相交的動態集合 s=

s=,一般都會使用乙個整數表示集合中的乙個元素。

每個集合可能包含乙個或多個元素,並選出集合中的某個元素作為代表。每個集合中具體包含了哪些元素是不關心的,具體選擇哪個元素作為代表一般也是不關心的。我們關心的是,對於給定的元素,可以很快的找到這個元素所在的集合(的代表),以及合併兩個元素所在的集合,而且這些操作的時間複雜度都是常數級的。

並查集的基本操作有三個:

makeset(s):建立乙個新的並查集,其中包含 s 個單元素集合。

unionset(x, y):把元素 x 和元素 y 所在的集合合併,要求 x 和 y 所在的集合不相交,如果相交則不合併。

find(x):找到元素 x 所在的集合的代表,該操作也可以用於判斷兩個元素是否位於同乙個集合,只要將它們各自的代表比較一下就可以了。

並查集的實現原理也比較簡單,就是使用樹來表示集合,樹的每個節點就表示集合中的乙個元素,樹根對應的元素就是該集合的代表,如圖 1 所示。

圖 1 並查集的樹表示

圖中有兩棵樹,分別對應兩個集合,其中第乙個集合為

,代表元素是

a a

;第二個集合為

,代表元素是

e e

。樹的節點表示集合中的元素,指標表示指向父節點的指標,根節點的指標指向自己,表示其沒有父節點。沿著每個節點的父節點不斷向上查詢,最終就可以找到該樹的根節點,即該集合的代表元素。

現在,應該可以很容易的寫出 makeset 和 find 的**了,假設使用乙個足夠長的陣列來儲存樹節點(很類似之前講到的靜態鍊錶),那麼 makeset 要做的就是構造出如圖 2 的森林,其中每個元素都是乙個單元素集合,即父節點是其自身:

圖 2 構造並查集初始化

相應的**如下所示,時間複雜度是 o(

n)o(n)

const int maxsize = 500;

int uset[maxsize];

void makeset(int size)

接下來,就是 find 操作了,如果每次都沿著父節點向上查詢,那時間複雜度就是樹的高度,完全不可能達到常數級。這裡需要應用一種非常簡單而有效的策略——路徑壓縮。

路徑壓縮,就是在每次查詢時,令查詢路徑上的每個節點都直接指向根節點,如圖 3 所示。

圖 3 路徑壓縮

我準備了兩個版本的 find 操作實現,分別是遞迴版和非遞迴版,不過兩個版本目前並沒有發現有什麼明顯的效率差距,所以具體使用哪個完全憑個人喜好了。

int find(int x) 

int find(int x)

return x;

}

最後是合併操作 unionset,並查集的合併也非常簡單,就是將乙個集合的樹根指向另乙個集合的樹根,如圖 4 所示。

圖 4 並查集的合併

這裡也可以應用乙個簡單的啟發式策略——按秩合併。該方法使用秩來表示樹高度的上界,在合併時,總是將具有較小秩的樹根指向具有較大秩的樹根。簡單的說,就是總是將比較矮的樹作為子樹,新增到較高的樹中。為了儲存秩,需要額外使用乙個與 uset 同長度的陣列,並將所有元素都初始化為 0。

void unionset(int x, int y) 

}

下面是按秩合併的並查集的完整**,這裡只包含了遞迴的 find 操作。

const int maxsize = 500;

int uset[maxsize];

int rank[maxsize];

void makeset(int size)

int find(int x)

void unionset(int x, int y)

}

除了按秩合併,並查集還有一種常見的策略,就是按集合中包含的元素個數(或者說樹中的節點數)合併,將包含節點較少的樹根,指向包含節點較多的樹根。這個策略與按秩合併的策略類似,同樣可以提公升並查集的執行速度,而且省去了額外的 rank 陣列。

這樣的並查集具有乙個略微不同的定義,即若 uset 的值是正數,則表示該元素的父節點(的索引);若是負數,則表示該元素是所在集合的代表(即樹根),而且值的相反數即為集合中的元素個數。相應的**如下所示,同樣包含遞迴和非遞迴的 find 操作:

const int maxsize = 500;

int uset[maxsize];

void makeset(int size)

int find(int x)

int find(int x)

return x;

}void unionset(int x, int y) else

}

如果要獲取某個元素 x 所在集合包含的元素個數,可以使用 -uset[find(x)] 得到。

並查集的空間複雜度是 o(

n)o(n)

的,這個很顯然,如果是按秩合併的,佔的空間要多一些。find 和 unionset 操作都可以看成是常數級的,或者準確來說,在乙個包含

n n

個元素的並查集中,進行

m m

次查詢或合併操作,最壞情況下所需的時間為 o(

mα(n

))o(mα(n))

,這裡的

α α

是 ackerman 函式的某個反函式,在極大的範圍內(比可觀察到的宇宙中估計的原子數量

1080

1080

還大很多)都可以認為是不大於 4 的。具體的時間複雜度分析,請參見《演算法導論》的 21.4 節 帶路徑壓縮的按秩合併的分析。

初學並查集 並查集1

現在,我是用乙個初學者的眼光來寫並查集,此文最初寫於我學並查集的那天,後經過多次修改。1 並查集是什麼 並查集是乙個具有多個連通分支的圖,他擁有合併兩個連通分支,和查詢兩個元素是否位於同乙個連通分支的功能。2 並查集的簡單應用 並查集解決什麼問題 假如有一些點,你知道哪些點是直接相連的,但實際上間接...

並查集初學

並查集是一種樹型的資料結構,用於處理一些不相交集合的合併及查詢問題。並查集的思想是用乙個陣列表示了整片森林 parent 樹的根節點唯一標識了乙個集合,我們只要找到了某個元素的的樹根,就能確定它在哪個集合裡。並查集用在一些有 n 個元素的集合應用問題中,我們通常是在開始時讓每個元素構成乙個單元素的集...

C 初學記錄(並查集)

並查集 題目 今天是伊格那丟的生日。他邀請了很多朋友。現在該吃晚飯了。伊格那丟想知道他至少需要多少張桌子。你必須注意到並不是所有的朋友都認識對方,而且所有的朋友都不想和陌生人待在一起。這個問題的乙個重要規則是如果我告訴你a認識b,b認識c,這意味著a,b,c互相認識,所以它們可以在乙個表中。例如 如...