union-find解決島嶼計數問題

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

解題思路

拿到題目,如果對數據結構——“圖”的內容比較熟悉,很容易就能想到用DFSBFS來解決。通過這兩種途徑解決該問題相對不太難,并且網上有大量講解,所以我不再贅述。今天要說的是種新的算法(對我來說)——union-find 算法(見《算法》1.5節)。

union-find 算法簡介

顧名思義,union即合并,find為查找,核心就在這兩部分。完整的算法的行為就是通過不斷的find出某兩個元素分別所屬的集合,判斷他們是否是同一個,然后將不屬于同一集合的兩個元素union到同一個集合中。
那它到底是用來干嘛的呢?
嗯...書上說...它主要是用來解決動態連通性問題的。如果你不知道什么是動態連通性,還是請你查閱書籍,我不覺得自己有能力解釋的比書上更好,起碼在現階段...
好在要解決今天這個問題,你還不需要明白那些復雜的東西,讓我們進入正題。

UF 數據結構的實現(C++)

抽象圖

在此之前我們先來分析下題目:
將所給二維數組抽象成一個圖(如上),標記 1 的為陸地,標記為 0 的是水域,相鄰的陸地連接在一起成為島嶼,即圖中的連通分量。所以本題就轉換成為:求所給圖的連通分量總數

class UF {
public:    
  int count = 0; //用來記錄總的連通分量數目   
  int *id;     //數組的元素對應各頂點,存儲的內容為它自身所屬所屬連通分量的名稱  
  //這里不太容易理解,我打個比方:  
  //一群互不相識的小孩兒參加夏令營,老師把他們分成多組,每組選出一人為隊長,并要求按組排隊集合。
  //集合站隊時,隊長站在最前方,其他人通過辨認自己的隊長來選擇自己的隊伍。
  //理論上只要每人都記住隊長的樣子就能站好隊伍,但也并非必須如此。
  //也許在第一次排隊的時候小孩兒B沒記住自己的隊長,但他記住了自己前面的小朋友A,那只要A站對了位置,他就能跟著A站到正確的位置了。
  //這里id數組的元素就像是一個小孩兒,它所存儲的就是自己記住的那個跟自己在同一隊的小伙伴的樣子,當然這個人可能是隊長,也可能是其他任何一個同隊的人 。    
  
//構造函數    
  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值的時候,我們假設每塊陸地起初都是孤立的,所以有多少塊陸地,就有多少個連通分量
      }       
    }        
    int a[m*n];        
    id = a;        
    for (int i = 0; i < m * n; i++) {            
      id[i] = i;  
      //如上面所假設的,每塊陸地都是孤立的(換成排隊即指每個孩子除了自己誰都不認識,所以他們各自為營),所以自己的id里存儲的就是自己的下標       
    }   
  }        

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

  //判斷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)認q的隊長(qRoot)為自己的隊長,就是說pRoot的職務被罷免了...        
    count--; //結果當然是總隊伍數少一個~   
  }
};

利用 UF 設計算法求島嶼數

實現數據結構的時候,我們假設一開始每塊陸地是孤立的,就是把每塊陸地都當成一個島嶼。這顯然不符合題意,現在我們要做的就是找出所有相連的陸地,并把他們union到一起,直到所有相連的陸地都被包含在同一個島嶼中為止。

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

  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);           
      }       
    }   
  }  
  //由于我們是按照從上向下,從左向右的順序進行遍歷,所以當我們遍歷到某頂點時,它上面和左邊的頂點一定已經遍歷過了
  //所以事實上我們只需要對每個頂點作右和下方的判斷即可      

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

??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;
}

總結

一開始我只是看到說union-find算法可以解決島嶼問題,剛好手邊的《算法》書里有詳細講解,就想學習后自己試著實現一下。可當我花了近兩個小時終于似乎學會了這個算法,打算小試牛刀的時候,卻完全找不到用它解決島嶼問題的思路。最終我還是放棄了,到 LeetCode 上查看了大佬的 solution,并醍醐灌頂,自嘆不如。大佬是用 Java 實現的,看過之后為加深印象,也為溫習 C++,我決定重新實現一下,便有了這篇文章。寫過之后我發現自己對這個算法的理解更深了一層,希望能幫助更多的朋友。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,505評論 6 533
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,556評論 3 418
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,463評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,009評論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,778評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,218評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,281評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,436評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,969評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,795評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,993評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,537評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,229評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,659評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,917評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,687評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,990評論 2 374

推薦閱讀更多精彩內容