一文秒杀所有岛屿题目

读完本文,你不仅学会了算法套路,还可以顺便解决如下题目:

LeetCode 力扣 难度
1020. Number of Enclaves 1020. 飞地的数量 🟠
1254. Number of Closed Islands 1254. 统计封闭岛屿的数目 🟠
1905. Count Sub Islands 1905. 统计子岛屿 🟠
200. Number of Islands 200. 岛屿数量 🟠
694. Number of Distinct Islands🔒 694. 不同岛屿的数量🔒 🟠
695. Max Area of Island 695. 岛屿的最大面积 🟠
- 剑指 Offer II 105. 岛屿的最大面积 🟠

———–

岛屿系列算法问题是经典的面试高频题,虽然基本的问题并不难,但是这类问题有一些有意思的扩展,比如求子岛屿数量,求形状不同的岛屿数量等等,本文就来把这些问题一网打尽。

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

本文主要来讲解如何用 DFS 算法来秒杀岛屿系列题目,不过用 BFS 算法的核心思路是完全一样的,无非就是把 DFS 改写成 BFS 而已。

那么如何在二维矩阵中使用 DFS 搜索呢?如果你把二维矩阵中的每一个位置看做一个节点,这个节点的上下左右四个位置就是相邻节点,那么整个矩阵就可以抽象成一幅网状的「图」结构。

根据 学习数据结构和算法的框架思维,完全可以根据二叉树的遍历框架改写出二维矩阵的 DFS 代码框架:

// 二叉树遍历框架
void traverse(TreeNode root) {
    traverse(root.left);
    traverse(root.right);
}

// 二维矩阵遍历框架
void dfs(int[][] grid, int i, int j, boolean[][] visited) {
    int m = grid.length, n = grid[0].length;
    if (i < 0 || j < 0 || i >= m || j >= n) {
        // 超出索引边界
        return;
    }
    if (visited[i][j]) {
        // 已遍历过 (i, j)
        return;
    }
    // 进入节点 (i, j)
    visited[i][j] = true;
    dfs(grid, i - 1, j, visited); // 上
    dfs(grid, i + 1, j, visited); // 下
    dfs(grid, i, j - 1, visited); // 左
    dfs(grid, i, j + 1, visited); // 右
}
// 注意:cpp 代码由 chatGPT🤖 根据我的 java 代码翻译,旨在帮助不同背景的读者理解算法逻辑。
// 本代码还未经过力扣测试,仅供参考,如有疑惑,可以参照我写的 java 代码对比查看。

void traverse(TreeNode* root) {
    traverse(root->left);
    traverse(root->right);
}

void dfs(vector<vector<int>>& grid, int i, int j, vector<vector<bool>>& visited) {
    int m = grid.size(), n = grid[0].size();
    if (i < 0 || j < 0 || i >= m || j >= n) {
        return;
    }
    if (visited[i][j]) {
        return;
    }
    visited[i][j] = true;
    dfs(grid, i - 1, j, visited); // 上
    dfs(grid, i + 1, j, visited); // 下
    dfs(grid, i, j - 1, visited); // 左
    dfs(grid, i, j + 1, visited); // 右
}
# 注意:python 代码由 chatGPT🤖 根据我的 java 代码翻译,旨在帮助不同背景的读者理解算法逻辑。
# 本代码还未经过力扣测试,仅供参考,如有疑惑,可以参照我写的 java 代码对比查看。

# 二叉树遍历框架
def traverse(root: TreeNode):
    if not root:
        return
    traverse(root.left)
    traverse(root.right)

# 二维矩阵遍历框架
def dfs(grid: List[List[int]], i: int, j: int, visited: List[List[bool]]):
    m, n = len(grid), len(grid[0])
    if i < 0 or j < 0 or i >= m or j >= n:
        # 超出索引边界
        return
    if visited[i][j]:
        # 已遍历过 (i, j)
        return
    # 进入节点 (i, j)
    visited[i][j] = True
    dfs(grid, i - 1, j, visited) # 上
    dfs(grid, i + 1, j, visited) # 下
    dfs(grid, i, j - 1, visited) # 左
    dfs(grid, i, j + 1, visited) # 右
// 注意:go 代码由 chatGPT🤖 根据我的 java 代码翻译,旨在帮助不同背景的读者理解算法逻辑。
// 本代码还未经过力扣测试,仅供参考,如有疑惑,可以参照我写的 java 代码对比查看。

// 二叉树遍历框架
func traverse(root *TreeNode) {
    traverse(root.Left)
    traverse(root.Right)
}

// 二维矩阵遍历框架
func dfs(grid [][]int, i int, j int, visited [][]bool) {
    m, n := len(grid), len(grid[0])
    if i < 0 || j < 0 || i >= m || j >= n {
        // 超出索引边界
        return
    }
    if visited[i][j] {
        // 已遍历过 (i, j)
        return
    }
    // 进入节点 (i, j)
    visited[i][j] = true
    dfs(grid, i-1, j, visited) // 上
    dfs(grid, i+1, j, visited) // 下
    dfs(grid, i, j-1, visited) // 左
    dfs(grid, i, j+1, visited) // 右
}
// 注意:javascript 代码由 chatGPT🤖 根据我的 java 代码翻译,旨在帮助不同背景的读者理解算法逻辑。
// 本代码还未经过力扣测试,仅供参考,如有疑惑,可以参照我写的 java 代码对比查看。

var traverse = function(root) {
    traverse(root.left);
    traverse(root.right);
};

var dfs = function(grid, i, j, visited) {
    var m = grid.length, n = grid[0].length;
    if (i < 0 || j < 0 || i >= m || j >= n) {
        // 超出索引边界
        return;
    }
    if (visited[i][j]) {
        // 已遍历过 (i, j)
        return;
    }
    // 进入节点 (i, j)
    visited[i][j] = true;
    dfs(grid, i - 1, j, visited); // 上
    dfs(grid, i + 1, j, visited); // 下
    dfs(grid, i, j - 1, visited); // 左
    dfs(grid, i, j + 1, visited); // 右
};

因为二维矩阵本质上是一幅「图」,所以遍历的过程中需要一个 visited 布尔数组防止走回头路,如果你能理解上面这段代码,那么搞定所有岛屿系列题目都很简单。

这里额外说一个处理二维数组的常用小技巧,你有时会看到使用「方向数组」来处理上下左右的遍历,和前文 union-find 算法详解 的代码很类似:

// 方向数组,分别代表上、下、左、右
int[][] dirs = new int[][]{{-1,0}, {1,0}, {0,-1}, {0,1}};

void dfs(int[][] grid, int i, int j, boolean[][] visited) {
    int m = grid.length, n = grid[0].length;
    if (i < 0 || j < 0 || i >= m || j >= n) {
        // 超出索引边界
        return;
    }
    if (visited[i][j]) {
        // 已遍历过 (i, j)
        return;
    }

    // 进入节点 (i, j)
    visited[i][j] = true;
    // 递归遍历上下左右的节点
    for (int[] d : dirs) {
        int next_i = i + d[0];
        int next_j = j + d[1];
        dfs(grid, next_i, next_j, visited);
    }
    // 离开节点 (i, j)
}
// 注意:cpp 代码由 chatGPT🤖 根据我的 java 代码翻译,旨在帮助不同背景的读者理解算法逻辑。
// 本代码还未经过力扣测试,仅供参考,如有疑惑,可以参照我写的 java 代码对比查看。

vector<vector<int>> dirs = {{-1, 0}, {1, 0}, {0, -1}, {0, 1}};

void dfs(vector<vector<int>>& grid, int i, int j, vector<vector<bool>>& visited) {
    int m = grid.size(), n = grid[0].size();
    if (i < 0 || j < 0 || i >= m || j >= n) {
        // 超出索引边界
        return;
    }
    if (visited[i][j]) {
        // 已遍历过 (i, j)
        return;
    }

    // 进入节点 (i, j)
    visited[i][j] = true;
    // 递归遍历上下左右的节点
    for (auto &d : dirs) {
        int next_i = i + d[0];
        int next_j = j + d[1];
        dfs(grid, next_i, next_j, visited);
    }
    // 离开节点 (i, j)
}
# 注意:python 代码由 chatGPT🤖 根据我的 java 代码翻译,旨在帮助不同背景的读者理解算法逻辑。
# 本代码还未经过力扣测试,仅供参考,如有疑惑,可以参照我写的 java 代码对比查看。

# 方向数组,分别代表上、下、左、右
dirs = [[-1,0], [1,0], [0,-1], [0,1]]

