Disjoint Sets

Disjoint Sets

Disjoint Sets的意思是一堆集合們,它們相互之間都沒有交集。沒有交集是指:各個集合之間沒有擁有共同、相同的元素。中文稱作「分離集」。

Disjoint Sets的性質相當特殊。資訊學家仔細觀察其特性後,精心設計出一套優雅美觀的資料結構,可以快速的做集合運算。

Union、Find、Split

由於每個Disjoint Sets指的就是集合們都沒有交集,我們就不用考慮交集、差集等等的運算,因為結果很明顯。所以只需要考慮union、find、split這三個集合運算:

union就是將兩個集合做聯集,合併成一個集合。find就是找找看一個元素是在哪個集合裡面。split就是把一個元素從一個集合中分離出來。

【split此處暫不介紹,俟編者讀過書後再來寫。】

Disjoint Sets的應用範例:分組

現在教室裡有十個學生,他們為了做報告,要進行自由分組──每組的人數是不固定的,有些人是自己一個人一組,也有可能這十個學生都同一組。學生聲稱已經分好組了,但是你還是很擔心分組的情況,所以去找了幾個學生來問話。學生會告訴你他和誰誰誰一組,但是他可能不知道他們那組全部的人到底有誰,因為同一組的組員可能又去拉別人入組,但是他不知情。現在你知道了一些學生告訴你的資訊,你要怎麼知道到底誰和誰同組,誰和誰不同組呢?

要解決這樣的問題,便可以使用Disjoint Sets。不管學生怎麼分組,任何一個學生除了是自己組的組員之外,不可能是別組的組員。所以這些學生分出來的組(集合),必定是Disjoint Sets。

Disjoint Sets: 簡單的資料結構

簡單的資料結構

讓一條int陣列的第x格代表第x人,格子裡填上這個人所屬的團體編號。若兩個人在同一團體,他們的格子裡就會有相同的團體編號。這是很直觀的方式。

初始化

一開始大家還沒開始分團的時候,其實可以想做是:每個人都不同團,每個人都是自己一人一團。有個方便的初始值設定方法,就是將第x格的值設成x,這樣每個人就都是不同團體的了。

Union: 兩個人想合併自己所屬團體

現在有兩團想要合併成一團,交涉的人分別是x和y。x y想要合併成一團,只要把所有與x y同團的人,都填上同一個團體編號就行了。可以找x y其中一團的團體編號,作為新的團體編號,這樣就不需要額外的編號了。(這裡我們不考慮會不會有人不服氣的問題。)

Find: 找出一個人在哪一團?

直接看團體編號即可。

Equivalent Relation: 兩個人是否同團?

直接看團體編號即可。

Number of Sets: 全部總共有幾個團體?

兩團合併成一團後,總團體數就會減少一團。所以只要修改一下union的程式碼就可以了。

Cardinality of a Set: 一個團體總共有幾個人?

一個一個數是差勁的方法:

比較好的方法是:另外開一條陣列去紀錄每個團體的人數吧!陣列第x格填入團體編號為x的人數。要找出一個團體的人數,就直接從陣列裡面找。

以團體的角度來看:兩團合併成一團後,團體人數就會改變。以人的角度來看:當一個人所屬的團體被改變時,就調整人數。所以只要修改一下union的程式碼就可以了。

根據團體的人數多寡來做union

合併團體時,將小的團體併入大的團體,可以節省一點點設定團體和增減人數所需的時間。

Singleton Set: 團體是否合併過?

自己一個人一組,沒有union過。

時間複雜度

union為O(N),find、equivalence、cardinality、singleton為O(1)。

如果有N個人,全部的人都union過一遍,每次union要花O(N)時間,總共是花O(N^2)時間。

空間複雜度

如果有N個人,就需要一條N格的陣列,為O(N)。

UVa 10608

Disjoint Sets: Disjoint-set Linked Lists

Disjoint-set Linked Lists

和Set Linked Lists的方式是一樣的。【待補文字】

Disjoint Sets: Disjoint-set Forest

Disjoint-set Forest

讓一條int陣列的第x格代表第x人──不過,格子裡改成填上x的老大是誰:

有一點像是老鼠會,也可以看作是圖論所提到的有根樹(rooted tree)。各位可能會有一個疑問:一個團體之中,每個人都有一個老大,那麼某君的老大是誰呢?可以暫且設定成自己:

以無法向上追溯的設定,來代表這個人是團體的大頭目。團體的所有成員,他們往上追溯之後,會是同一個頭目。一個團體中,也只會有一個頭目,由他來支配團體、做為團體的代表。

一個團體就像是一棵分支很複雜的有根樹,不過卻是萬流歸宗的。這些團體構成了一叢森林,故名Disjoint-set Forest。

初始化

一開始大家還沒開始分團的時候,其實可以想做是:每個人都不同團,每個人都是自己一人一團,而且自己當頭目。根據上述的設定方是,要將第x格的值設成x,這樣每個人就都是不同團體的頭目了。

Find: 找出一個人在哪一團?

接下來談談頭目吧。頭目在一個團體之中扮演舉足輕重的角色,一個團體只會有一個頭目,所以可以用頭目做為一個團體的代表。

find的時候可以順便把遇到的人,將其老大都設為頭目。如此一來下次find的時候就會變更快了。

Union: 兩個人想合併自己所屬團體

