union-find解決島嶼計數(shù)問題

Number of Islands

題目描述

Given a 2d grid map of '1' s (land) and '0' s (water), count the number of islands. An island is surrounded by water and is formed by connecting adjacent lands horizontally or vertically. You may assume all four edges of the grid are all surrounded by water.
Example 1:

11110
11010
11000
00000

Answer: 1
Example 2:

11000
11000
00100
00011

Answer: 3

解題思路

拿到題目,如果對數(shù)據(jù)結(jié)構(gòu)——“圖”的內(nèi)容比較熟悉,很容易就能想到用DFSBFS來解決。通過這兩種途徑解決該問題相對不太難币砂,并且網(wǎng)上有大量講解,所以我不再贅述裹虫。今天要說的是種新的算法(對我來說)——union-find 算法(見《算法》1.5節(jié))恐锦。

union-find 算法簡介

顧名思義,union即合并役听,find為查找颓鲜,核心就在這兩部分表窘。完整的算法的行為就是通過不斷的find出某兩個元素分別所屬的集合,判斷他們是否是同一個甜滨,然后將不屬于同一集合的兩個元素union到同一個集合中乐严。
那它到底是用來干嘛的呢?
嗯...書上說...它主要是用來解決動態(tài)連通性問題的衣摩。如果你不知道什么是動態(tài)連通性昂验,還是請你查閱書籍,我不覺得自己有能力解釋的比書上更好艾扮,起碼在現(xiàn)階段...
好在要解決今天這個問題既琴,你還不需要明白那些復(fù)雜的東西,讓我們進(jìn)入正題栏渺。

UF 數(shù)據(jù)結(jié)構(gòu)的實現(xiàn)(C++)

抽象圖

在此之前我們先來分析下題目:
將所給二維數(shù)組抽象成一個圖(如上)呛梆,標(biāo)記 1 的為陸地,標(biāo)記為 0 的是水域磕诊,相鄰的陸地連接在一起成為島嶼,即圖中的連通分量纹腌。所以本題就轉(zhuǎn)換成為:求所給圖的連通分量總數(shù)霎终。

class UF {
public:    
  int count = 0; //用來記錄總的連通分量數(shù)目   
  int *id;     //數(shù)組的元素對應(yīng)各頂點,存儲的內(nèi)容為它自身所屬所屬連通分量的名稱  
  //這里不太容易理解升薯,我打個比方:  
  //一群互不相識的小孩兒參加夏令營莱褒,老師把他們分成多組,每組選出一人為隊長涎劈,并要求按組排隊集合广凸。
  //集合站隊時,隊長站在最前方蛛枚,其他人通過辨認(rèn)自己的隊長來選擇自己的隊伍谅海。
  //理論上只要每人都記住隊長的樣子就能站好隊伍,但也并非必須如此蹦浦。
  //也許在第一次排隊的時候小孩兒B沒記住自己的隊長扭吁,但他記住了自己前面的小朋友A,那只要A站對了位置盲镶,他就能跟著A站到正確的位置了侥袜。
  //這里id數(shù)組的元素就像是一個小孩兒,它所存儲的就是自己記住的那個跟自己在同一隊的小伙伴的樣子溉贿,當(dāng)然這個人可能是隊長枫吧,也可能是其他任何一個同隊的人 。    
  
//構(gòu)造函數(shù)    
  UF(int m, int n, vector<vector<char>>& grid) {  
    for (int i = 0; i < m; i++) {           
      for (int j = 0; j < n; j++) {                
        if (grid[i][j] == '1') count++;  
        //初始化count值的時候宇色,我們假設(shè)每塊陸地起初都是孤立的九杂,所以有多少塊陸地闽寡,就有多少個連通分量
      }       
    }        
    int a[m*n];        
    id = a;        
    for (int i = 0; i < m * n; i++) {            
      id[i] = i;  
      //如上面所假設(shè)的,每塊陸地都是孤立的(換成排隊即指每個孩子除了自己誰都不認(rèn)識尼酿,所以他們各自為營)爷狈,所以自己的id里存儲的就是自己的下標(biāo)       
    }   
  }        

