Skip to content

洪水填充


基本介绍

洪水填充是一种很简单的技巧,设置路径信息进行剪枝和统计,类似感染的过程

路径信息不撤销,来保证每一片的感染过程可以得到区分

看似是暴力递归过程,其实时间复杂度非常好,遍历次数和样本数量的规模一致

岛屿数量

题目链接

https://leetcode.cn/problems/number-of-islands/

思路分析

遇到一个 1,岛屿数量加一,把与其相连的所有 1 都感染了(dfs 的过程),被感染的点改为 0

dfs 中如果碰到了之前已经打过标记的点,直接返回,并不会展开,这里做了剪枝

时间复杂度:O(n * m)

代码实现

java
class Solution {
    public int numIslands(char[][] grid) {
        int n = grid.length;
        int m = grid[0].length;
        int islands = 0;
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < m; j++) {
                // 遇到 1,将与其相连的点都感染了,改为 0,岛屿数量加一
                if (grid[i][j] == '1') {
                    islands++;
                    dfs(grid, n, m, i, j);
                }
            }
        }
        return islands;
    }

    public void dfs(char[][] grid, int n, int m, int i, int j) {
        // 越界,之前访问过,直接返回,不处理
        if (i < 0 || i == n || j < 0 || j == m || grid[i][j] != '1') {
            return;
        }
        // grid[i][j] = '1'
        grid[i][j] = '0';
        // 上
        dfs(grid, n, m, i - 1, j);
        // 下
        dfs(grid, n, m, i + 1, j);
        // 左
        dfs(grid, n, m, i, j - 1);
        // 右
        dfs(grid, n, m, i, j + 1);
    }
}

被围绕的区域

题目链接

https://leetcode.cn/problems/surrounded-regions/

思路分析

要求岛中被包围的部分,可以先从四周往中间遍历,把那些不被包围的岛屿(与边界相连的岛屿)打上标记,剩余部分就是被包围的,最后再将之前打的标记去掉即可

代码实现

java
class Solution {
    public void solve(char[][] board) {
        int n = board.length;
        int m = board[0].length;

        // 上下两行往中间遍历
        for (int j = 0; j < m; j++) {
            // 上边行
            if (board[0][j] == 'O') {
                dfs(board, n, m, 0, j);
            }
            // 下边行
            if (board[n - 1][j] == 'O') {
                dfs(board, n, m, n - 1, j);
            }
        }

        // 左右两列往中间遍历,注意有一部分点在之前已经处理了
        for (int i = 1; i < n - 1; i++) {
            if (board[i][0] == 'O') {
                dfs(board, n, m, i, 0);
            }
            if (board[i][m - 1] == 'O') {
                dfs(board, n, m, i, m - 1);
            }
        }

        // 撤销之前打过的标记,处理被包围的岛屿
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < m; j++) {
                // 被包围的岛屿,根据题意设置为 X
                if (board[i][j] == 'O') {
                    board[i][j] = 'X';
                }
                // 与边界相连的岛屿,撤销之前打的标记
                if (board[i][j] == 'F') {
                    board[i][j] = 'O';
                }
            }
        }
    }

    public void dfs(char[][] board, int n, int m, int i, int j) {
        // 越界,之前被打过标记了,不处理
        if (i < 0 || i >= n || j < 0 || j >= m || board[i][j] != 'O') {
            return;
        }
        // 处理与边界相连岛屿,打上标记
        board[i][j] = 'F';
        // 上
        dfs(board, n, m, i + 1, j);
        // 下
        dfs(board, n, m, i - 1, j);
        // 左
        dfs(board, n, m, i, j - 1);
        // 右
        dfs(board, n, m, i, j + 1);
    }
}

最大人工岛

题目链接

https://leetcode.cn/problems/making-a-large-island/

思路分析

先给每一块岛屿打上标记,然后讨论每个 0 位置变成 1 后于周围的岛屿相接会不会使得岛屿最大

⚠️ 注意:需要考虑特殊情况,如果所有点都是 1,则最大岛就是整个二维矩阵

代码实现

