零基礎徹底弄懂"並查集"

咱們從一個故事說起——解密犯罪團伙。

快過年了,犯罪分子們也開始爲年終獎“奮鬥”了,小哼的家鄉出現了多次搶劫事件。由於強盜人數過於龐大,作案頻繁,警方想查清楚到底有幾個犯罪團伙實在是太不容易了,不過警察叔叔還是蒐集到了一些線索,需要咱們幫忙分析一下。

現在有11個強盜。

1號強盜與2號強盜是同夥。

3號強盜與4號強盜是同夥。

5號強盜與2號強盜是同夥。

4號強盜與6號強盜是同夥。

2號強盜與6號強盜是同夥。

7號強盜與11號強盜是同夥。

8號強盜與7號強盜是同夥。

9號強盜與7號強盜是同夥。

9號強盜與11號強盜是同夥。

1號強盜與6號強盜是同夥。

有一點需要注意:強盜同夥的同夥也是同夥。你能幫助警方查出有多少個獨立的犯罪團伙嗎?

要想解決這個問題,首先我們假設這11個強盜相互是不認識的,他們各自爲政,每個人都是首領,他們只聽從自己的。之後我們將通過警方提供的線索,一步步地來“合併同夥”。

第一步:我們申請一個一維數組f,我們用f[1]~f[11]分別存儲1~11號強盜中每個強盜的首領“BOSS”是誰。

第二步:初始化。根據我們之前的約定,這11個強盜最開始是各自爲政的,每個強盜的BOSS就是自己。“1號強盜”的BOSS就是“1號強盜”自己,因此f[1]的值爲1。以此類推,“11號強盜”的BOSS是“11號強盜”,即f[11]的值爲11。請注意,這是很重要的一步。

我們用數組下標來表示強盜的編號

每個單元格中存儲的是每個強盜的“BOSS”是誰


第三步:開始“合併同夥”,即如果發現目前兩個強盜是同夥,則這兩個強盜是同一個犯罪團伙。現在有一個問題:合併之後誰纔是這個犯罪團伙的BOSS呢?

例如警方得到的第1條線索是“1號強盜與2號強盜是同夥”。“1號強盜”和“2號強盜”原來的BOSS都是自己,如今發現“1號強盜”和“2號強盜”其實是同一個犯罪團伙,那麼究竟是讓“1號強盜”變成“2號強盜”的BOSS,還是讓“2號強盜”變成“1號強盜”的BOSS呢?一個犯罪團伙只能有一個首領。其實無所謂,都可以。我們這裏假定左邊的強盜更厲害一些,給這個規定起個名字叫作“靠左”法則。也就是說“2號強盜”的BOSS將變成“1號強盜”。因此我們將f[2]中的數改爲1,表明“2號強盜”歸順了“1號強盜”。其實準確地說應該是原本歸順“2號強盜”的所有人都歸順了“1號強盜”纔對,只不過此時“2號強盜”只孤身一人,因此只需要將f[2]的值改爲1。不要着急,繼續往後面看,你就知道我爲什麼這樣說了,如下。

警方得到的第2條線索是“3號強盜與4號強盜是同夥”,說明“3號強盜”和“4號強盜”也是同一個犯罪團伙。根據“靠左”原則“4號強盜”歸順了“3號強盜”,所以f[4]中的值要改爲3,原理和剛纔處理第1條線索是一樣的,如下。

警方得到的第3條線索是“5號強盜”與“2號強盜”是同夥。f[5]的值是5,說明“5號強盜”的BOSS仍然是自己。f[2]的值是1,說明“2號強盜”的BOSS是“1號強盜”。根據“靠左”法則,右邊的強盜必須歸順於左邊的強盜。此時你可能會將f[2]的值改爲5。注意啦!此時如果你將f[2]的值改爲5,就是說讓“2號強盜”歸順“5號強盜”。那“1號強盜”可就不幹了,你憑什麼搶我的人?他非跟你幹一架不可。這樣會讓“2號強盜”很難選擇,我究竟歸順誰好呢?