def dfs(grid: List[List[int]], i: int, j: int, visited: List[List[bool]]) -> None:
    m, n = len(grid), len(grid[0])
    if i < 0 or j < 0 or i >= m or j >= n:
        # 超出索引边界
        return
    if visited[i][j]:
        # 已遍历过 (i, j)
        return

    # 进入节点 (i, j)
    visited[i][j] = True
    # 递归遍历上下左右的节点
    for d in dirs:
        next_i = i + d[0]
        next_j = j + d[1]
        dfs(grid, next_i, next_j, visited)
    # 离开节点 (i, j)
// 注意:go 代码由 chatGPT🤖 根据我的 java 代码翻译,旨在帮助不同背景的读者理解算法逻辑。
// 本代码还未经过力扣测试,仅供参考,如有疑惑,可以参照我写的 java 代码对比查看。

func dfs(grid [][]int, i int, j int, visited [][]bool) {
    var dirs = [][]int{{-1, 0}, {1, 0}, {0, -1}, {0, 1}}

    var dfsCore func(i, j int)
    dfsCore = func(i int, j int) {
        m, n := len(grid), len(grid[0])
        if i < 0 || j < 0 || i >= m || j >= n {
            // 超出索引边界
            return
        }
        if visited[i][j] {
            // 已遍历过 (i, j)
            return
        }

        // 进入节点 (i, j)
        visited[i][j] = true
        // 递归遍历上下左右的节点
        for _, d := range dirs {
            next_i := i + d[0]
            next_j := j + d[1]
            dfsCore(next_i, next_j)
        }
        // 离开节点 (i, j)
    }

    dfsCore(i, j)
}
// 注意:javascript 代码由 chatGPT🤖 根据我的 java 代码翻译,旨在帮助不同背景的读者理解算法逻辑。
// 本代码还未经过力扣测试,仅供参考,如有疑惑,可以参照我写的 java 代码对比查看。

var dirs = [[-1,0], [1,0], [0,-1], [0,1]];

function dfs(grid, i, j, visited) {
    var m = grid.length, n = grid[0].length;
    if (i < 0 || j < 0 || i >= m || j >= n) {
        // 超出索引边界
        return;
    }
    if (visited[i][j]) {
        // 已遍历过 (i, j)
        return;
    }

    // 进入节点 (i, j)
    visited[i][j] = true;
    // 递归遍历上下左右的节点
    for (var d of dirs) {
        var next_i = i + d[0];
        var next_j = j + d[1];
        dfs(grid, next_i, next_j, visited);
    }
    // 离开节点 (i, j)
}

这种写法无非就是用 for 循环处理上下左右的遍历罢了,你可以按照个人喜好选择写法。

岛屿数量

这是力扣第 200 题「 岛屿数量」,最简单也是最经典的一道问题,题目会输入一个二维数组 grid,其中只包含 0 或者 10 代表海水,1 代表陆地,且假设该矩阵四周都是被海水包围着的。

我们说连成片的陆地形成岛屿,那么请你写一个算法,计算这个矩阵 grid 中岛屿的个数,函数签名如下:

int numIslands(char[][] grid);
// 注意:cpp 代码由 chatGPT🤖 根据我的 java 代码翻译,旨在帮助不同背景的读者理解算法逻辑。
// 本代码还未经过力扣测试,仅供参考,如有疑惑,可以参照我写的 java 代码对比查看。

int numIslands(vector<vector<char>>& grid);
# 注意:python 代码由 chatGPT🤖 根据我的 java 代码翻译,旨在帮助不同背景的读者理解算法逻辑。
# 本代码还未经过力扣测试,仅供参考,如有疑惑,可以参照我写的 java 代码对比查看。

def numIslands(grid: List[List[int]]) -> int:
// 注意:go 代码由 chatGPT🤖 根据我的 java 代码翻译,旨在帮助不同背景的读者理解算法逻辑。
// 本代码还未经过力扣测试,仅供参考,如有疑惑,可以参照我写的 java 代码对比查看。

func numIslands(grid [][]byte) int {
// 注意:javascript 代码由 chatGPT🤖 根据我的 java 代码翻译,旨在帮助不同背景的读者理解算法逻辑。
// 本代码还未经过力扣测试,仅供参考,如有疑惑,可以参照我写的 java 代码对比查看。

var numIslands = function(grid) {
  // function body here
};

比如说题目给你输入下面这个 grid 有四片岛屿,算法应该返回 4:

思路很简单,关键在于如何寻找并标记「岛屿」,这就要 DFS 算法发挥作用了,我们直接看解法代码:

class Solution {
    // 主函数,计算岛屿数量
    int numIslands(char[][] grid) {
        int res = 0;
        int m = grid.length, n = grid[0].length;
        // 遍历 grid
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                if (grid[i][j] == '1') {
                    // 每发现一个岛屿,岛屿数量加一
                    res++;
                    // 然后使用 DFS 将岛屿淹了
                    dfs(grid, i, j);
                }
            }
        }
        return res;
    }

    // 从 (i, j) 开始,将与之相邻的陆地都变成海水
    void dfs(char[][] grid, int i, int j) {
        int m = grid.length, n = grid[0].length;
        if (i < 0 || j < 0 || i >= m || j >= n) {
            // 超出索引边界
            return;
        }
        if (grid[i][j] == '0') {
            // 已经是海水了
            return;
        }
        // 将 (i, j) 变成海水
        grid[i][j] = '0';
        // 淹没上下左右的陆地
        dfs(grid, i + 1, j);
        dfs(grid, i, j + 1);
        dfs(grid, i - 1, j);
        dfs(grid, i, j - 1);
    }
}
// 注意:cpp 代码由 chatGPT🤖 根据我的 java 代码翻译,旨在帮助不同背景的读者理解算法逻辑。
// 本代码还未经过力扣测试,仅供参考,如有疑惑,可以参照我写的 java 代码对比查看。

class Solution {
public:
    // 主函数,计算岛屿数量
    int numIslands(vector<vector<char>>& grid) {
        int res = 0;
        int m = grid.size(), n = grid[0].size();
        // 遍历 grid
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                if (grid[i][j] == '1') {
                    // 每发现一个岛屿,岛屿数量加一
                    res++;
                    // 然后使用 DFS 将岛屿淹了
                    dfs(grid, i, j);
                }
            }
        }
        return res;
    }

    // 从 (i, j) 开始,将与之相邻的陆地都变成海水
    void dfs(vector<vector<char>>& grid, int i, int j) {
        int m = grid.size(), n = grid[0].size();
        if (i < 0 || j < 0 || i >= m || j >= n) {
            // 超出索引边界
            return;
        }
        if (grid[i][j] == '0') {
            // 已经是海水了
            return;
        }
        // 将 (i, j) 变成海水
        grid[i][j] = '0';
        // 淹没上下左右的陆地
        dfs(grid, i + 1, j);
        dfs(grid, i, j + 1);
        dfs(grid, i - 1, j);
        dfs(grid, i, j - 1);
    }
};
# 注意:python 代码由 chatGPT🤖 根据我的 java 代码翻译,旨在帮助不同背景的读者理解算法逻辑。
# 本代码还未经过力扣测试,仅供参考,如有疑惑,可以参照我写的 java 代码对比查看。

class Solution:
    def numIslands(self, grid: List[List[str]]) -> int:
        res = 0
        m, n = len(grid), len(grid[0])
        # 遍历 grid
        for i in range(m):
            for j in range(n):
                if grid[i][j] == '1':
                    # 每发现一个岛屿,岛屿数量加一
                    res += 1
                    # 然后使用 DFS 将岛屿淹了
                    self.dfs(grid, i, j)
        return res

    # 从 (i, j) 开始,将与之相邻的陆地都变成海水
    def dfs(self, grid: List[List[str]], i: int, j: int) -> None:
        m, n = len(grid), len(grid[0])
        if i < 0 or j < 0 or i >= m or j >= n:
            # 超出索引边界
            return
        if grid[i][j] == '0':
            # 已经是海水了
            return
        # 将 (i, j) 变成海水
        grid[i][j] = '0'
        # 淹没上下左右的陆地
        self.dfs(grid, i + 1, j)
        self.dfs(grid, i, j + 1)
        self.dfs(grid, i - 1, j)
        self.dfs(grid, i, j - 1)
// 注意:go 代码由 chatGPT🤖 根据我的 java 代码翻译,旨在帮助不同背景的读者理解算法逻辑。
// 本代码还未经过力扣测试,仅供参考,如有疑惑,可以参照我写的 java 代码对比查看。

func numIslands(grid [][]byte) int {
    res := 0
    m, n := len(grid), len(grid[0])
    // 遍历 grid
    for i := 0; i < m; i++ {
        for j := 0; j < n; j++ {
            if grid[i][j] == '1' {
                // 每发现一个岛屿,岛屿数量加一
                res++
                // 然后使用DFS将岛屿淹了
                dfs(grid, i, j)
            }
        }
    }
    return res
}