  //尋找p所屬的連通分量的名稱(換成排隊即尋找p小孩兒的隊長)    
  int find(int p) {           
    while (p != id[p]) {             
      id[p] = id[id[p]];            
      p = id[p];       
    }        
    return p;   
  }  
  //當(dāng)然隊長只需要認(rèn)識自己,站在原地不動就好裳擎,所以如果id[p]==p涎永,那他就是隊長,否則就說明他(id[p])只是P小孩記住的那個同隊的小伙伴鹿响。
  //為了找到隊長羡微,需要再問id[p]小朋友他記住的那個同隊的小伙伴(id[id[p]])是不是隊長了。
  //一直這么問下去惶我,總會找到隊長本人的(畢竟不聽話的搗蛋鬼只是少數(shù)呀)        

  //判斷p妈倔,q是否屬于同一連通分量(即兩塊陸地是否相連...或者兩個小孩是不是同一隊的)    
  bool isConnected(int p, int q) {         
    int pRoot = find(p); //p的隊長        
    int qRoot = find(q); //q的隊長        
    if (pRoot != qRoot)           
      return false; //若兩個隊長不是同一個人,則他們不同隊        
    else           
      return true; //否則同隊    
  }        

  //合并p绸贡,q所屬的兩個連通分量(或者說把兩隊小孩組成一隊)    
  void myUnion(int p, int q) {        
    int pRoot = find(p);        
    int qRoot = find(q);        
    if (pRoot == qRoot) return;        
    id[pRoot] = qRoot; //讓p的隊長(pRoot)認(rèn)q的隊長(qRoot)為自己的隊長盯蝴,就是說pRoot的職務(wù)被罷免了...        
    count--; //結(jié)果當(dāng)然是總隊伍數(shù)少一個~   
  }
};

利用 UF 設(shè)計算法求島嶼數(shù)

實現(xiàn)數(shù)據(jù)結(jié)構(gòu)的時候,我們假設(shè)一開始每塊陸地是孤立的听怕,就是把每塊陸地都當(dāng)成一個島嶼捧挺。這顯然不符合題意,現(xiàn)在我們要做的就是找出所有相連的陸地尿瞭,并把他們union到一起闽烙,直到所有相連的陸地都被包含在同一個島嶼中為止。

//求總的島嶼數(shù)(即其中的連通分量總數(shù))
int numIslands(vector<vector<char>>& grid) {    
  if (grid.size() == 0 || grid[0].size() == 0)       
    return 0;  //如果沒有陸地声搁,當(dāng)然就沒有島嶼...    

  int m = (int)grid.size(), n = (int)grid[0].size();    
  UF uf = UF(m, n, grid);      

  //從上向下黑竞,從左向右地遍歷整個圖,遇到陸地疏旨,就把它和自己周圍的(上下左右四個方向)陸地or島嶼合并
  for (int i = 0; i < m; i++) {        
    for (int j = 0; j < n; j++) {            
      if (grid[i][j] == '0') continue;            
      int p = i * n + j;            
      int q;            
      if (i < m - 1 && grid[i + 1][j] == '1') { //右邊相鄰                
        q = p + n;                
        uf.myUnion(p, q);           
      }            
      if (j < n - 1 && grid[i][j + 1] == '1') { //下邊相鄰                
        q = p + 1;                
        uf.myUnion(p, q);           
      }       
    }   
  }  
  //由于我們是按照從上向下很魂,從左向右的順序進(jìn)行遍歷,所以當(dāng)我們遍歷到某頂點時充石,它上面和左邊的頂點一定已經(jīng)遍歷過了
  //所以事實上我們只需要對每個頂點作右和下方的判斷即可      

  return uf.count; 
  //每次union都會count--莫换,所以當(dāng)完整遍歷整個圖之后,count就是我們需要的島嶼數(shù)量了
}