目標是將x y兩個團體做合併,並重新選出一個頭目。最簡單的方式是:讓x的頭目帶著他所有小弟,投靠y團體的隨便一個人之下,如此一來兩個團體就擁有共同的頭目了,也依然保持著老鼠會的架構。

union的時候,直接投靠對方的老大,可以讓樹的深度增加最少。如此一來下次find的時候就會變更快了。

實做小叮嚀:union要確保投奔的人是頭目,投奔後頭目只有一個。另外也要避免同團體的人互相設定彼此是頭目,否則find會無限循環。

Equivalent Relation: 兩個人是否同團?

同一個團體中的成員,他們的頭目都是同一個人。要看兩個人是不是同一團,看看他們的頭目是不是同一人就行了。

Number of Sets: 全部總共有幾個團體?

兩團合併成一團後,總團體數就會減少一團。所以只要修改一下union的程式碼就可以了。

Cardinality of a Set: 一個團體總共有幾個人?

先前提到頭目可以支配、代表一個團體,所以把焦點放在頭目上吧。嘗試開一個陣列來記錄頭目帶領的人數,n[頭目] = 頭目帶領的人數。

以團體的角度來看:兩團合併成一團後,團體人數就會改變。以人的角度來看:當一個人所屬的團體被改變時,就調整人數。所以只要修改一下union的程式碼就可以了。

Singleton Set: 團體是否合併過?

自己一個人一組,沒有union過。

也就是自已當頭目的意思。

時間複雜度

union、find、singleton、equivalence的平均時間是Ω(α(N)),無法更低了,cardinality為O(1)。其中α(N)是Ackermann function f(N,N)的反函數。我不會證。【待補文字】

空間複雜度

如果有N個人,就需要一條N格的陣列,為O(N)。

UVa 793 879 10158 10505 10583 10608 10685

Empty Set: 空集合

之前我們都未處理空集合。現在我們要改良原本的方法,讓它可以處理空集合,而效率仍然保持一樣。

先將資料結構做點改變。現在將陣列的第0格當作是一個空集合,不代表任何人。總人數如果有100人,那麼就要開101格的陣列。第0格是空集合,第1格到第100格,分別代表著100個人。

現在既然有了空集合,便可將頭目的老大設定為空集合,更具義理。也就是說,初始化時要將陣列的初始值都改成0。

多了空集合,就要另外考慮空集合做聯集時的影響。不管什麼集合,只要和空集合作聯集,集合都不會改變。所以,凡是遇到空集合,就不必做聯集了。

其他部分大致都不變,就不另外說明了。

Disjoint Sets進階應用

Disjoint Sets進階應用(UVa 10608)

我們可將Disjoint Set運用在結交朋友上面。但是樹立敵人該怎麼做呢?

先來整理一下Disjoint-set Forest的概念:

一、每個團體的頭目只有一個。每個團體的成員,他們的頭目都一樣,頭目可以視為一個團體的代表。

二、紀錄每個團體的人數時,我們利用了一條陣列,令n[頭目] = 頭目帶領的人數。

靈感來了:如法炮製,開一條類似記錄團體人數的陣列,用來紀錄頭目的敵人是哪一個頭目,意義同於紀錄敵對團體。

初始化

一開始大家都是沒有敵人的。

樹立敵人

兩個團體的頭目互相設定彼此是敵人即可。

有一些意外狀況要考慮清楚。一、在a b互相設定為敵人之前,a b其實都各自有敵人了;二、a本來沒有敵人,b本來沒有敵人,或者a b本來都沒有敵人;三、a的敵人可能早就是b了,b的敵人也可能早就是a了,或者早就互為敵人了;四、a b互為朋友,不可能互為敵人。

第一點:在a b互相設定為敵人之前,a b其實都各自有敵人了。按照題意,a的敵人,是b的朋友,所以a b互設敵人,要將a既有的敵人,併入b的團體中;同理,也要將b既有的敵人,併入a的團體中。

第二點:a本來沒有敵人,b本來沒有敵人,或者a b本來都沒有敵人。我們的聯集程式碼可以處理空集合,沒有問題。

第三點:a的敵人可能早就是b了,b的敵人也可能早就是a了,或者早就互為敵人了。我們的聯集程式碼可以處理相同集合做聯集,沒有問題。

若是 enemy[a] == b,按照union的程式碼,enemy[a]和b不會真的做union。所以沒有必要增加程式碼。維持第二點的程式碼即可。

第四點:a b互為朋友,不可能互為敵人。這會導致矛盾,所以要修改一下程式碼,預防這個情況。

結交朋友

由於現在多了enemy陣列的關係,交友就不能單純的只做union了。

如同樹立敵人,有一些意外狀況要考慮清楚。一、在a b互相設定為朋友之前,a b其實都各自有朋友了;二、a本來沒有朋友,b本來沒有朋友,或者a b本來都沒有朋友;三、a的朋友可能早就是b了,b的朋友也可能早就是a了,或者早就互為朋友了;四、a b互為敵人,不可能互為朋友。

第一點。其實就是union的概念。

第二點。仍是union的概念。

第三點。我們的聯集程式碼可以處理相同集合做聯集,沒有問題。

第四點。這會導致矛盾,所以要修改一下程式碼。

大功告成。這裡列出類似的題目。

UVa 10158 10505 10608