// 从 (i, j) 开始,将与之相邻的陆地都变成海水
func dfs(grid [][]byte, i, j int) {
    m, n := len(grid), len(grid[0])
    if i < 0 || j < 0 || i >= m || j >= n {
        // 超出索引边界
        return
    }
    if grid[i][j] == '0' {
        // 已经是海水了
        return
    }
    // 将 (i,j) 变成海水
    grid[i][j] = '0'
    // 淹没上下左右的陆地
    dfs(grid, i+1, j)
    dfs(grid, i, j+1)
    dfs(grid, i-1, j)
    dfs(grid, i, j-1)
}
// 注意:javascript 代码由 chatGPT🤖 根据我的 java 代码翻译,旨在帮助不同背景的读者理解算法逻辑。
// 本代码还未经过力扣测试,仅供参考,如有疑惑,可以参照我写的 java 代码对比查看。

var numIslands = function(grid) {
    let res = 0;
    let m = grid.length, n = grid[0].length;
    // 遍历 grid
    for(let i = 0; i < m; i++) {
        for(let j = 0; j < n; j++) {
            if(grid[i][j] === '1') {
                // 每发现一个岛屿,岛屿数量加一
                res++;
                // 然后使用 DFS 将岛屿淹了
                dfs(grid, i, j);
            }
        }
    }
    return res;
};

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

🌈 代码可视化动画 🌈

为什么每次遇到岛屿,都要用 DFS 算法把岛屿「淹了」呢?主要是为了省事,避免维护 visited 数组

因为 dfs 函数遍历到值为 0 的位置会直接返回,所以只要把经过的位置都设置为 0,就可以起到不走回头路的作用。

这类 DFS 算法还有个别名叫做 FloodFill 算法,现在有没有觉得 FloodFill 这个名字还挺贴切的~

这个最最基本的算法问题就说到这,我们来看看后面的题目有什么花样。

封闭岛屿的数量

上一题说二维矩阵四周可以认为也是被海水包围的,所以靠边的陆地也算作岛屿。

力扣第 1254 题「 统计封闭岛屿的数目」和上一题有两点不同:

1、用 0 表示陆地,用 1 表示海水。

2、让你计算「封闭岛屿」的数目。所谓「封闭岛屿」就是上下左右全部被 1 包围的 0,也就是说靠边的陆地不算作「封闭岛屿」

函数签名如下:

int closedIsland(int[][] grid)
// 注意:cpp 代码由 chatGPT🤖 根据我的 java 代码翻译,旨在帮助不同背景的读者理解算法逻辑。
// 本代码还未经过力扣测试,仅供参考,如有疑惑,可以参照我写的 java 代码对比查看。

int closedIsland(vector<vector<int>>& grid)
# 注意:python 代码由 chatGPT🤖 根据我的 java 代码翻译,旨在帮助不同背景的读者理解算法逻辑。
# 本代码还未经过力扣测试,仅供参考,如有疑惑,可以参照我写的 java 代码对比查看。

def closedIsland(grid: List[List[int]]) -> int:
// 注意:go 代码由 chatGPT🤖 根据我的 java 代码翻译,旨在帮助不同背景的读者理解算法逻辑。
// 本代码还未经过力扣测试,仅供参考,如有疑惑,可以参照我写的 java 代码对比查看。

func closedIsland(grid [][]int) int {
// 注意:javascript 代码由 chatGPT🤖 根据我的 java 代码翻译,旨在帮助不同背景的读者理解算法逻辑。
// 本代码还未经过力扣测试,仅供参考,如有疑惑,可以参照我写的 java 代码对比查看。

var closedIsland = function(grid) {

比如题目给你输入如下这个二维矩阵:

算法返回 2,只有图中灰色部分的 0 是四周全都被海水包围着的「封闭岛屿」。

那么如何判断「封闭岛屿」呢?其实很简单,把上一题中那些靠边的岛屿排除掉,剩下的不就是「封闭岛屿」了吗

有了这个思路,就可以直接看代码了,注意这题规定 0 表示陆地,用 1 表示海水:

class Solution {
    // 主函数:计算封闭岛屿的数量
    int closedIsland(int[][] grid) {
        int m = grid.length, n = grid[0].length;
        for (int j = 0; j < n; j++) {
            // 把靠上边的岛屿淹掉
            dfs(grid, 0, j);
            // 把靠下边的岛屿淹掉
            dfs(grid, m - 1, j);
        }
        for (int i = 0; i < m; i++) {
            // 把靠左边的岛屿淹掉
            dfs(grid, i, 0);
            // 把靠右边的岛屿淹掉
            dfs(grid, i, n - 1);
        }
        // 遍历 grid,剩下的岛屿都是封闭岛屿
        int res = 0;
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                if (grid[i][j] == 0) {
                    res++;
                    dfs(grid, i, j);
                }
            }
        }
        return res;
    }

    // 从 (i, j) 开始,将与之相邻的陆地都变成海水
    void dfs(int[][] grid, int i, int j) {
        int m = grid.length, n = grid[0].length;
        if (i < 0 || j < 0 || i >= m || j >= n) {
            return;
        }
        if (grid[i][j] == 1) {
            // 已经是海水了
            return;
        }
        // 将 (i, j) 变成海水
        grid[i][j] = 1;
        // 淹没上下左右的陆地
        dfs(grid, i + 1, j);
        dfs(grid, i, j + 1);
        dfs(grid, i - 1, j);
        dfs(grid, i, j - 1);
    }
}
// 注意:cpp 代码由 chatGPT🤖 根据我的 java 代码翻译,旨在帮助不同背景的读者理解算法逻辑。
// 本代码还未经过力扣测试,仅供参考,如有疑惑,可以参照我写的 java 代码对比查看。

class Solution {
public:
    // 主函数:计算封闭岛屿的数量
    int closedIsland(vector<vector<int>>& grid) {
        int m = grid.size(), n = grid[0].size();
        for (int j = 0; j < n; j++) {
            // 把靠上边的岛屿淹掉
            dfs(grid, 0, j);
            // 把靠下边的岛屿淹掉
            dfs(grid, m - 1, j);
        }
        for (int i = 0; i < m; i++) {
            // 把靠左边的岛屿淹掉
            dfs(grid, i, 0);
            // 把靠右边的岛屿淹掉
            dfs(grid, i, n - 1);
        }
        // 遍历 grid,剩下的岛屿都是封闭岛屿
        int res = 0;
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                if (grid[i][j] == 0) {
                    res++;
                    dfs(grid, i, j);
                }
            }
        }
        return res;
    }

    // 从 (i, j) 开始,将与之相邻的陆地都变成海水
    void dfs(vector<vector<int>>& grid, int i, int j) {
        int m = grid.size(), n = grid[0].size();
        if (i < 0 || j < 0 || i >= m || j >= n) {
            return;
        }
        if (grid[i][j] == 1) {
            // 已经是海水了
            return;
        }
        // 将 (i, j) 变成海水
        grid[i][j] = 1;
        // 淹没上下左右的陆地
        dfs(grid, i + 1, j);
        dfs(grid, i, j + 1);
        dfs(grid, i - 1, j);
        dfs(grid, i, j - 1);
    }
};
# 注意:python 代码由 chatGPT🤖 根据我的 java 代码翻译,旨在帮助不同背景的读者理解算法逻辑。
# 本代码还未经过力扣测试,仅供参考,如有疑惑,可以参照我写的 java 代码对比查看。

class Solution:
    def closedIsland(self, grid: List[List[int]]) -> int:
        m, n = len(grid), len(grid[0])
        for j in range(n):
            # 把靠上边的岛屿淹掉
            self.dfs(grid, 0, j)
            # 把靠下边的岛屿淹掉
            self.dfs(grid, m - 1, j)
        for i in range(m):
            # 把靠左边的岛屿淹掉
            self.dfs(grid, i, 0)
            # 把靠右边的岛屿淹掉
            self.dfs(grid, i, n - 1)
        # 遍历 grid,剩下的岛屿都是封闭岛屿
        res = 0
        for i in range(m):
            for j in range(n):
                if grid[i][j] == 0:
                    res += 1
                    self.dfs(grid, i, j)
        return res

    # 从 (i, j) 开始,将与之相邻的陆地都变成海水
    def dfs(self, grid: List[List[int]], i: int, j: int):
        m, n = len(grid), len(grid[0])
        if i < 0 or j < 0 or i >= m or j >= n:
            return
        if grid[i][j] == 1:
            # 已经是海水了
            return
        # 将 (i, j) 变成海水
        grid[i][j] = 1
        # 淹没上下左右的陆地
        self.dfs(grid, i + 1, j)
        self.dfs(grid, i, j + 1)
        self.dfs(grid, i - 1, j)
        self.dfs(grid, i, j - 1)
