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阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容