【坐在馬桶上看算法】啊哈算法13:零基礎完全弄懂"並查集"

我們從一個故事提及——解密犯罪團伙。php

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

如今有11個強盜。編程

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

3號強盜與4號強盜是同夥。數據結構

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

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

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

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

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

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號犯罪團伙(由五、一、二、三、四、6號強盜組成),9號犯罪團伙(由九、八、七、11號強盜組成)以及10號犯罪團伙(只有10號強盜一我的)。從下面這張圖就能夠清晰地看出,若是f[i]=i,就表示此人是一個犯罪團伙的最高領導人,有多少個最高領導人就是有多少個「獨立的犯罪團伙」。最後數組中f[5]=五、f[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;//t一、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

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

相關文章
相關標籤/搜索