互斥集-Disjoint Sets

本文详细解释了Disjoint Sets的概念及其在并查集算法中的应用,包括union、find、split等操作,以及如何通过数组实现Disjoint Sets,并提供了解决UVa系列问题的实例。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Disjoint Sets

「互斥集」的意思是一堆集合們,大家擁有的元素都不相同,也就是說這些集合們之間都沒有交集。

A = {1, 3, 7, 8}
B = {4, 5}
C = {2}
A、B、C構成Disjoint sets。

D = {1, 2, 3}
A、B、C、D不是Disjoint sets。

舉例來說,有十個學生,要製作分組報告,分成四組,這四組就是 Disjoint sets 。

甲君、乙君、丙君、丁君、戊君、己君、庚君、辛君、壬君、癸君
共十人,分成了四組:

第一組:甲君、丙君、辛君、壬君
第二組:乙君
第三組:丁君、戊君、己君
第四組:庚君、癸君

這四組構成Disjoint sets。

union 、 find 、 split

由於集合們都沒有交集,因此諸如交集運算、差集運算等等結果很明顯的運算,就不必特別說明。這裡只談 union 、 find 、 split 這三個運算: union 就是將兩個集合做聯集,合併成一個集合。 find 就是找找看一個元素是在哪個集合裡面。 split 就是把一個集合拆成兩個集合。

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

Disjoint Sets:
Array

程度★ 難度★

Disjoint-sets Array

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

  1. int g[10];  // 每個人所屬的團體編號
  2. g[0] = 0;   // 第 0 人在第 0 團
  3. g[1] = 0;   // 第 1 人在第 0 團
  4. g[2] = 1;   // 第 2 人在第 1 團
  5. g[9] = 7;   // 第 9 人在第 7 團

初始化

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

  1. void initialize()
  2. {
  3.     for (int x=0; x<10; x++)
  4.         g[x] = x;
  5. }

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

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

  1. void union(int x, int y)
  2. {
  3.     // 要是 x y 不同團,才設成同團,以節省時間。
  4.     if (g[x] != g[y])
  5.     {
  6.         int gmin = min(g[x], g[y]); // 團體編號較小者
  7.         int gmax = max(g[x], g[y]); // 團體編號較大者
  8.         // 把所有與 x y 同團的人,都填上同一個團體編號。
  9.         // 設定為團體編號較小者,
  10.         // 讓所有團體編號維持是從零0開始的連續數字。
  11.         for (int i=0; i<10; i++)
  12.             if (g[i] == max)
  13.                 g[i] = min;
  14.     }
  15. }
  1. void union(int x, int y)
  2. {
  3.     // 精簡程式碼結構
  4.     if (g[x] == g[y]) return;
  5.     // 為了精簡程式碼,不管團體編號大小了。
  6.     int gx = g[x], gy = g[y];
  7.     for (int i=0; i<10; i++)
  8.         if (g[i] == gx)
  9.             g[i] = gy;
  10. }

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

直接看團體編號即可。

  1. int find(int x)
  2. {
  3.     return g[x];
  4. }

Equivalent Relation: 兩個人是否同團?

直接看團體編號即可。

  1. bool equivlence(int x, int y)
  2. {
  3.     return g[x] == g[y];
  4. }

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

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

  1. int groups = 10;    // 團體數
  2. void union(int x, int y)
  3. {
  4.     if (g[x] == g[y]) return;
  5.     groups--;   // 兩團合併成一團,總團體數就會減少一團。
  6.     int gx = g[x], gy = g[y];
  7.     for (int i=0; i<10; i++)
  8.         if (g[i] == gx)
  9.             g[i] = y;
  10. }

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

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

  1. // 計算出團體編號為 gn 的人數
  2. int cardinality(int gn)
  3. {
  4.     int people = 0;
  5.     for (int i=0; i<10; i++)
  6.         if (g[i] == gn)
  7.             people++;
  8.     return people;
  9. }

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

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

  1. int n[10];  // 每個團體的人數
  2. void initialize()
  3. {
  4.     for (int i=0; i<10; i++)
  5.     {
  6.         g[i] = i;
  7.         n[i] = 1;   // 團體編號從 0 到 9,每團都是一個人。
  8.     }
  9. }
  10. void union(int x, int y)
  11. {
  12.     if (g[x] == g[y]) return;
  13.     groups--;
  14.     int gx = g[x], gy = g[y];
  15.     for (int i=0; i<10; i++)
  16.         if (g[i] == gx)
  17.         {
  18.             g[i] = gy;
  19.             n[gx]--;    // 團體編號為 gx 的人數減少一人
  20.             n[gy]++;    // 團體編號為 gy 的人數增加一人
  21.         }
  22. }
  23. int cardinality(int gn)
  24. {
  25.     return n[gn];
  26. }

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

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

  1. void union(int x, int y)
  2. {
  3.     if (g[x] == g[y]) return;
  4.     groups--;
  5.     int glarge = g[x], gsmall = g[y];
  6.     if (n[x] < n[y]) swap(glarge, gsmall);
  7.     for (int i=0; i<10; i++)
  8.         if (g[i] == gsmall)
  9.         {
  10.             g[i] = glarge;
  11.             n[gsmall]--;
  12.             n[glarge]++;
  13.         }
  14. }