// 注意:go 代码由 chatGPT🤖 根据我的 java 代码翻译,旨在帮助不同背景的读者理解算法逻辑。
// 本代码还未经过力扣测试,仅供参考,如有疑惑,可以参照我写的 java 代码对比查看。

func dfs(grid [][]int, i int, j int) {
    // 获取网格的长和宽
    m, n := len(grid), len(grid[0])
    // 判断是否越界
    if i < 0 || j < 0 || i >= m || j >= n {
        return
    }
    // 判断当前位置是否是海洋或已经遍历过
    if grid[i][j] == 1 {
        return
    }
    // 标记当前位置为已遍历
    grid[i][j] = 1
    // 向上、右、下、左四个方向遍历
    dfs(grid, i + 1, j)
    dfs(grid, i, j + 1)
    dfs(grid, i - 1, j)
    dfs(grid, i, j - 1)
}

func closedIsland(grid [][]int) int {
    // 获取网格的长和宽
    m, n := len(grid), len(grid[0])
    // 构造边界:将第一行、最后一行、第一列、最后一列的所有陆地都变成海洋
    for j := 0; j < n; j ++ {
        dfs(grid, 0, j)
        dfs(grid, m - 1, j)
    }
    for i := 0; i < m; i ++ {
        dfs(grid, i, 0)
        dfs(grid, i, n - 1)
    }
    // 遍历整个网格,找到所有封闭岛屿的数量
    res := 0
    for i := 0; i < m; i ++ {
        for j := 0; j < n; j ++ {
            if grid[i][j] == 0 { // 找到一个陆地(封闭岛屿起点)
                res ++ // 将封闭岛屿数量加1
                dfs(grid, i, j) // 将这个陆地的所有相邻陆地都标记为已遍历
            }
        }
    }
    return res
}
// 注意:javascript 代码由 chatGPT🤖 根据我的 java 代码翻译,旨在帮助不同背景的读者理解算法逻辑。
// 本代码还未经过力扣测试,仅供参考,如有疑惑,可以参照我写的 java 代码对比查看。

// var 关键字声明了一个名为 closedIsland 的函数,该函数接收一个名为 grid 的二维数组作为参数并返回一个数字
var closedIsland = function(grid) {
  // 获取二维数组的行数和列数
  const m = grid.length, n = grid[0].length;
  // 按顺序遍历四周的边界,把所有岛屿都变成海洋 i 行 0 <= j < n 和 i = m - 1,0 <= j < n
  for (let j = 0; j < n; j++) {
    dfs(grid, 0, j);
    dfs(grid, m - 1, j);
  }
  // 按顺序遍历四周的边界,把所有岛屿都变成海洋 j 列 0 <= i < m 和 i = n - 1,0 <= j < n
  for (let i = 0; i < m; i++) {
    dfs(grid, i, 0);
    dfs(grid, i, n - 1);
  }
  // 遍历 grid,统计岛屿数量
  let res = 0;
  for (let i = 0; i < m; i++) {
    for (let j = 0; j < n; j++) {
      if (grid[i][j] === 0) { // 0 表示岛屿,1 表示海洋
        res++;
        // 从当前位置开始淹没所有可达的岛屿
        dfs(grid, i, j);
      }
    }
  }
  return res;
};

// 从 (i, j) 位置开始,淹没所有可达岛屿
function dfs(grid, i, j) {
  const m = grid.length, n = grid[0].length;
  if (i < 0 || j < 0 || i >= m || j >= n) { // 位置越界,返回
    return;
  }
  if (grid[i][j] === 1) { // 当前位置是海洋,返回
    return;
  }
  grid[i][j] = 1; // 把当前位置淹没
  // 淹没上下左右相邻的岛屿
  dfs(grid, i + 1, j);
  dfs(grid, i, j + 1);
  dfs(grid, i - 1, j);
  dfs(grid, i, j - 1);
}

🌟 代码可视化动画 🌟

只要提前把靠边的陆地都淹掉,然后算出来的就是封闭岛屿了。

处理这类岛屿题目除了 DFS/BFS 算法之外,Union Find 并查集算法也是一种可选的方法,前文 Union Find 算法运用 就用 Union Find 算法解决了一道类似的问题。

这道岛屿题目的解法稍微改改就可以解决力扣第 1020 题「 飞地的数量」,这题不让你求封闭岛屿的数量,而是求封闭岛屿的面积总和。

其实思路都是一样的,先把靠边的陆地淹掉,然后去数剩下的陆地数量就行了,注意第 1020 题中 1 代表陆地,0 代表海水:

class Solution {
    int numEnclaves(int[][] grid) {
        int m = grid.length, n = grid[0].length;
        // 淹掉靠边的陆地
        for (int i = 0; i < m; i++) {
            dfs(grid, i, 0);
            dfs(grid, i, n - 1);
        }
        for (int j = 0; j < n; j++) {
            dfs(grid, 0, j);
            dfs(grid, m - 1, j);
        }

        // 数一数剩下的陆地
        int res = 0;
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                if (grid[i][j] == 1) {
                    res += 1;
                }
            }
        }

        return res;
    }

    // 和之前的实现类似
    void dfs(int[][] grid, int i, int j) {
        // ...
    }
}
// 注意:cpp 代码由 chatGPT🤖 根据我的 java 代码翻译,旨在帮助不同背景的读者理解算法逻辑。
// 本代码还未经过力扣测试,仅供参考,如有疑惑,可以参照我写的 java 代码对比查看。

class Solution {
public:
    int numEnclaves(vector<vector<int>>& grid) {
        int m = grid.size(), n = grid[0].size();
        // 淹掉靠边的陆地
        for (int i = 0; i < m; i++) {
            dfs(grid, i, 0);
            dfs(grid, i, n - 1);
        }
        for (int j = 0; j < n; j++) {
            dfs(grid, 0, j);
            dfs(grid, m - 1, j);
        }

        // 数一数剩下的陆地
        int res = 0;
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                if (grid[i][j] == 1) {
                    res += 1;
                }
            }
        }

        return res;
    }

    // 和之前的实现类似
    void dfs(vector<vector<int>>& grid, int i, int j) {
        // ...
    }
};
# 注意:python 代码由 chatGPT🤖 根据我的 java 代码翻译,旨在帮助不同背景的读者理解算法逻辑。
# 本代码还未经过力扣测试,仅供参考,如有疑惑,可以参照我写的 java 代码对比查看。

class Solution:
    def numEnclaves(self, grid: List[List[int]]) -> int:
        m, n = len(grid), len(grid[0])
        # 淹掉靠边的陆地
        for i in range(m):
            self.dfs(grid, i, 0)
            self.dfs(grid, i, n - 1)
        for j in range(n):
            self.dfs(grid, 0, j)
            self.dfs(grid, m - 1, j)

        # 数一数剩下的陆地
        res = 0
        for i in range(m):
            for j in range(n):
                if grid[i][j] == 1:
                    res += 1

        return res

    # 和之前的实现类似
    def dfs(self, grid: List[List[int]], i: int, j: int) -> None:
        if i < 0 or j < 0 or i >= len(grid) or j >= len(grid[0]) or grid[i][j] == 0:
            return
        grid[i][j] = 0
        self.dfs(grid, i - 1, j)
        self.dfs(grid, i + 1, j)
        self.dfs(grid, i, j - 1)
        self.dfs(grid, i, j + 1)
// 注意:go 代码由 chatGPT🤖 根据我的 java 代码翻译,旨在帮助不同背景的读者理解算法逻辑。
// 本代码还未经过力扣测试,仅供参考,如有疑惑,可以参照我写的 java 代码对比查看。

func numEnclaves(grid [][]int) int {
    m, n := len(grid), len(grid[0])
    // 淹掉靠边的陆地
    for i := 0; i < m; i++ {
        dfs(grid, i, 0)
        dfs(grid, i, n-1)
    }
    for j := 0; j < n; j++ {
        dfs(grid, 0, j)
        dfs(grid, m-1, j)
    }

    // 数一数剩下的陆地
    res := 0
    for i := 0; i < m; i++ {
        for j := 0; j < n; j++ {
            if grid[i][j] == 1 {
                res += 1
            }
        }
    }

    return res
}

// 和之前的实现类似
func dfs(grid [][]int, i int, j int) {
    // ...
}
// 注意:javascript 代码由 chatGPT🤖 根据我的 java 代码翻译,旨在帮助不同背景的读者理解算法逻辑。
// 本代码还未经过力扣测试,仅供参考,如有疑惑,可以参照我写的 java 代码对比查看。

