• 【LeetCode】200.岛屿数量(dfs+bfs+并查集,超详细图文解释)


    题目

    image-20200702010338364

    分析

    img

    说明:以下介绍的算法,除了并查集以外,DFS 和 BFS 都属于很基础的算法内容,也非常好理解,写法也相对固定,读者需要多写,发现并记录自己的问题,我也是在写了几遍甚至是在写本题解的过程中,才发现出自己的问题。

    这道题是可以使用一个经典的算法来解决的,那就是 Flood fill,以下的定义来自 维基百科:Flood fill 词条

    Flood fill 算法是从一个区域中提取若干个连通的点与其他相邻区域区分开(或分别染成不同颜色)的经典 算法。因为其思路类似洪水从一个区域扩散到所有能到达的区域而得名。在 GNU Go扫雷 中,Flood Fill算法被用来计算需要被清除的区域。

    “Flood” 我查了一下,作为动词是 “淹没;充满” 的意思,作为名词是 “洪水” 的意思。下面我们简单解释一下这个算法:

    • 从一个区域中提取若干个连通的点与其他相邻区域区分开

    从一个点扩散开,找到与其连通的点,这不是什么高深的算法,其实就是从一个点开始,进行一次 “深度优先遍历” 或者 “广度优先遍历”,通过 “深度优先遍历” 或者 “广度优先遍历” 发现一片连着的区域,对于这道题来说,就是从一个是 “陆地” 的格子开始进行一次 “深度优先遍历” 或者 “广度优先遍历”,把与之相连的所有的格子都标记上,视为发现了一个 “岛屿”。

    说明:这里做 “标记” 的意思是,通过 “深度优先遍历” 或者 “广度优先遍历” 操作,我发现了一个新的格子,与起始点的那个格子是连通的,我们视为 “标记” 过,也可以说 “被访问过”。

    那么每一次进行 “深度优先遍历” 或者 “广度优先遍历” 的条件就是:

    1、这个格子是陆地 1,如果是水域 0 就无从谈论 “岛屿”;

    2、这个格子不能是之前发现 “岛屿” 的过程中执行了 “深度优先遍历” 或者 “广度优先遍历” 操作,而被标记的格子(这句话说得太拗口了,大家意会即可,意会不了不是您的问题,是我表达的问题,直接看代码会清楚很多)。

    方法一:深度优先遍历

    图解

    img

    img

    img

    img

    img

    img

    img

    img

    img

    img

    img

    img

    img

    img

    img

    img

    img

    img

    代码实现1:

    /**
     * 方法一:深度优先遍历
     */
    public class Solution {
    
        //           x-1,y
        //  x,y-1    x,y      x,y+1
        //           x+1,y
        // 方向数组,它表示了相对于当前位置的 4 个方向的横、纵坐标的偏移量,这是一个常见的技巧。上左下右
        private static final int[][] directions = {{-1, 0}, {0, -1}, {1, 0}, {0, 1}};
        // 标记数组,标记了 grid 的坐标对应的格子是否被访问过
        private boolean[][] marked;
        // grid 的行数
        private int rows;
        // grid 的列数
        private int cols;
        private char[][] grid;
    
        public int numIslands(char[][] grid) {
            rows = grid.length;
            if (rows == 0) {
                return 0;
            }
            cols = grid[0].length;
            this.grid = grid;
            marked = new boolean[rows][cols];
            int count = 0;
            for (int i = 0; i < rows; i++) {
                for (int j = 0; j < cols; j++) {
                    // 如果是岛屿中的一个点,并且没有被访问过
                    // 就进行深度优先遍历
                    if (!marked[i][j] && grid[i][j] == '1') {
                        count++;
                        dfs(i, j);
                    }
                }
            }
            return count;
        }
    
        // 从坐标为 (i,j) 的点开始进行深度优先遍历
        private void dfs(int i, int j) {
            marked[i][j] = true;
            // 得到 4 个方向的坐标
            for (int k = 0; k < 4; k++) {
                int newX = i + directions[k][0];
                int newY = j + directions[k][1];
                // 如果不越界、没有被访问过、并且还要是陆地
                if (inArea(newX, newY) && grid[newX][newY] == '1' && !marked[newX][newY]) {
                    dfs(newX, newY);
                }
            }
        }
    
        // 封装成 inArea 方法语义更清晰
        private boolean inArea(int x, int y) {
            // 等于号不要忘了
            return x >= 0 && x < rows && y >= 0 && y < cols;
        }
    
        public static void main(String[] args) {
            Solution solution = new Solution();
            char[][] grid1 = {
                    {'1', '1', '1', '1', '0'},
                    {'1', '1', '0', '1', '0'},
                    {'1', '1', '0', '0', '0'},
                    {'0', '0', '0', '0', '0'}};
            int numIslands1 = solution.numIslands(grid1);
            System.out.println(numIslands1);
    
            char[][] grid2 = {
                    {'1', '1', '0', '0', '0'},
                    {'1', '1', '0', '0', '0'},
                    {'0', '0', '1', '0', '0'},
                    {'0', '0', '0', '1', '1'}};
            int numIslands2 = solution.numIslands(grid2);
            System.out.println(numIslands2);
        }
    }
    

    代码实现2:

    class Solution {
        void dfs(char[][] grid, int r, int c) {
            int nr = grid.length;
            int nc = grid[0].length;
    
            if (r < 0 || c < 0 || r >= nr || c >= nc || grid[r][c] == '0') {
                return;
            }
    
            grid[r][c] = '0';
            dfs(grid, r - 1, c);
            dfs(grid, r + 1, c);
            dfs(grid, r, c - 1);
            dfs(grid, r, c + 1);
        }
    
        public int numIslands(char[][] grid) {
            if (grid == null || grid.length == 0) {
                return 0;
            }
    
            int nr = grid.length;
            int nc = grid[0].length;
            int num_islands = 0;
            for (int r = 0; r < nr; ++r) {
                for (int c = 0; c < nc; ++c) {
                    if (grid[r][c] == '1') {
                        ++num_islands;
                        dfs(grid, r, c);
                    }
                }
            }
    
            return num_islands;
        }
    }
    

    复杂度分析

    • 时间复杂度:O(MN),其中 M 和 N分别为行数和列数。
    • 空间复杂度:O(MN),在最坏情况下,整个网格均为陆地,深度优先搜索的深度达到 M N。

    方法二:广度优先遍历

    除了 “深度优先遍历”,你还可以使用 “广度优先遍历”,此时你就不用回溯了。“广度优先遍历” 需要一个 “辅助队列”。

    图解

    img

    img

    img

    img

    img

    img

    img

    img

    img

    img

    img

    img代码实现3:

    import java.util.LinkedList;
    
    /**
     * 方法二:广度优先遍历
     */
    public class Solution2 {
    
    
        private int rows;
        private int cols;
    
        public int numIslands(char[][] grid) {
            //           x-1,y
            //  x,y-1    x,y      x,y+1
            //           x+1,y
            int[][] directions = {{-1, 0}, {0, -1}, {1, 0}, {0, 1}};
    
            rows = grid.length;
            if (rows == 0) {
                return 0;
            }
            cols = grid[0].length;
            boolean[][] marked = new boolean[rows][cols];
            int count = 0;
            for (int i = 0; i < rows; i++) {
                for (int j = 0; j < cols; j++) {
                    // 如果是岛屿中的一个点,并且没有被访问过
                    // 从坐标为 (i,j) 的点开始进行广度优先遍历
                    if (!marked[i][j] && grid[i][j] == '1') {
                        count++;
                        LinkedList<Integer> queue = new LinkedList<>();
                        // 小技巧:把坐标转换为一个数字
                        // 否则,得用一个数组存,在 Python 中,可以使用 tuple 存
                        queue.addLast(i * cols + j);
                        // 注意:这里要标记上已经访问过
                        marked[i][j] = true;
                        while (!queue.isEmpty()) {
                            int cur = queue.removeFirst();
                            int curX = cur / cols;
                            int curY = cur % cols;
                            // 得到 4 个方向的坐标
                            for (int k = 0; k < 4; k++) {
                                int newX = curX + directions[k][0];
                                int newY = curY + directions[k][1];
                                // 如果不越界、没有被访问过、并且还要是陆地,我就继续放入队列,放入队列的同时,要记得标记已经访问过
                                if (inArea(newX, newY) && grid[newX][newY] == '1' && !marked[newX][newY]) {
                                    queue.addLast(newX * cols + newY);
                                    // 【特别注意】在放入队列以后,要马上标记成已经访问过,语义也是十分清楚的:反正只要进入了队列,你迟早都会遍历到它
                                    // 而不是在出队列的时候再标记
                                    // 【特别注意】如果是出队列的时候再标记,会造成很多重复的结点进入队列,造成重复的操作,这句话如果你没有写对地方,代码会严重超时的
                                    marked[newX][newY] = true;
                                }
                            }
                        }
                    }
                }
    
            }
            return count;
        }
    
        private boolean inArea(int x, int y) {
            // 等于号这些细节不要忘了
            return x >= 0 && x < rows && y >= 0 && y < cols;
        }
    
        public static void main(String[] args) {
            Solution2 solution2 = new Solution2();
            char[][] grid1 = {
                    {'1', '1', '1', '1', '0'},
                    {'1', '1', '0', '1', '0'},
                    {'1', '1', '0', '0', '0'},
                    {'0', '0', '0', '0', '0'}};
            int numIslands1 = solution2.numIslands(grid1);
            System.out.println(numIslands1);
    
            char[][] grid2 = {
                    {'1', '1', '0', '0', '0'},
                    {'1', '1', '0', '0', '0'},
                    {'0', '0', '1', '0', '0'},
                    {'0', '0', '0', '1', '1'}};
            int numIslands2 = solution2.numIslands(grid2);
            System.out.println(numIslands2);
        }
    }
    

    代码实现4:

    class Solution {
    public:
        int numIslands(vector<vector<char>>& grid) {
            int nr = grid.size();
            if (!nr) return 0;
            int nc = grid[0].size();
    
            int num_islands = 0;
            for (int r = 0; r < nr; ++r) {
                for (int c = 0; c < nc; ++c) {
                    if (grid[r][c] == '1') {
                        ++num_islands;
                        grid[r][c] = '0';
                        queue<pair<int, int>> neighbors;
                        neighbors.push({r, c});
                        while (!neighbors.empty()) {
                            auto rc = neighbors.front();
                            neighbors.pop();
                            int row = rc.first, col = rc.second;
                            if (row - 1 >= 0 && grid[row-1][col] == '1') {
                                neighbors.push({row-1, col});
                                grid[row-1][col] = '0';
                            }
                            if (row + 1 < nr && grid[row+1][col] == '1') {
                                neighbors.push({row+1, col});
                                grid[row+1][col] = '0';
                            }
                            if (col - 1 >= 0 && grid[row][col-1] == '1') {
                                neighbors.push({row, col-1});
                                grid[row][col-1] = '0';
                            }
                            if (col + 1 < nc && grid[row][col+1] == '1') {
                                neighbors.push({row, col+1});
                                grid[row][col+1] = '0';
                            }
                        }
                    }
                }
            }
    
            return num_islands;
        }
    };
    

    复杂度分析

    • 时间复杂度:O(MN),其中 MM 和 N分别为行数和列数。
    • 空间复杂度:O(min(M,N))O(min(M, N)),在最坏情况下,整个网格均为陆地,队列的大小可以达到$ min(M, N)$。

    方法三:使用并查集

    使用并查集解决本问题的思想很简单:

    1、如果当前是“陆地”,尝试与周围合并一下;

    2、如果当前是“水域”,就把所有的“水域”合并在一起,为此,我设置了一个虚拟的结点,表示“所有的水域都和这个虚拟结点是连接的”。

    注意

    1、针对上面的第 1 点:如果当前是 “陆地”,尝试与周围合并一下”,此时 “周围” 并不需要像 “深度优先遍历” 和 “广度优先遍历” 一样,方向是四周。事实上,只要 “向右”、“向下” 两个方向就可以了,原因很简单,你可以在脑子里想象一个 “4 个方向” 和 “2 个方向” 的算法执行流程(或者看我下面展示的动画),就知道 “4 个方向” 没有必要;

    2、针对上面的第 2 点:由于我设置了“虚拟结点”,最后返回“岛屿个数”的时候,应该是连通分量个数 - 1,不要忘记将 “虚拟结点” 代表的 “水域” 分量去掉,剩下的连通分量个数就是 “岛屿个数”。

    图解

    img

    img

    img

    img

    img

    img

    img

    img

    img

    img

    img

    img

    img

    img

    img

    img

    img

    img

    img

    img

    img

    代码实现5:

    from typing import List
    
    
    class Solution:
        def numIslands(self, grid: List[List[str]]) -> int:
    
            class UnionFind:
    
                def __init__(self, n):
                    self.count = n
                    self.parent = [i for i in range(n)]
                    self.rank = [1 for _ in range(n)]
    
                def get_count(self):
                    return self.count
    
                def find(self, p):
                    while p != self.parent[p]:
                        self.parent[p] = self.parent[self.parent[p]]
                        p = self.parent[p]
                    return p
    
                def is_connected(self, p, q):
                    return self.find(p) == self.find(q)
    
                def union(self, p, q):
                    p_root = self.find(p)
                    q_root = self.find(q)
                    if p_root == q_root:
                        return
    
                    if self.rank[p_root] > self.rank[q_root]:
                        self.parent[q_root] = p_root
                    elif self.rank[p_root] < self.rank[q_root]:
                        self.parent[p_root] = q_root
                    else:
                        self.parent[q_root] = p_root
                        self.rank[p_root] += 1
    
                    self.count -= 1
    
            row = len(grid)
            # 特判
            if row == 0:
                return 0
            col = len(grid[0])
    
            def get_index(x, y):
                return x * col + y
    
            # 注意:我们不用像 DFS 和 BFS 一样,4 个方向都要尝试,只要看一看右边和下面就可以了
            directions = [(1, 0), (0, 1)]
            # 多开一个空间,把水域 "0" 都归到这个虚拟的老大上
            dummy_node = row * col
    
            # 多开的一个空间就是那个虚拟的空间
            uf = UnionFind(dummy_node + 1)
            for i in range(row):
                for j in range(col):
                    # 如果是水域,都连到那个虚拟的空间去
                    if grid[i][j] == '0':
                        uf.union(get_index(i, j), dummy_node)
                    if grid[i][j] == '1':
                        # 向下向右如果都是陆地,即 "1",就要合并一下
                        for direction in directions:
                            new_x = i + direction[0]
                            new_y = j + direction[1]
                            if new_x < row and new_y < col and grid[new_x][new_y] == '1':
                                uf.union(get_index(i, j), get_index(new_x, new_y))
            # 不要忘记把那个虚拟结点减掉
            return uf.get_count() - 1
    
    
    if __name__ == '__main__':
        grid = [['1', '1', '1', '1', '0'],
                ['1', '1', '0', '1', '0'],
                ['1', '1', '0', '0', '0'],
                ['0', '0', '0', '0', '0']]
        solution = Solution()
        result = solution.numIslands(grid)
        print(result)
    

    这里有一点需要注意:解决这个问题是可以不用建立 marked数组,但有以下的知识点需要大家知道。

    • 对于算法的输入而言,很多时候是介意修改输入的,除非题目就是要我们修改输入数据;
    • 再者 marked数组没有必要节约这个空间,现在空间越来越不值钱了,而且程序执行完成以后,空间可以回收。我们写算法的时候,应该力求时间复杂度最优,并且代码可读性强。
  • 相关阅读:
    26_为什么我的删除刷新没有办法删除动态添加的数据呢?
    075_not in (null):这代表是什么意思呢?
    074_form表单中的value值
    025_回调函数没有遍历出数据
    073_模糊查询
    072_jsp追加与刷新数据
    071_为什么要catch return.setCode()?
    070_jstl中的三目表达式
    069_都是查询语句时得事物?
    打印周报模板
  • 原文地址:https://www.cnblogs.com/hzcya1995/p/13307996.html
Copyright © 2020-2023  润新知