岛屿问题

岛屿系列题目的核心考点就是用 DFS/BFS 算法遍历二维数组。本文分析DFS算法。

一、框架

因为二维矩阵本质上是一幅「图」,所以遍历的过程中需要一个 visited 布尔数组防止走回头路

// 二维矩阵遍历框架
func dfs(grid [][]int, i, j int, visited [][]bool) {
    // 超出索引
    if i < 0 || j < 0 || i >= len(grid) || j >= len(grid[0]) {
        return
    }
    // 已经遍历过(i, j)
    if visited[i][j] {
        return
    }
    // 进入节点(i, j)
    visited[i][j] = true
    dfs(grid, i-1, j) // 上
    dfs(grid, i+1, j) // 下
    dfs(grid, i, j-1) // 左
    dfs(grid, i, j+1) // 右
}

二、真题

200. 岛屿数量

给你一个由 '1'(陆地)和 '0'(水)组成的的二维网格,请你计算网格中岛屿的数量。岛屿总是被水包围,并且每座岛屿只能由水平方向和/或竖直方向上相邻的陆地连接形成。此外,你可以假设该网格的四条边均被水包围。
eg: 如图,岛屿数量为4


image.png
func numIslands(grid [][]byte) int {
    var result int
    for i := 0; i < len(grid); i++ {
        for j := 0; j < len(grid[0]); j++ {
            if grid[i][j] == '1' {
                result++
                dfs(grid, i, j)
            }
        }
    }
    return result
}

// 从(i, j)开始,将与之相邻的陆地都变成海水
func dfs(grid [][]byte, i, j int) {
    // 超出索引边界
    if i < 0 || j < 0 || i >= len(grid) || j >= len(grid[0]) {
        return
    }
    // 已经是海水了
    if grid[i][j] == '0' {
        return
    }
    // 将 (i, j) 变成海水
    grid[i][j] = '0'
    // 淹没上下左右的陆地
    dfs(grid, i-1, j)
    dfs(grid, i+1, j)
    dfs(grid, i, j-1)
    dfs(grid, i, j+1)
}

为什么每次遇到岛屿,都要用 DFS 算法把岛屿「淹了」呢?主要是为了省事,避免维护 visited 数组。因为 dfs 函数遍历到值为 0 的位置会直接返回,所以只要把经过的位置都设置为 0,就可以起到不走回头路的作用。

PS:这类 DFS 算法还有个别名叫做 [FloodFill 算法]

1254. 统计封闭岛屿的数目

有一个二维矩阵 grid ,每个位置要么是陆地(记号为 0 )要么是水域(记号为 1 )。我们从一块陆地出发,每次可以往上下左右 4 个方向相邻区域走,能走到的所有陆地区域,我们将其称为一座「岛屿」。如果一座岛屿 完全 由水域包围,即陆地边缘上下左右所有相邻区域都是水域,那么我们将其称为 「封闭岛屿」。请返回封闭岛屿的数目。
eg: 如下图,封闭岛屿数量为2


image.png

思路:把靠边的岛屿排除掉,剩下的就是「封闭岛屿」

func closedIsland(grid [][]int) int {
    n, m := len(grid), len(grid[0])
    for j := 0; j < m; j++ {
        // 把靠上边的岛屿淹掉
        dfs(grid, 0, j)
        // 把靠下边的岛屿淹掉
        dfs(grid, n-1, j)
    }
    for i := 0; i < n; i++ {
        // 把靠左边的岛屿淹掉
        dfs(grid, i, 0)
        // 把靠右边的岛屿淹掉
        dfs(grid, i, m-1)
    }
    var result int
    for i := 0; i < n; i++ {
        for j := 0; j < m; j++ {
            if grid[i][j] == 0 {
                result++
                dfs(grid, i, j)
            }
        }
    }
    return result
}

func dfs(grid [][]int, i, j int) {
    // 超出索引边界
    if i < 0 || j < 0 || i >= len(grid) || j >= len(grid[0]) {
        return
    }
    // 已经是海水了
    if grid[i][j] == 1 {
        return
    }
    // 将 (i, j) 变成海水
    grid[i][j] = 1
    // 淹没上下左右的陆地
    dfs(grid, i-1, j)
    dfs(grid, i+1, j)
    dfs(grid, i, j-1)
    dfs(grid, i, j+1)
}

695. 岛屿的最大面积

思路:在 dfs 函数淹没岛屿的同时,记录这个岛屿的面积,即 dfs 函数设置返回值,记录每次淹没的陆地的个数。

func maxAreaOfIsland(grid [][]int) int {
    var maxArea int
    for i := 0; i < len(grid); i++ {
        for j := 0; j < len(grid[0]); j++ {
            if grid[i][j] == 1 {
                maxArea = max(maxArea, dfs(grid, i, j))
            }
        }
    }
    return maxArea
}

func dfs(grid [][]int, i, j int) int {
    // 超出索引边界
    if i < 0 || j < 0 || i >= len(grid) || j >= len(grid[0]) {
        return 0
    }
    // 已经是海水了
    if grid[i][j] == 0 {
        return 0
    }
    // 将 (i, j) 变成海水
    grid[i][j] = 0
    // 淹没上下左右的陆地
    return dfs(grid, i, j-1) + dfs(grid, i, j+1) + dfs(grid, i-1, j) + dfs(grid, i+1, j) + 1
}

func max(a, b int) int {
    if a > b {
        return a
    }
    return b
}

1905. 统计子岛屿

eg:如下图,子岛屿数量为红色部分,3个

image.png

思路:当岛屿 B 中所有陆地在岛屿 A 中也是陆地的时候,岛屿 B 是岛屿 A 的子岛。反过来说,如果岛屿 B 中存在一片陆地,在岛屿 A 的对应位置是海水,那么岛屿 B 就不是岛屿 A 的子岛。那么,我们只要遍历 grid2 中的所有岛屿,把那些不可能是子岛的岛屿排除掉,剩下的就是子岛。

func countSubIslands(grid1 [][]int, grid2 [][]int) int {
    // 岛屿2中存在一片陆地,在岛屿1的对应位置是海水,那么岛屿2就不是岛屿1的子岛
    for i := 0; i < len(grid2); i++ {
        for j := 0; j < len(grid2[0]); j++ {
            if grid1[i][j] == 0 && grid2[i][j] == 1 {
                dfs(grid2, i, j)
            }
        }
    }

    var result int
    for i := 0; i < len(grid2); i++ {
        for j := 0; j < len(grid2[0]); j++ {
            if grid2[i][j] == 1 {
                result++
                dfs(grid2, i, j)
            }
        }
    }
    return result
}

func dfs(grid [][]int, i, j int) {
    // 超出索引边界
    if i < 0 || j < 0 || i >= len(grid) || j >= len(grid[0]) {
        return
    }
    // 已经是海水了
    if grid[i][j] == 0{
        return
    }
    // 将 (i, j) 变成海水
    grid[i][j] = 0
    // 淹没上下左右的陆地
    dfs(grid, i-1, j)
    dfs(grid, i+1, j)
    dfs(grid, i, j-1)
    dfs(grid, i, j+1)
}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,132评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,802评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,566评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,858评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,867评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,695评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,064评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,705评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,915评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,677评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,796评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,432评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,041评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,992评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,223评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,185评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,535评论 2 343

推荐阅读更多精彩内容