??int main(int argc, const char * argv[]) {    
  vector<vector<char>> grid;    
  string s[4] = {"11000", "11000", "00100", "00011"};    
  int i = 0;    while (i < 4) {        
    vector<char> line(s[i].begin(), s[i].begin() + s[i].length());        
    grid.push_back(line);        
    ++i;   
  }    
  cout << numIslands(grid) << endl;    
  return 0;
}

總結(jié)

一開始我只是看到說union-find算法可以解決島嶼問題骤铃,剛好手邊的《算法》書里有詳細(xì)講解拉岁,就想學(xué)習(xí)后自己試著實現(xiàn)一下《枧溃可當(dāng)我花了近兩個小時終于似乎學(xué)會了這個算法喊暖,打算小試牛刀的時候,卻完全找不到用它解決島嶼問題的思路撕瞧。最終我還是放棄了陵叽,到 LeetCode 上查看了大佬的 solution狞尔,并醍醐灌頂,自嘆不如巩掺。大佬是用 Java 實現(xiàn)的偏序,看過之后為加深印象,也為溫習(xí) C++胖替,我決定重新實現(xiàn)一下研儒,便有了這篇文章。寫過之后我發(fā)現(xiàn)自己對這個算法的理解更深了一層独令,希望能幫助更多的朋友端朵。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市燃箭,隨后出現(xiàn)的幾起案子冲呢,更是在濱河造成了極大的恐慌,老刑警劉巖招狸,帶你破解...
    沈念sama閱讀 211,265評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件敬拓,死亡現(xiàn)場離奇詭異,居然都是意外死亡瓢颅,警方通過查閱死者的電腦和手機恩尾,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,078評論 2 385
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來挽懦,“玉大人,你說我怎么就攤上這事木人⌒攀粒” “怎么了?”我有些...
    開封第一講書人閱讀 156,852評論 0 347
  • 文/不壞的土叔 我叫張陵醒第,是天一觀的道長渔嚷。 經(jīng)常有香客問我,道長稠曼,這世上最難降的妖魔是什么形病? 我笑而不...
    開封第一講書人閱讀 56,408評論 1 283
  • 正文 為了忘掉前任,我火速辦了婚禮霞幅,結(jié)果婚禮上漠吻,老公的妹妹穿的比我還像新娘。我一直安慰自己司恳,他們只是感情好途乃,可當(dāng)我...
    茶點故事閱讀 65,445評論 5 384
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著扔傅,像睡著了一般耍共。 火紅的嫁衣襯著肌膚如雪烫饼。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,772評論 1 290
  • 那天试读,我揣著相機與錄音杠纵,去河邊找鬼。 笑死钩骇,一個胖子當(dāng)著我的面吹牛比藻,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播伊履,決...
    沈念sama閱讀 38,921評論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼韩容,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了唐瀑?” 一聲冷哼從身側(cè)響起群凶,我...
    開封第一講書人閱讀 37,688評論 0 266
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎哄辣,沒想到半個月后请梢,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,130評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡力穗,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,467評論 2 325
  • 正文 我和宋清朗相戀三年毅弧,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片当窗。...
    茶點故事閱讀 38,617評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡够坐,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出崖面,到底是詐尸還是另有隱情元咙,我是刑警寧澤,帶...
    沈念sama閱讀 34,276評論 4 329
  • 正文 年R本政府宣布巫员,位于F島的核電站庶香,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏简识。R本人自食惡果不足惜赶掖,卻給世界環(huán)境...
    茶點故事閱讀 39,882評論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望七扰。 院中可真熱鬧奢赂,春花似錦、人聲如沸戳寸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,740評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽疫鹊。三九已至袖瞻,卻和暖如春司致,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背聋迎。 一陣腳步聲響...
    開封第一講書人閱讀 31,967評論 1 265
  • 我被黑心中介騙來泰國打工脂矫, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人霉晕。 一個月前我還...
    沈念sama閱讀 46,315評論 2 360
  • 正文 我出身青樓庭再,卻偏偏與公主長得像,于是被迫代替她去往敵國和親牺堰。 傳聞我的和親對象是個殘疾皇子拄轻,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,486評論 2 348

推薦閱讀更多精彩內(nèi)容