本文作者封承成,年僅12歲,非常感謝他的投稿。
並查集是什麼
並查集,是一種判斷「遠房親戚」的演算法。
打個比方:你身邊的某個「朋友」,很有可能就是你父親的母親的姑媽的大姨的哥哥的表妹的孫子的女兒的父親的孫子。如果給定這麼一張「家譜」(無向圖),如何判斷兩個頂點是不是「親戚」呢?用人話說,就是判斷乙個圖中兩個點是否聯通(兩個頂點相互聯通則為親戚)。
並查集是專門用來解決這樣的問題的,和搜尋不同,並查集在構建圖的時候同時就標記出了哪個「人」屬於哪個「團夥」(一團夥中的點兩兩聯通)。
並查集的操作
1. 初始化
並查集的思想是通過標記確定該頂點所在的組。
所以對於乙個n個點,m條邊的圖,我們需要新建乙個長度為n的陣列f(可以理解為father),f[n]代表點n的團夥「代表人」,當兩個點所在團夥「代表人」相同,則這兩個點所在團夥相同。
而在最開始,每個頂點間都是互相不連通的,所以每個頂點單獨屬於乙個團夥,每個頂點理所應當成為自己團夥的「代表人」,所以我們把f[n]的初始值賦為n。
2. 合併團夥
我們以連線3和1這兩個點做例子:
在連線點3和點1時,3和1形成了乙個團夥,而3和1的團夥代表人f[3]和f[1]就應該統一,具體是讓3做代表人還是讓1做代表人隨便,我們讓1做代表人。f[3] = 1,這條語句可以理解為讓1所在團夥的代表人同時成為3所在團夥的代表人。
(箭頭只是體現了f陣列中「團夥成員」和「代表人」的關係,其實這個圖是無向圖)
可是,像f[a] = b這樣合併真的對嗎?請讀者考慮這樣一種情況。
剛剛我們合併了3和1,現在我們需要合併3和2。如果按照f[a] = b這樣合併,那麼,f[3]就被賦值為了2。這樣,f[3]原本的值1就被覆蓋了,也就是說,1和3的團夥就被硬生生地「拆散」了。
下面我們換乙個例子:合併3和4。此時我們不應該令f[3] =4,應該讓f[3的團夥代表人] = (4的團夥代表人),如下圖。
這樣,合併兩個團夥的工作就完成了。總結起來就一句話:f[a的團夥代表人] = (b的團夥代表人)。
3. 查詢團夥代表人
緊接著,又乙個問題浮出水面:根據上面的公式f[a的團夥代表人] = (b的團夥代表人),可是a、b的團夥代表人怎麼求?是f[a]嗎?不不不,這裡的情況變得複雜了。大家再次考慮一種特殊情況。
在這種情況下,3的團夥代表人是誰?1還是4?正確答案是4。因為,乙個團夥中每乙個點都直接或間接地「指向」這個團夥的代表人。(1,3,4)這個團夥中,1直接地指向4,3間接地指向4,所以4才是這個團夥裡的代表人。
那麼,點x的團夥代表人怎麼求呢?我們會發現另乙個特徵,任何乙個團夥的代表人a,都有f[a] = a。很好理解,團夥代表人也是團夥的乙個成員,團夥代表人所在團夥的代表人就是它自己。
而對於其他點a,f[a]均不等於a。並且如果乙個頂點a有f[a] ≠ a,那麼這個點一定不是團夥的代表人,因為f[a]不會間接地或直接地指向a(並查集保證不會存在環)。
根據這一特性,我們可以判斷點a是否為某個團夥的代表人。
在例子中,我們想要知道1是否為團夥代表人,就可以看f[1]是否等於1,很明顯,f[1] = 4,所以1不是該團夥的代表人,我們要繼續「追本溯源」,對5進行判斷。這個過程就是一種遞迴的尋找過程。
知道了這個特性,我們就可以寫出相應的c++**(這裡還給出了迴圈版的**,根據情況使用):
int getfather(int x) {
return f[x] == x ? x : getfather(f[x]);
int getfather(int x) {
while (f[x] != x)
x = f[x];
return x;
這是乙個遞迴函式,如果f[x] = x,說明這個點已經是該團夥的代表人,直接返回就好了,如果它不是該團夥的代表人,那麼就返回自己指向的點的團夥代表人。
在求getfather(3)時,f[3] != 3,返回getfather(f[3])也就是getfather(1);
在求getfather(1)時,f[1] != 1,返回getfather(f[1])也就是getfather(4);
在求getfather(4)時,f[4] == 4,返回4。遞迴結束。最後計算出3的團夥代表人是4。
4. 查詢頂點是否在同一團夥
並查集的最後一種操作叫做查詢,就是查詢兩個點是否連通(在同一團夥)。
前面已經講了,當兩個點所在團夥「代表人」相同,則這兩個點所在團夥相同。判斷兩個點a、b在同一團夥的方法就是:
getfather(a) == getfather(b)
5. 完整**
const int n = 100; // 節點數量
int f[n];
int init() {
// 初始化
for (int i=0; i
f[i] = i;
int getfather(int x) {
// 查詢所在團夥代表人
return f[x]==x ? x : getfather(f[x]);
int merge(int a, int b) {
// 合併操作
f[getfather(a)] = getfather(b);
bool query(int a, int b) {
// 查詢操作
return getfather(a) == getfather(b);
int main() {
init();
merge(3, 1); // 3和1是親戚
merge(1, 4); // 1和4是親戚
cout << getfather(3) << endl; // 輸出3的團夥代表人+換行
cout << query(3, 1) << endl; // 輸出3和1是否是親戚+換行
並查集巧妙吧!我們既沒有構建圖,也沒有構建邊,自始至終只用到了f陣列,又優化了時間。
不要小瞧並查集**短,在很多時候並查集都會派上用場,比如著名的克魯斯卡爾演算法,就是通過並查集判斷兩個頂點是否相連的。更重要的是體會並查集的思想,用這種思想來優化**。
團夥 並查集 什麼是 「並查集」 ?
本文作者封承成,年僅12歲,非常感謝他的投稿。並查集是什麼 並查集,是一種判斷 遠房親戚 的演算法。打個比方 你身邊的某個 朋友 很有可能就是你父親的母親的姑媽的大姨的哥哥的表妹的孫子的女兒的父親的孫子。如果給定這麼一張 家譜 無向圖 如何判斷兩個頂點是不是 親戚 呢?用人話說,就是判斷乙個圖中兩個...
團夥 並查集 團夥 並查集
題目描述 1920年的芝加哥,出現了一群強盜。如果兩個強盜遇上了,那麼他們要麼是朋友,要麼是敵人。而且有一點是肯定的,就是 我朋友的朋友是我的朋友 我敵人的敵人也是我的朋友。兩個強盜是同一團夥的條件是當且僅當他們是朋友。現在給你一些關於強盜們的資訊,問你最多有多少個強盜團夥。輸入輸出格式 輸入格式 ...
團夥 並查集 並查集 團夥
2 n 1000 1 m 5000 1 p q n 試題分析 這種問題我們一般有兩種解法 打標記 多個並查集 打標記這類方法會在銀河英雄傳說中看到 那麼多個並查集如何解決呢?我們設1 n節點是記錄朋友,n 1 2 n節點是記錄敵人 既然兩個人是朋友,根據題意,我們也要把他們的敵人合起來 如果兩個人是...