var numEnclaves = function(grid) {
    const m = grid.length, n = grid[0].length;
    // 淹掉靠边的陆地
    for (let i = 0; i < m; i++) {
        dfs(grid, i, 0);
        dfs(grid, i, n - 1);
    }
    for (let j = 0; j < n; j++) {
        dfs(grid, 0, j);
        dfs(grid, m - 1, j);
    }

    // 数一数剩下的陆地
    let res = 0;
    for (let i = 0; i < m; i++) {
        for (let j = 0; j < n; j++) {
            if (grid[i][j] === 1) {
                res += 1;
            }
        }
    }

    return res;
};

// 和之前的实现类似
function dfs(grid, i, j) {
    // ...
}

🎃 代码可视化动画 🎃

篇幅所限,具体代码我就不写了,我们继续看其他的岛屿题目。

岛屿的最大面积

这是力扣第 695 题「 岛屿的最大面积」,0 表示海水,1 表示陆地,现在不让你计算岛屿的个数了,而是让你计算最大的那个岛屿的面积,函数签名如下:

int maxAreaOfIsland(int[][] grid)
// 注意:cpp 代码由 chatGPT🤖 根据我的 java 代码翻译,旨在帮助不同背景的读者理解算法逻辑。
// 本代码还未经过力扣测试,仅供参考,如有疑惑,可以参照我写的 java 代码对比查看。

int maxAreaOfIsland(vector<vector<int>>& grid)
# 注意:python 代码由 chatGPT🤖 根据我的 java 代码翻译,旨在帮助不同背景的读者理解算法逻辑。
# 本代码还未经过力扣测试,仅供参考,如有疑惑,可以参照我写的 java 代码对比查看。

def maxAreaOfIsland(grid: List[List[int]]) -> int
// 注意:go 代码由 chatGPT🤖 根据我的 java 代码翻译,旨在帮助不同背景的读者理解算法逻辑。
// 本代码还未经过力扣测试,仅供参考,如有疑惑,可以参照我写的 java 代码对比查看。

func maxAreaOfIsland(grid [][]int) int {
// 注意:javascript 代码由 chatGPT🤖 根据我的 java 代码翻译,旨在帮助不同背景的读者理解算法逻辑。
// 本代码还未经过力扣测试,仅供参考,如有疑惑,可以参照我写的 java 代码对比查看。

var maxAreaOfIsland = function(grid) {

}

比如题目给你输入如下一个二维矩阵:

其中面积最大的是橘红色的岛屿,算法返回它的面积 6。

这题的大体思路和之前完全一样,只不过 dfs 函数淹没岛屿的同时,还应该想办法记录这个岛屿的面积

我们可以给 dfs 函数设置返回值,记录每次淹没的陆地的个数,直接看解法吧:

class Solution {
    int maxAreaOfIsland(int[][] grid) {
        // 记录岛屿的最大面积
        int res = 0;
        int m = grid.length, n = grid[0].length;
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                if (grid[i][j] == 1) {
                    // 淹没岛屿,并更新最大岛屿面积
                    res = Math.max(res, dfs(grid, i, j));
                }
            }
        }
        return res;
    }

    // 淹没与 (i, j) 相邻的陆地,并返回淹没的陆地面积
    int dfs(int[][] grid, int i, int j) {
        int m = grid.length, n = grid[0].length;
        if (i < 0 || j < 0 || i >= m || j >= n) {
            // 超出索引边界
            return 0;
        }
        if (grid[i][j] == 0) {
            // 已经是海水了
            return 0;
        }
        // 将 (i, j) 变成海水
        grid[i][j] = 0;

        return dfs(grid, i + 1, j)
            + dfs(grid, i, j + 1)
            + dfs(grid, i - 1, j)
            + dfs(grid, i, j - 1) + 1;
    }
}
// 注意:cpp 代码由 chatGPT🤖 根据我的 java 代码翻译,旨在帮助不同背景的读者理解算法逻辑。
// 本代码还未经过力扣测试,仅供参考,如有疑惑,可以参照我写的 java 代码对比查看。

class Solution {
public:
    // 计算岛屿的最大面积
    int maxAreaOfIsland(vector<vector<int>>& grid) {
        // 记录岛屿的最大面积
        int res = 0;
        int m = grid.size(), n = grid[0].size();
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                if (grid[i][j] == 1) {
                    // 淹没岛屿,并更新最大岛屿面积
                    res = max(res, dfs(grid, i, j));
                }
            }
        }
        return res;
    }

    // 淹没与 (i, j) 相邻的陆地,并返回淹没的陆地面积
    int dfs(vector<vector<int>>& grid, int i, int j) {
        int m = grid.size(), n = grid[0].size();
        if (i < 0 || j < 0 || i >= m || j >= n) {
            // 超出索引边界
            return 0;
        }
        if (grid[i][j] == 0) {
            // 已经是海水了
            return 0;
        }
        // 将 (i, j) 变成海水
        grid[i][j] = 0;

        return dfs(grid, i + 1, j)
            + dfs(grid, i, j + 1)
            + dfs(grid, i - 1, j)
            + dfs(grid, i, j - 1) + 1;
    }
};
# 注意:python 代码由 chatGPT🤖 根据我的 java 代码翻译,旨在帮助不同背景的读者理解算法逻辑。
# 本代码还未经过力扣测试,仅供参考,如有疑惑,可以参照我写的 java 代码对比查看。

class Solution:
    def maxAreaOfIsland(self, grid: List[List[int]]) -> int:
        """
        :type grid: List[List[int]]
        :rtype: int
        """
        # 记录岛屿的最大面积
        res = 0
        m, n = len(grid), len(grid[0])
        
        def dfs(i: int, j: int) -> int:
            if i < 0 or j < 0 or i >= m or j >= n:
                # 超出索引边界
                return 0
            if grid[i][j] == 0:
                # 已经是海水了
                return 0
            # 将 (i, j) 变成海水
            grid[i][j] = 0
            # 淹没与 (i, j) 相邻的陆地,并返回淹没的陆地面积
            return dfs(i + 1, j) + dfs(i, j + 1) + dfs(i - 1, j) + dfs(i, j - 1) + 1
        
        for i in range(m):
            for j in range(n):
                if grid[i][j] == 1:
                    # 淹没岛屿,并更新最大岛屿面积
                    res = max(res, dfs(i, j))
                    
        return res
// 注意:go 代码由 chatGPT🤖 根据我的 java 代码翻译,旨在帮助不同背景的读者理解算法逻辑。
// 本代码还未经过力扣测试,仅供参考,如有疑惑,可以参照我写的 java 代码对比查看。

// 记录岛屿的最大面积
func maxAreaOfIsland(grid [][]int) int {
    // 最大岛屿面积
    res := 0
    // 网格的行数和列数
    m := len(grid)
    n := len(grid[0])
    // 遍历网格
    for i := 0; i < m; i++ {
        for j := 0; j < n; j++ {
            if grid[i][j] == 1 {
                // 淹没岛屿,并更新最大岛屿面积
                res = max(res, dfs(grid, i, j))
            }
        }
    }
    return res
}

// 淹没与 (i, j) 相邻的陆地,并返回淹没的陆地面积
func dfs(grid [][]int, i int, j int) int {
    // 网格的行数和列数
    m := len(grid)
    n := len(grid[0])
    // 超出索引边界
    if i < 0 || j < 0 || i >= m || j >= n {
        return 0
    }
    // 已经是海水了
    if grid[i][j] == 0 {
        return 0
    }
    // 将 (i, j) 变成海水
    grid[i][j] = 0

    return dfs(grid, i + 1, j)
        + dfs(grid, i, j + 1)
        + dfs(grid, i - 1, j)
        + dfs(grid, i, j - 1) + 1
}

// 返回两个数的最大值
func max(x, y int) int {
    if x > y {
        return x
    }
    return y
}
// 注意:javascript 代码由 chatGPT🤖 根据我的 java 代码翻译,旨在帮助不同背景的读者理解算法逻辑。
// 本代码还未经过力扣测试,仅供参考,如有疑惑,可以参照我写的 java 代码对比查看。

/**
 * 计算岛屿的最大面积
 * @param {number[][]} grid - 二维数组,每个元素为0或1
 * @return {number} - 岛屿的最大面积
 */