java
class Solution {
    public int largestIsland(int[][] grid) {
        int n = grid.length;
        int m = grid[0].length;

        int id = 2;

        // 首先给每一片岛屿打标记
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < m; j++) {
                if (grid[i][j] == 1) {
                    dfs(grid, n, m, i, j, id++);
                }
            }
        }

        // 统计每一片岛屿的数量
        int[] sizes = new int[id];

        // 考虑特殊情况,如果岛屿全是 1,在打标记中就会被处理为 2
        // 经过计算,ans 即为矩阵的面积,在后续步骤中不会进入 dfs
        // 直接返回 ans,否则 ans 维持的就是最大岛的大小,在后续
        // 进入 dfs 后,讨论每个点改变为 1 后是否会带来更大的岛屿
        // 不断更新最大岛屿的数值,直到 dfs 结束后,返回 ans
        int ans = 0;

        for (int i = 0; i < n; i++) {
            for (int j = 0; j < m; j++) {
                if (grid[i][j] > 1) {
                    ans = Math.max(ans, ++sizes[grid[i][j]]);
                }
            }
        }

        // 每个岛屿不能重复计算,设置 visited 数组
        boolean[] visited = new boolean[id];

        // 讨论每个 0 位置,变成 1 之后是否带来更大的岛屿
        int up, down, left, right, merge;
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < m; j++) {
                if (grid[i][j] == 0) {
                    // 讨论上下左右四个方向是否越界,取出对应的值
                    up = i > 0 ? grid[i - 1][j] : 0;
                    down = i + 1 < n ? grid[i + 1][j] : 0;
                    left = j > 0 ? grid[i][j - 1] : 0;
                    right = j + 1 < m ? grid[i][j + 1] : 0;
                    // 先标记 up 方向,再讨论其他方向
                    visited[up] = true;
                    // 当前点 + 下方岛屿的大小
                    merge = 1 + sizes[up];
                    if (!visited[down]) {
                        merge += sizes[down];
                        visited[down] = true;
                    }
                    if (!visited[left]) {
                        merge += sizes[left];
                        visited[left] = true;
                    }
                    if (!visited[right]) {
                        merge += sizes[right];
                        visited[right] = true;
                    }

                    // 更新答案
                    ans = Math.max(ans, merge);

                    // 讨论下一个 0,不同 0 之间互不影响
                    visited[up] = false;
                    visited[down] = false;
                    visited[left] = false;
                    visited[right] = false;
                }
            }
        }
        return ans;
    }

    public void dfs(int[][] grid, int n, int m, int i, int j, int id) {
        // 越界,之前标记过,直接返回
        if (i < 0 || i >= n || j < 0 || j >= m || grid[i][j] != 1) {
            return;
        }
        // 给每一块岛屿打标记,用于区分不同的岛屿
        grid[i][j] = id;
        // 上
        dfs(grid, n, m, i + 1, j, id);
        // 下
        dfs(grid, n, m, i - 1, j, id);
        // 左
        dfs(grid, n, m, i, j + 1, id);
        // 右
        dfs(grid, n, m, i, j - 1, id);
    }
}

⭐ 打砖块

题目链接

https://leetcode.cn/problems/bricks-falling-when-hit/

思路分析

将所有炮弹位置 -1

将天花板的所有元素感染

时光倒流技巧,从后往前遍历炮弹

(1)将炮弹位置 +1,如果发现是 0,说明炮弹打空了,无需处理

(2)如果发现炮弹位置的四周存在天花板或者炮弹位置就在天花板,把与其相连的元素都感染,计算新增感染元素的个数

(3)掉落元素的个数 = 新增感染元素个数 - 1,减一是因为炮弹位置的元素被击碎了,才导致元素的掉落,被击碎的元素并不会掉落

代码实现

java
class Solution {
    public int[] hitBricks(int[][] grid, int[][] hits) {
        int n = grid.length;
        int m = grid[0].length;
        int[] ans = new int[hits.length];

        // 特判,如果只有天花板元素,不会有元素掉落
        if (n == 1) {
            return ans;
        }

        // 第一步;将炮弹位置减一
        for (int[] hit : hits) {
            grid[hit[0]][hit[1]]--;
        }

        // 第二步:将天花板元素都感染
        for (int i = 0; i < m; i++) {
            dfs(grid, n, m, 0, i);
        }

        // 第三步:时光倒流
        // (1)从后往前遍历炮弹,将炮弹位置 +1
        // (2)根据 worth 函数判断该位置是否值得 dfs
        for (int i = hits.length - 1; i >= 0; i--) {
            int row = hits[i][0];
            int col = hits[i][1];
            grid[row][col]++;
            if (worth(grid, n, m, row, col)) {
                // 注意减一,炮弹位置被击碎,掉落的元素是与其相连的元素
                ans[i] = dfs(grid, n, m, row, col) - 1;
            }
        }
        return ans;
    }

    // 返回被感染元素的个数
    public int dfs(int[][] grid, int n, int m, int i, int j) {
        // 越界,之前标记过,不处理
        if (i < 0 || i >= n || j < 0 || j >= m || grid[i][j] != 1) {
            return 0;
        }
        // 打标记
        grid[i][j] = 2;
        // 从 [i,j] 位置开始,被感染的元素个数 = [i,j] 这个点 + 四周与其相连的元素
        return 1 + dfs(grid, n, m, i - 1, j)
                + dfs(grid, n, m, i + 1, j)
                + dfs(grid, n, m, i, j - 1)
                + dfs(grid, n, m, i, j + 1);
    }

    // 判断是否值得遍历,前提是该位置的是 1
    // (1)周围存在天花板元素
    // (2)该元素在天花板位置
    public boolean worth(int[][] grid, int n, int m, int i, int j) {
        return grid[i][j] == 1 &&
                (i == 0 || i > 0 && grid[i - 1][j] == 2
                        || i < n - 1 && grid[i + 1][j] == 2
                        || j > 0 && grid[i][j - 1] == 2
                        || j < m - 1 && grid[i][j + 1] == 2);
    }
}