現在我來給你支個招,嘿嘿( ^_^ )古語云“擒賊先擒王”。你直接找“2號強盜”的BOSS“1號強盜”談,讓其歸順“5號強盜”就OK了,也就是將f[1]的值改爲5。現在“2號強盜”的BOSS是“1號強盜”,而“1號強盜”的BOSS變成了“5號強盜”,即“1號強盜”帶領手下“2號強盜”歸順了“5號強盜”,這樣所有的關係信息就都保留下來了。如下。

警方得到的第4條線索是“4號強盜”與“6號強盜”是同夥。f[4]的值是3,f[6]的值是6。根據“靠左”原則,讓“6號強盜”加入“3號犯罪團伙”。我們需要將f[6]的值改爲3。原理和處理第1條和第2條線索相同。

警方得到的第5條線索是“2號強盜”與“6號強盜”是同夥。f[2]的值是1,f[1]的值是5,即“2號強盜”的大BOSS(首領)是“5號強盜”。f[6]的值是3,即“6號強盜”的BOSS是“3號強盜”。根據“靠左”原則和“擒賊先擒王”原則,讓“6號強盜”的BOSS“3號強盜”歸順“2號強盜”的大BOSS(首領)“5號強盜”。因此我們需要將f[3]的值改爲5,即讓“3號強盜”帶領其手下歸順“5號強盜”。

需要特別注意的是,此時,“5號強盜”團伙內部發生了一些變動。我們在尋找“2號強盜”的大BOSS(首領)是誰時,順帶將f[2]從1改成了5,也就是說現在“2號強盜”也變成大BOSS(首領)“5號強盜”的直屬手下了。

這就是強盜團伙的江湖規矩,誰能找到自己幫派的大BOSS(首領),誰就會被大BOSS(首領)提拔,升職加薪,成爲大BOSS(首領)的直屬手下。這種扁平化管理的方式可以有效地提高強盜團伙找大BOSS的效率,在“並查集”算法中有一個專門的術語,叫作“路徑壓縮”,具體代碼在後面展示。

細心的同學會問了,剛纔不是說如果直接把f[2]改成5,“2號強盜”和“1號強盜”之間的關係就斷了嗎?此一時,彼一時。在得到第3條線索的時候,那時候“1號強盜”和“5號強盜”的關係還沒有建立起來,如果把f[2]改爲5,“2號強盜”想要找 “1號強盜”就找不到了。但到了第5條線索的時候,“2號強盜”和“1號強盜”已經都在大BOSS(首領)“5號強盜”手下工作了,這時候將f[2]改爲5,“2號強盜”想找大BOSS(首領)“5號強盜”變得更加方便,而“1號強盜”和“2號強盜”之間的關係也沒有丟失,因此整體上效率變得更高了。


警方得到的第6條線索是“7號強盜”與“11號強盜”是同夥。f[11]的值是11,f[7]的值是7。根據“靠左”原則,讓“11號強盜”歸順“7號強盜”。我們需要將f[11]的值改爲7。

警方得到的第7條線索是“8號強盜”與“7號強盜”是同夥。f[8]的值是8,f[7]的值是7。根據“靠左”原則,讓“7號強盜”歸順“8號強盜”。我們需要將f[7]的值改爲8。


警方得到的第8條線索是“9號強盜”與“7號強盜”是同夥。f[9]的值是9,f[7]的值是8。根據“靠左”原則和“擒賊先擒王”原則,我們需要將f[8]的值改爲9。