var maxAreaOfIsland = function(grid) {
  // 记录岛屿的最大面积
  let res = 0;
  const m = grid.length, n = grid[0].length;

  /**
   * 淹没与 (i, j) 相邻的陆地,并返回淹没的陆地面积
   * @param {number[][]} grid - 二维数组,每个元素为0或1
   * @param {number} i - 当前遍历元素的行坐标
   * @param {number} j - 当前遍历元素的列坐标
   * @return {number} - 当前岛屿的面积
   */
  const dfs = function(grid, i, j) {
    if (i < 0 || j < 0 || i >= m || j >= n) {
        // 超出索引边界
        return 0;
    }
    if (grid[i][j] === 0) {
        // 已经是海水了
        return 0;
    }
    // 将 (i, j) 变成海水
    grid[i][j] = 0;
    // 继续淹没与 (i,j) 相邻的岛屿
    return dfs(grid, i + 1, j)
        + dfs(grid, i, j + 1)
        + dfs(grid, i - 1, j)
        + dfs(grid, i, j - 1) + 1;
  };
  
  // 遍历二维数组,找到所有岛屿
  for (let i = 0; i < m; i++) {
    for (let j = 0; j < n; j++) {
      if (grid[i][j] === 1) {
        // 淹没岛屿,并更新最大岛屿面积
        res = Math.max(res, dfs(grid, i, j));
      }
    }
  }
  return res;
}

👾 代码可视化动画 👾

解法和之前相比差不多,我也不多说了,接下来的两道岛屿题目是比较有技巧性的,我们重点来看一下。

子岛屿数量

如果说前面的题目都是模板题,那么力扣第 1905 题「 统计子岛屿」可能得动动脑子了:

这道题的关键在于,如何快速判断子岛屿?肯定可以借助 Union Find 并查集算法 来判断,不过本文重点在 DFS 算法,就不展开并查集算法了。

什么情况下 grid2 中的一个岛屿 Bgrid1 中的一个岛屿 A 的子岛?

当岛屿 B 中所有陆地在岛屿 A 中也是陆地的时候,岛屿 B 是岛屿 A 的子岛。

反过来说,如果岛屿 B 中存在一片陆地,在岛屿 A 的对应位置是海水,那么岛屿 B 就不是岛屿 A 的子岛

那么,我们只要遍历 grid2 中的所有岛屿,把那些不可能是子岛的岛屿排除掉,剩下的就是子岛。

依据这个思路,可以直接写出下面的代码:

class Solution {
    int countSubIslands(int[][] grid1, int[][] grid2) {
        int m = grid1.length, n = grid1[0].length;
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                if (grid1[i][j] == 0 && grid2[i][j] == 1) {
                    // 这个岛屿肯定不是子岛,淹掉
                    dfs(grid2, i, j);
                }
            }
        }
        // 现在 grid2 中剩下的岛屿都是子岛,计算岛屿数量
        int res = 0;
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                if (grid2[i][j] == 1) {
                    res++;
                    dfs(grid2, i, j);
                }
            }
        }
        return res;
    }

    // 从 (i, j) 开始,将与之相邻的陆地都变成海水
    void dfs(int[][] grid, int i, int j) {
        int m = grid.length, n = grid[0].length;
        if (i < 0 || j < 0 || i >= m || j >= n) {
            return;
        }
        if (grid[i][j] == 0) {
            return;
        }

        grid[i][j] = 0;
        dfs(grid, i + 1, j);
        dfs(grid, i, j + 1);
        dfs(grid, i - 1, j);
        dfs(grid, i, j - 1);
    }
}
// 注意:cpp 代码由 chatGPT🤖 根据我的 java 代码翻译,旨在帮助不同背景的读者理解算法逻辑。
// 本代码还未经过力扣测试,仅供参考,如有疑惑,可以参照我写的 java 代码对比查看。

class Solution {
public:
    int countSubIslands(vector<vector<int>>& grid1, vector<vector<int>>& grid2) {
        int m = grid1.size(), n = grid1[0].size();
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                if (grid1[i][j] == 0 && grid2[i][j] == 1) {
                    // 这个岛屿肯定不是子岛,淹掉
                    dfs(grid2, i, j);
                }
            }
        }
        // 现在 grid2 中剩下的岛屿都是子岛,计算岛屿数量
        int res = 0;
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                if (grid2[i][j] == 1) {
                    res++;
                    dfs(grid2, i, j);
                }
            }
        }
        return res;
    }

    // 从 (i, j) 开始,将与之相邻的陆地都变成海水
    void dfs(vector<vector<int>>& grid, int i, int j) {
        int m = grid.size(), n = grid[0].size();
        if (i < 0 || j < 0 || i >= m || j >= n) {
            return;
        }
        if (grid[i][j] == 0) {
            return;
        }

        grid[i][j] = 0;
        dfs(grid, i + 1, j);
        dfs(grid, i, j + 1);
        dfs(grid, i - 1, j);
        dfs(grid, i, j - 1);
    }
};
# 注意:python 代码由 chatGPT🤖 根据我的 java 代码翻译,旨在帮助不同背景的读者理解算法逻辑。
# 本代码还未经过力扣测试,仅供参考,如有疑惑,可以参照我写的 java 代码对比查看。

class Solution:
    def countSubIslands(self, grid1: List[List[int]], grid2: List[List[int]]) -> int:
        m, n = len(grid1), len(grid1[0])
        # 这个岛屿肯定不是子岛,淹掉
        for i in range(m):
            for j in range(n):
                if grid1[i][j] == 0 and grid2[i][j] == 1:
                    self.dfs(grid2, i, j)

        # 现在 grid2 中剩下的岛屿都是子岛,计算岛屿数量
        res = 0
        for i in range(m):
            for j in range(n):
                if grid2[i][j] == 1:
                    res += 1
                    self.dfs(grid2, i, j)
        return res

    # 从 (i, j) 开始,将与之相邻的陆地都变成海水
    def dfs(self, grid: List[List[int]], i: int, j: int) -> None:
        m, n = len(grid), len(grid[0])
        if i < 0 or j < 0 or i >= m or j >= n:
            return
        if grid[i][j] == 0:
            return

        grid[i][j] = 0
        self.dfs(grid, i + 1, j)
        self.dfs(grid, i, j + 1)
        self.dfs(grid, i - 1, j)
        self.dfs(grid, i, j - 1)
// 注意:go 代码由 chatGPT🤖 根据我的 java 代码翻译,旨在帮助不同背景的读者理解算法逻辑。
// 本代码还未经过力扣测试,仅供参考,如有疑惑,可以参照我写的 java 代码对比查看。

func countSubIslands(grid1 [][]int, grid2 [][]int) int {
    m, n := len(grid1), len(grid1[0])
    for i := 0; i < m; i++ {
        for j := 0; j < n; j++ {
            if grid1[i][j] == 0 && grid2[i][j] == 1 {
                // 这个岛屿肯定不是子岛,淹掉
                dfs(grid2, i, j)
            }
        }
    }
    // 现在 grid2 中剩下的岛屿都是子岛,计算岛屿数量
    res := 0
    for i := 0; i < m; i++ {
        for j := 0; j < n; j++ {
            if grid2[i][j] == 1 {
                res++
                dfs(grid2, i, j)
            }
        }
    }
    return res
}

// 从 (i, j) 开始,将与之相邻的陆地都变成海水
func dfs(grid [][]int, i, j int) {
    m, n := len(grid), len(grid[0])
    if i < 0 || j < 0 || i >= m || j >= n {
        return
    }
    if grid[i][j] == 0 {
        return
    }

    grid[i][j] = 0
    dfs(grid, i+1, j)
    dfs(grid, i, j+1)
    dfs(grid, i-1, j)
    dfs(grid, i, j-1)
}
// 注意:javascript 代码由 chatGPT🤖 根据我的 java 代码翻译,旨在帮助不同背景的读者理解算法逻辑。
// 本代码还未经过力扣测试,仅供参考,如有疑惑,可以参照我写的 java 代码对比查看。

/**
 * 计算子岛屿数量
 *
 * @param {number[][]} grid1 岛屿地图1
 * @param {number[][]} grid2 岛屿地图2
 * @return {number} 子岛屿数量
 */
var countSubIslands = function(grid1, grid2) {
    var m = grid1.length, n = grid1[0].length;
    for (var i = 0; i < m; i++) {
        for (var j = 0; j < n; j++) {
            // 如果岛屿1和岛屿2在 (i, j) 处不是同一块陆地
            if (grid1[i][j] == 0 && grid2[i][j] == 1) {
                // 这个岛屿肯定不是子岛,淹掉
                dfs(grid2, i, j);
            }
        }
    }
    // 现在 grid2 中剩下的岛屿都是子岛,计算岛屿数量
    var res = 0;
    for (var i = 0; i < m; i++) {
        for (var j = 0; j < n; j++) {
            if (grid2[i][j] == 1) {
                res++;
                dfs(grid2, i, j);
            }
        }
    }
    return res;
};

