我們從一個故事提及——解密犯罪團伙。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」)是不能說明問題的,必須找到其祖宗(樹的根結點),判斷兩個結點的祖宗是不是同一個根結點才行。下面我將「解密犯罪團伙」這個問題模型化,並給出代碼和註釋:
#include <stdio.h>
int f[1001]={0},n,m,sum=0;
//這裏是初始化,很是地重要,數組裏面存的是本身數組下標的編號就行了。
void init()
{
int i;
for(i=1;i<=n;i++)
f[i]=i;
return;
}
//這是找爹的遞歸函數,不停地去找爹,直到找到祖宗爲止,其實就是去找犯罪團伙的最高領導人,
//「擒賊先擒王」原則。
int getf(int v)
{
if(f[v]==v)
return v;
else
{
//這裏是路徑壓縮,每次在函數返回的時候,順帶把路上遇到的人的「BOSS」改成最後找
//到的祖宗編號,也就是犯罪團伙的最高領導人編號。這樣能夠提升從此找到犯罪團伙的
//最高領導人(其實就是樹的祖先)的速度。
f[v]=getf(f[v]);//這裏進行了路徑壓縮
return f[v];
}
}
//這裏是合併兩子集合的函數
void merge(int v,int u)
{
int t1,t2;//t一、t2分別爲v和u的大BOSS(首領),每次雙方的會談都必須是各自最高領導人才行
t1=getf(v);
t2=getf(u);
if( t1!=t2 ) //判斷兩個結點是否在同一個集合中,便是否爲同一個祖先。
{
f[t2]=t1;
//「靠左」原則,左邊變成右邊的BOSS。即把右邊的集合,做爲左邊集合的子集合。
}
return;
}
//請今後處開始閱讀程序,從主函數開始閱讀程序是一個好習慣。
int main()
{
int i,x,y;
scanf("%d %d",&n,&m);
init(); //初始化是必須的
for(i=1;i<=m;i++)
{
//開始合併犯罪團伙
scanf("%d %d",&x,&y);
merge(x,y);
}
//最後掃描有多少個獨立的犯罪團伙
for(i=1;i<=n;i++)
{
if(f[i]==i)
sum++;
}
printf("%d\n",sum);
getchar();getchar();
return 0;
}
複製代碼
能夠輸入如下數據進行驗證。第一行n m,n表示強盜的人數,m表示警方蒐集到的m條線索。接下來的m行每一行有兩個數a和b,表示強盜a和強盜b是同夥。
11 10
1 2
3 4
5 2
4 6
2 6
7 11
8 7
9 7
9 11
1 6
運行結果是:
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
(出處: 啊哈磊_編程從這裏起步)