警方得到的第9條線索是“9號強盜”與“11號強盜”是同夥。f[9]的值是9,f[11]的值是7。什麼?他們竟然不在同一個犯罪團伙中?這貌似不對吧,通過上圖可以很顯然地看出來“11號強盜”和“9號強盜”都在同一個犯罪團伙中。不過“11號強盜”並不直屬於大BOSS(首領)“9號強盜”,而是歸順在“7號強盜”的手下。現在來看看“7號強盜”又歸順了誰呢?我們發現f[7]=8,也就是說“7號強盜”歸順了“8號強盜”。而f[8]=9,也就是說“8號強盜”歸順了“9號強盜”。我們再來看看“9號強盜”有沒有歸順於別的人。發現f[9]的值還是9,太牛了!說明“9號強盜”的BOSS仍然是自己,他就是所在團伙的大BOSS(首領)。

我們剛纔模擬的過程其實就是遞歸的過程。從“11號強盜”順藤摸瓜一直找到他所在團伙的大BOSS(首領)“9號強盜”。剛纔說了,強盜團伙的江湖規矩是,誰能找到自己幫派的大BOSS(首領),就會被提拔爲首領的直屬手下。經過這一次“路徑壓縮”,一路上“11號強盜”“7號強盜”和“8號強盜”,都找到了自己的大BOSS“9號強盜”。下次再問他們的BOSS是誰的時候,他們就能馬上回答出是“9號強盜”啦。

警方得到的最後一條線索是“1號強盜”與“6號強盜”是同夥。這又是一次“路徑壓縮”的過程。f[1]是5,“1號強盜”的BOSS是“5號強盜”。f[6]是3,“6號強盜”的BOSS是“3號強盜”。f[3]是5,“3號強盜”的BOSS是“5號強盜”。說明“6號強盜”和“1號強盜”是在一個團伙中的,但他現在並不是直接跟着團伙的大BOSS(首領)“5號強盜”,而是跟着“3號強盜”。而經過這次“路徑壓縮”,他的BOSS就改成了“5號強盜”。但是注意,這一次的“路徑壓縮”只發生在“6號強盜”“3號強盜”“5號強盜”這條路徑上,團伙中的“4號強盜”不在被壓縮的路徑上,所以他的BOSS暫時不會改變,還是“3號強盜”。


好了,所有的線索分析完畢,那麼究竟有多少個犯罪團伙呢?我想你從上面的圖中一眼就可以看出來了,一共有3個犯罪團伙,分別是5號犯罪團伙(由5、1、2、3、4、6號強盜組成),9號犯罪團伙(由9、8、7、11號強盜組成)以及10號犯罪團伙(只有10號強盜一個人)。從下面這張圖就可以清晰地看出,如果f[i]=i,就表示此人是一個犯罪團伙的最高領導人,有多少個最高領導人就是有多少個“獨立的犯罪團伙”。最後數組中f[5]=5、f[9]=9、f[10]=10,因此有3個獨立的犯罪團伙。