/**
 * 深度优先遍历,从 (i, j) 开始,
 * 将与之相邻的陆地都变成海水
 *
 * @param {number[][]} grid 地图
 * @param {number} i 横坐标
 * @param {number} j 纵坐标
 */
var dfs = function(grid, i, j) {
    var m = grid.length, n = grid[0].length;
    if (i < 0 || j < 0 || i >= m || j >= n) {
        return;
    }
    if (grid[i][j] == 0) {
        return;
    }

    grid[i][j] = 0;
    dfs(grid, i + 1, j);
    dfs(grid, i, j + 1);
    dfs(grid, i - 1, j);
    dfs(grid, i, j - 1);
};

🌈 代码可视化动画 🌈

这道题的思路和计算「封闭岛屿」数量的思路有些类似,只不过后者排除那些靠边的岛屿,前者排除那些不可能是子岛的岛屿。

不同的岛屿数量

这是本文的最后一道岛屿题目,作为压轴题,当然是最有意思的。

力扣第 694 题「 不同的岛屿数量」,题目还是输入一个二维矩阵,0 表示海水,1 表示陆地,这次让你计算 不同的 (distinct) 岛屿数量,函数签名如下:

int numDistinctIslands(int[][] grid)
// 注意:cpp 代码由 chatGPT🤖 根据我的 java 代码翻译,旨在帮助不同背景的读者理解算法逻辑。
// 本代码还未经过力扣测试,仅供参考,如有疑惑,可以参照我写的 java 代码对比查看。

int numDistinctIslands(vector<vector<int>>& grid)
# 注意:python 代码由 chatGPT🤖 根据我的 java 代码翻译,旨在帮助不同背景的读者理解算法逻辑。
# 本代码还未经过力扣测试,仅供参考,如有疑惑,可以参照我写的 java 代码对比查看。

def numDistinctIslands(grid: List[List[int]]) -> int:
// 注意:go 代码由 chatGPT🤖 根据我的 java 代码翻译,旨在帮助不同背景的读者理解算法逻辑。
// 本代码还未经过力扣测试,仅供参考,如有疑惑,可以参照我写的 java 代码对比查看。

func numDistinctIslands(grid [][]int) int {
// 注意:javascript 代码由 chatGPT🤖 根据我的 java 代码翻译,旨在帮助不同背景的读者理解算法逻辑。
// 本代码还未经过力扣测试,仅供参考,如有疑惑,可以参照我写的 java 代码对比查看。

var numDistinctIslands = function(grid) {

比如题目输入下面这个二维矩阵:

其中有四个岛屿,但是左下角和右上角的岛屿形状相同,所以不同的岛屿共有三个,算法返回 3。

很显然我们得想办法把二维矩阵中的「岛屿」进行转化,变成比如字符串这样的类型,然后利用 HashSet 这样的数据结构去重,最终得到不同的岛屿的个数。

如果想把岛屿转化成字符串,说白了就是序列化,序列化说白了就是遍历嘛,前文 二叉树的序列化和反序列化 讲了二叉树和字符串互转,这里也是类似的。

首先,对于形状相同的岛屿,如果从同一起点出发,dfs 函数遍历的顺序肯定是一样的

因为遍历顺序是写死在你的递归函数里面的,不会动态改变:

void dfs(int[][] grid, int i, int j) {
    // 递归顺序:
    dfs(grid, i - 1, j); // 上
    dfs(grid, i + 1, j); // 下
    dfs(grid, i, j - 1); // 左
    dfs(grid, i, j + 1); // 右
}
// 注意:cpp 代码由 chatGPT🤖 根据我的 java 代码翻译,旨在帮助不同背景的读者理解算法逻辑。
// 本代码还未经过力扣测试,仅供参考,如有疑惑,可以参照我写的 java 代码对比查看。

void dfs(int grid[][], int i, int j) {
    // 递归顺序:
    dfs(grid, i - 1, j); // 上
    dfs(grid, i + 1, j); // 下
    dfs(grid, i, j - 1); // 左
    dfs(grid, i, j + 1); // 右
}
# 注意:python 代码由 chatGPT🤖 根据我的 java 代码翻译,旨在帮助不同背景的读者理解算法逻辑。
# 本代码还未经过力扣测试,仅供参考,如有疑惑,可以参照我写的 java 代码对比查看。

def dfs(grid: List[List[int]], i: int, j: int) -> None:
    # 递归顺序:
    dfs(grid, i - 1, j) # 上
    dfs(grid, i + 1, j) # 下
    dfs(grid, i, j - 1) # 左
    dfs(grid, i, j + 1) # 右
// 注意:go 代码由 chatGPT🤖 根据我的 java 代码翻译,旨在帮助不同背景的读者理解算法逻辑。
// 本代码还未经过力扣测试,仅供参考,如有疑惑,可以参照我写的 java 代码对比查看。

func dfs(grid [][]int, i int, j int) {
    // 递归顺序:
    dfs(grid, i - 1, j) // 上
    dfs(grid, i + 1, j) // 下
    dfs(grid, i, j - 1) // 左
    dfs(grid, i, j + 1) // 右
}
// 注意:javascript 代码由 chatGPT🤖 根据我的 java 代码翻译,旨在帮助不同背景的读者理解算法逻辑。
// 本代码还未经过力扣测试,仅供参考,如有疑惑,可以参照我写的 java 代码对比查看。

var dfs = function(grid, i, j) {
    dfs(grid, i - 1, j); // 上
    dfs(grid, i + 1, j); // 下
    dfs(grid, i, j - 1); // 左
    dfs(grid, i, j + 1); // 右
}

所以,遍历顺序从某种意义上说就可以用来描述岛屿的形状,比如下图这两个岛屿:

假设它们的遍历顺序是:

下,右,上,撤销上,撤销右,撤销下

如果我用分别用 1, 2, 3, 4 代表上下左右,用 -1, -2, -3, -4 代表上下左右的撤销,那么可以这样表示它们的遍历顺序:

2, 4, 1, -1, -4, -2

你看,这就相当于是岛屿序列化的结果,只要每次使用 dfs 遍历岛屿的时候生成这串数字进行比较,就可以计算到底有多少个不同的岛屿了

细心的读者问到,为什么记录「撤销」操作才能唯一表示遍历顺序呢?不记录撤销操作好像也可以?实际上不是的。

比方说「下,右,撤销右,撤销下」和「下,撤销下,右,撤销右」显然是两个不同的遍历顺序,但如果不记录撤销操作,那么它俩都是「下,右」,成了相同的遍历顺序,显然是不对的。

所以我们需要稍微改造 dfs 函数,添加一些函数参数以便记录遍历顺序:

void dfs(int[][] grid, int i, int j, StringBuilder sb, int dir) {
    int m = grid.length, n = grid[0].length;
    if (i < 0 || j < 0 || i >= m || j >= n 
        || grid[i][j] == 0) {
        return;
    }
    // 前序遍历位置:进入 (i, j)
    grid[i][j] = 0;
    sb.append(dir).append(',');
    
    dfs(grid, i - 1, j, sb, 1); // 上
    dfs(grid, i + 1, j, sb, 2); // 下
    dfs(grid, i, j - 1, sb, 3); // 左
    dfs(grid, i, j + 1, sb, 4); // 右
    
    // 后序遍历位置:离开 (i, j)
    sb.append(-dir).append(',');
}
// 注意:cpp 代码由 chatGPT🤖 根据我的 java 代码翻译,旨在帮助不同背景的读者理解算法逻辑。
// 本代码还未经过力扣测试,仅供参考,如有疑惑,可以参照我写的 java 代码对比查看。

void dfs(vector<vector<int>>& grid, int i, int j, string& sb, int dir) {
    int m = grid.size(), n = grid[0].size();
    if (i < 0 || j < 0 || i >= m || j >= n || grid[i][j] == 0) {
        return;
    }
    // 前序遍历位置:进入 (i, j)
    grid[i][j] = 0;
    sb += to_string(dir) + ',';
    
    dfs(grid, i - 1, j, sb, 1); // 上
    dfs(grid, i + 1, j, sb, 2); // 下
    dfs(grid, i, j - 1, sb, 3); // 左
    dfs(grid, i, j + 1, sb, 4); // 右
    
    // 后序遍历位置:离开 (i, j)
    sb += to_string(-dir) + ',';
}
# 注意:python 代码由 chatGPT🤖 根据我的 java 代码翻译,旨在帮助不同背景的读者理解算法逻辑。
# 本代码还未经过力扣测试,仅供参考,如有疑惑,可以参照我写的 java 代码对比查看。

def dfs(grid: List[List[int]], i: int, j: int, sb: StringBuilder, dir: int):
    # 获取数组行列数信息
    m, n = len(grid), len(grid[0])
    # 边界条件,如果越界或者为0,退出
    if i < 0 or j < 0 or i >= m or j >= n or grid[i][j] == 0:
        return
    # 标记当前节点已经被搜索过
    grid[i][j] = 0
    # 搜素方向,以1,2,3,4来标记上下左右
    sb.append(dir).append(',')
    
    # 搜索相邻节点
    dfs(grid, i - 1, j, sb, 1) # 上
    dfs(grid, i + 1, j, sb, 2) # 下
    dfs(grid, i, j - 1, sb, 3) # 左
    dfs(grid, i, j + 1, sb, 4) # 右
    
    # 搜素后,回溯到上一级
    sb.append(-dir).append(',')
// 注意:go 代码由 chatGPT🤖 根据我的 java 代码翻译,旨在帮助不同背景的读者理解算法逻辑。
// 本代码还未经过力扣测试,仅供参考,如有疑惑,可以参照我写的 java 代码对比查看。

func dfs(grid [][]int, i int, j int, sb *strings.Builder, dir int) {
    m, n := len(grid), len(grid[0])
    if i < 0 || j < 0 || i >= m || j >= n || grid[i][j] == 0 {
        return
    }
    // 前序遍历位置:进入 (i, j)
    grid[i][j] = 0
    sb.WriteString(strconv.Itoa(dir) + ",")
    
    dfs(grid, i - 1, j, sb, 1) // 上
    dfs(grid, i + 1, j, sb, 2) // 下
    dfs(grid, i, j - 1, sb, 3) // 左
    dfs(grid, i, j + 1, sb, 4) // 右
    
    // 后序遍历位置:离开 (i, j)
    sb.WriteString(strconv.Itoa(-dir) + ",")
}
// 注意:javascript 代码由 chatGPT🤖 根据我的 java 代码翻译,旨在帮助不同背景的读者理解算法逻辑。
// 本代码还未经过力扣测试,仅供参考,如有疑惑,可以参照我写的 java 代码对比查看。

var dfs = function(grid, i, j, sb, dir) {
    var m = grid.length, n = grid[0].length;
    if (i < 0 || j < 0 || i >= m || j >= n || grid[i][j] == 0) {
        return;
    }
    // 前序遍历位置:进入 (i, j)
    grid[i][j] = 0;
    sb.append(dir).append(",");
    
    dfs(grid, i - 1, j, sb, 1); // 上
    dfs(grid, i + 1, j, sb, 2); // 下
    dfs(grid, i, j - 1, sb, 3); // 左
    dfs(grid, i, j + 1, sb, 4); // 右
    
    // 后序遍历位置:离开 (i, j)
    sb.append(-dir).append(",");
}

仔细看这个代码,在递归前做选择,在递归后撤销选择,它像不像 回溯算法框架?实际上它就是回溯算法,因为它关注的是「树枝」(岛屿的遍历顺序),而不是「节点」(岛屿的每个格子)。

dir 记录方向,dfs 函数递归结束后,sb 记录着整个遍历顺序。有了这个 dfs 函数就好办了,我们可以直接写出最后的解法代码:

class Solution {
    public int numDistinctIslands(int[][] grid) {
        int m = grid.length, n = grid[0].length;
        // 记录所有岛屿的序列化结果
        HashSet<String> islands = new HashSet<>();
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                if (grid[i][j] == 1) {
                    // 淹掉这个岛屿,同时存储岛屿的序列化结果
                    StringBuilder sb = new StringBuilder();
                    // 初始的方向可以随便写,不影响正确性
                    dfs(grid, i, j, sb, 666);
                    islands.add(sb.toString());/**<extend up -200><img src="/algo/images/岛屿/6.png"> */
                }
            }
        }
        // 不相同的岛屿数量
        return islands.size();
    }
}
// 注意:cpp 代码由 chatGPT🤖 根据我的 java 代码翻译,旨在帮助不同背景的读者理解算法逻辑。
// 本代码还未经过力扣测试,仅供参考,如有疑惑,可以参照我写的 java 代码对比查看。