Singleton Set: 團體是否合併過?

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

  1. bool singleton(int x)
  2. {
  3.     return member[x] == 1;
  4. }

時間複雜度

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:
Circular Linked List

程度★ 難度★★

Disjoint Sets:
Forest

程度★ 難度★★

Disjoint-sets Forest

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

  1. int g[10];  // 紀錄每個人的老大
  2. g[0] = 0;   // 第 0 人的老大是第 0 人
  3. g[1] = 0;   // 第 1 人的老大是第 0 人
  4. g[2] = 1;   // 第 2 人在老大是第 1 人
  5. g[9] = 3;   // 第 9 人在老大是第 8 人

有一點像是老鼠會,也可以看作是圖論所提到的有根樹(rooted tree)。以萬流歸宗的方式,來代表這個人是團體的大頭目。團體的所有成員,他們往上追溯之後,會是同一個頭目。一個團體中,也只會有一個頭目,由他來支配團體、作為團體的代表。

一個團體就像是一棵分支很複雜的有根樹。這些團體構成了一叢森林,故名Disjoint-sets Forest。

各位可能會有一個疑問:一個團體之中,每個人都有一個頭目,那麼頭目的老大是誰呢?可以姑且設定成自己:

初始化

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

  1. int p[10];
  2. void initialize()
  3. {
  4.     for (int x=0; x<10; x++)
  5.         p[x] = x;
  6. }

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

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

  1. int find(int x)
  2. {
  3.     // 當 x 不是頭目,就繼續追本溯源,直到找到頭目。
  4.     while (x != p[x])
  5.         x = p[x];
  6.     return x;
  7. }
  1. int find(int x)
  2. {
  3.     return x == p[x] ? x : find(p[x]);
  4. }

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

  1. int find(int x)
  2. {
  3.     return x == p[x] ? x : (p[x] = find(p[x]));
  4. }

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

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

  1. void union(int x, int y)
  2. {
  3.     p[find(x)] = y;
  4. }

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

  1. void union(int x, int y)
  2. {
  3.     p[find(x)] = find(y);
  4. }

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

Equivalent Relation: 兩個人是否同團?

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

  1. bool equivalence(int x, int y)
  2. {
  3.     return find(x) == find(y);
  4. }

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

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

  1. int groups = 10;    // 團體數
  2. void union(int x, int y)
  3. {
  4.     x = find(x); y = find(y);
  5.     if (x == y) return;
  6.     groups--;   // 兩團合併成一團,總團體數就會減少一團。
  7.     p[x] = y;
  8. }

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

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

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

  1. int n[10];  // 每個頭目帶領的人數
  2. void initialize()
  3. {
  4.     for (int i=0; i<10; i++)
  5.     {
  6.         g[i] = i;
  7.         n[i] = 1;   // 頭目有第 0 到第 9 人,每團都是一個人。
  8.     }
  9. }
  10. void union(int x, int y)
  11. {
  12.     x = find(x); y = find(y);
  13.     if (x == y) return;
  14.     groups--;
  15.     n[y] += n[x];   // 新頭目吸收人數
  16.     n[x] = 0;       // 舊頭目不再帶領人
  17.     p[x] = y;
  18. }
  19. int cardinality(int x)
  20. {
  21.     return n[find(x)];
  22. }

Singleton Set: 團體是否合併過?

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

  1. bool singleton(int x)
  2. {
  3.     return n[find(x)] == 1;
  4. }

時間複雜度

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

空間複雜度

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

UVa 793 879 10158 10505 10583 10608 10685 11987

Empty Set: 空集合

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

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

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

  1. int g[10+1];
  2. void initialize()
  3. {
  4.     for (int x=0; x<10+1; x++)
  5.         p[x] = 0;
  6. }
  1. bool empty(int x)
  2. {
  3.     return x == 0;
  4. }

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

  1. void union(int x, int y)
  2. {
  3.     x = find(x); y = find(y);
  4.     if (x == y || x == 0 || y == 0) return;
  5.     p[x] = y;
  6. }

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值