我們剛纔模擬的過程其實就是並查集的算法。並查集通過一個一維數組來實現,其本質是維護一個森林。剛開始的時候,森林的每個點都是孤立的,也可以理解爲每個點就是一棵只有一個結點的樹,之後通過一些條件,逐漸將這些樹合併成一棵大樹。其實合併的過程就是“認爹”的過程。在“認爹”的過程中,要遵守“靠左”原則和“擒賊先擒王”原則。在每次判斷兩個結點是否已經在同一棵樹中的時候(一棵樹其實就是一個集合),也要注意必須求其根源,中間父親結點(“小BOSS”)是不能說明問題的,必須找到其祖宗(樹的根結點),判斷兩個結點的祖宗是否是同一個根結點才行。下面我將“解密犯罪團伙”這個問題模型化,並給出代碼和註釋:

  1. #include <stdio.h>   

  2. int f[1001]={0},n,m,sum=0;   

  3. //這裏是初始化,非常地重要,數組裏面存的是自己數組下標的編號就好了。   

  4. void init()   

  5. {   

  6.     int i;   

  7.     for(i=1;i<=n;i++)   

  8.         f[i]=i;   

  9.     return;

  10. }   

  11. //這是找爹的遞歸函數,不停地去找爹,直到找到祖宗爲止,其實就是去找犯罪團伙的最高領導人,

  12. //“擒賊先擒王”原則。

  13. int getf(int v)   

  14. {   

  15.     if(f[v]==v)   

  16.         return v;   

  17.     else  

  18.     {   

  19.         //這裏是路徑壓縮,每次在函數返回的時候,順帶把路上遇到的人的“BOSS”改爲最後找

  20.         //到的祖宗編號,也就是犯罪團伙的最高領導人編號。這樣可以提高今後找到犯罪團伙的

  21.         //最高領導人(其實就是樹的祖先)的速度。

  22.         f[v]=getf(f[v]);//這裏進行了路徑壓縮

  23.         return f[v];   

  24.     }   

  25. }   

  26. //這裏是合併兩子集合的函數

  27. void merge(int v,int u)   

  28. {   

  29.     int t1,t2;//t1、t2分別爲v和u的大BOSS(首領),每次雙方的會談都必須是各自最高領導人才行

  30.     t1=getf(v);

  31.     t2=getf(u);

  32.     if( t1!=t2 ) //判斷兩個結點是否在同一個集合中,即是否爲同一個祖先。

  33.     {  

  34.         f[t2]=t1;

  35.              //“靠左”原則,左邊變成右邊的BOSS。即把右邊的集合,作爲左邊集合的子集合。

  36.     }

  37.     return;

  38. }   

  39.   

  40. //請從此處開始閱讀程序,從主函數開始閱讀程序是一個好習慣。

  41. int main()   

  42. {     

  43.     int i,x,y;   

  44.     scanf("%d %d",&n,&m);   

  45.   

  46.     init();  //初始化是必須的 

  47.     for(i=1;i<=m;i++)   

  48.     {   

  49.         //開始合併犯罪團伙   

  50.         scanf("%d %d",&x,&y);   

  51.         merge(x,y);   

  52.     }


  53.     //最後掃描有多少個獨立的犯罪團伙

  54.     for(i=1;i<=n;i++)

  55.     {

  56.         if(f[i]==i)

  57.             sum++;

  58.     }

  59.     printf("%d\n",sum);

  60.     getchar();getchar();

  61.     return 0;   



複製代碼

可以輸入以下數據進行驗證。第一行n mn表示強盜的人數,m表示警方蒐集到的m條線索。接下來的m行每一行有兩個數a和b,表示強盜a和強盜b是同夥。


  1. 11 10

  2. 1 2

  3. 3 4

  4. 5 2

  5. 4 6

  6. 2 6

  7. 7 11

  8. 8 7

  9. 9 7

  10. 9 11

  11. 1 6

  12. 運行結果是:

  13. 3


複製代碼


並查集也稱爲不相交集數據結構。此算法的發展經歷了十多年,研究它的人也很多,其中Robert E. Tarjan做出了很大的貢獻。在此之前John E. Hopcroft和Jeffrey D. Ullman也進行了大量的分析。你是不是又感覺Robert E. Tarjan和John E. Hopcroft很熟悉?沒錯,就是發明了深度優先搜索的兩個人——1986年的圖靈獎得主。你看牛人們從來都不閒着的。他們到處交流,尋找合作伙伴,一起改變世界。

好了,到了本章結尾的部分啦。其實樹還有很多神奇的用法,比如:線段樹、樹狀數組、Trie樹(字典樹)、二叉搜索樹、紅黑樹(是一種平衡二叉搜索樹)等等。這些數據結構較爲複雜,感興趣的同學可以參考其他資料,或等待下一本《啊哈!算法2—偉大思維閃耀時》哈哈。

零基礎徹底弄懂"並查集"

https://bbs.codeaha.com/forum.php?mod=viewthread&tid=11223&fromuid=1

(出處: 啊哈磊_編程從這裏起步)


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章