int numDistinctIslands(vector<vector<int>>& grid) {
    int m = grid.size(), n = grid[0].size();
    // 记录所有岛屿的序列化结果
    unordered_set<string> islands;
    for (int i = 0; i < m; i++) {
        for (int j = 0; j < n; j++) {
            if (grid[i][j] == 1) {
                // 淹掉这个岛屿,同时存储岛屿的序列化结果
                string curIsland = "";
                // 初始的方向可以随便写,不影响正确性
                dfs(grid, i, j, curIsland, 666);
                islands.insert(curIsland);/**<extend up -200><img src="/algo/images/岛屿/6.png"> */
            }
        }
    }
    // 不相同的岛屿数量
    return islands.size();
}
# 注意:python 代码由 chatGPT🤖 根据我的 java 代码翻译,旨在帮助不同背景的读者理解算法逻辑。
# 本代码还未经过力扣测试,仅供参考,如有疑惑,可以参照我写的 java 代码对比查看。

def numDistinctIslands(grid: List[List[int]]) -> int:
    m, n = len(grid), len(grid[0])
    # 记录所有岛屿的序列化结果
    islands = set()
    for i in range(m):
        for j in range(n):
            if grid[i][j] == 1:
                # 淹掉这个岛屿,同时存储岛屿的序列化结果
                sb = []
                dfs(grid, i, j, sb, 666)
                islands.add(''.join(sb)) # <extend up -200><img src="/algo/images/岛屿/6.png"> #
    # 不相同的岛屿数量
    return len(islands)
// 注意:go 代码由 chatGPT🤖 根据我的 java 代码翻译,旨在帮助不同背景的读者理解算法逻辑。
// 本代码还未经过力扣测试,仅供参考,如有疑惑,可以参照我写的 java 代码对比查看。

func numDistinctIslands(grid [][]int) int {
    m, n := len(grid), len(grid[0])
    // 记录所有岛屿的序列化结果
    islands := make(map[string]bool)
    for i := 0; i < m; i++ {
        for j := 0; j < n; j++ {
            if grid[i][j] == 1 {
                // 淹掉这个岛屿,同时存储岛屿的序列化结果
                var sb strings.Builder
                // 初始的方向可以随便写,不影响正确性
                dfs(grid, i, j, &sb, 666)
                islands[sb.String()] = true
                /**
                extend up -200
                <figure><img src="/images/%e5%b2%9b%e5%b1%bf/6.png"
     class="shadow myimage"/>
</figure>
                */
            }
        }
    }
    // 不相同的岛屿数量
    return len(islands)
}
// 注意:javascript 代码由 chatGPT🤖 根据我的 java 代码翻译,旨在帮助不同背景的读者理解算法逻辑。
// 本代码还未经过力扣测试,仅供参考,如有疑惑,可以参照我写的 java 代码对比查看。

var numDistinctIslands = function(grid) {
    var m = grid.length, n = grid[0].length;
    // 记录所有岛屿的序列化结果
    var islands = new Set();
    for (var i = 0; i < m; i++) {
        for (var j = 0; j < n; j++) {
            if (grid[i][j] == 1) {
                // 淹掉这个岛屿,同时存储岛屿的序列化结果
                var sb = new StringBuilder();
                // 初始的方向可以随便写,不影响正确性
                dfs(grid, i, j, sb, 666);
                islands.add(sb.toString());/**<extend up -200><img src="/algo/images/岛屿/6.png"> */
            }
        }
    }
    // 不相同的岛屿数量
    return islands.size;
};

🌟 代码可视化动画 🌟

这样,这道题就解决了,至于为什么初始调用 dfs 函数时的 dir 参数可以随意写,因为这个 dfs 函数实际上是回溯算法,它关注的是「树枝」而不是「节点」,前文 图算法基础 有写具体的区别,这里就不赘述了。

以上就是全部岛屿系列题目的解题思路,也许前面的题目大部分人会做,但是最后两题还是比较巧妙的,希望本文对你有帮助。


引用本文的题目

安装 我的 Chrome 刷题插件 点开下列题目可直接查看解题思路:

LeetCode 力扣
79. Word Search 79. 单词搜索
- 剑指 Offer 13. 机器人的运动范围
- 剑指 Offer II 105. 岛屿的最大面积

引用本文的文章

_____________

《labuladong 的算法小抄》已经出版,关注公众号查看详情;后台回复关键词「进群」可加入算法群;回复「全家桶」可下载配套 PDF 和刷题全家桶

共同维护高质量学习环境,评论礼仪见这里,违者直接拉黑不解释