LeetCode 1254. Number of Closed Islands【DFS,BFS,并查集】中等

news/2024/7/20 23:04:12 标签: leetcode, 深度优先, 宽度优先

本文属于「征服LeetCode」系列文章之一,这一系列正式开始于2021/08/12。由于LeetCode上部分题目有锁,本系列将至少持续到刷完所有无锁题之日为止;由于LeetCode还在不断地创建新题,本系列的终止日期可能是永远。在这一系列刷题文章中,我不仅会讲解多种解题思路及其优化,还会用多种编程语言实现题解,涉及到通用解法时更将归纳总结出相应的算法模板。

为了方便在PC上运行调试、分享代码文件,我还建立了相关的仓库:https://github.com/memcpy0/LeetCode-Conquest。在这一仓库中,你不仅可以看到LeetCode原题链接、题解代码、题解文章链接、同类题目归纳、通用解法总结等,还可以看到原题出现频率和相关企业等重要信息。如果有其他优选题解,还可以一同分享给他人。

由于本系列文章的内容随时可能发生更新变动,欢迎关注和收藏征服LeetCode系列文章目录一文以作备忘。

二维矩阵 grid 由 0 (土地)和 1 (水)组成。岛是由最大的4个方向连通的 0 组成的群,封闭岛是一个 完全 由1包围(左、上、右、下)的岛。请返回 封闭岛屿 的数目。

示例 1:

输入:grid = [[1,1,1,1,1,1,1,0],[1,0,0,0,0,1,1,0],[1,0,1,0,1,1,1,0],[1,0,0,0,0,1,0,1],[1,1,1,1,1,1,1,0]]
输出:2
解释:
灰色区域的岛屿是封闭岛屿,因为这座岛屿完全被水域包围(即被 1 区域包围)。

示例 2:

输入:grid = [[0,0,1,0,0],[0,1,0,1,0],[0,1,1,1,0]]
输出:1

示例 3:

输入:grid = [[1,1,1,1,1,1,1],
             [1,0,0,0,0,0,1],
             [1,0,1,1,1,0,1],
             [1,0,1,0,1,0,1],
             [1,0,1,1,1,0,1],
             [1,0,0,0,0,0,1],
             [1,1,1,1,1,1,1]]
输出:2

提示:

  • 1 <= grid.length, grid[0].length <= 100
  • 0 <= grid[i][j] <= 1

本题为「200. 岛屿数量」的变形题目,解法几乎一样,本质是均为遍历图中的连通区域,唯一不同的是本题中的岛屿要求是「封闭」的,根据题意可以知道「封闭岛屿」定义如下:完全由 1 1 1 包围(左、上、右、下)的岛

设矩阵的行数与列数分别为  m , n m,n m,n ,如果从一个 0 0 0(岛屿格子)出发,向四方向的陆地格子移动,可以移动到网格图的边界最外面一圈的格子,即第 0 0 0 行、第 0 0 0 列,第 m − 1 m - 1 m1 行、第 n − 1 n - 1 n1,那么这个 0 0 0 所处的岛屿就不是封闭的;反之,如果无法移动到网格图边界,就是封闭的,说明这个岛屿的上下左右 1 1 1(水域格子)包围住

从这个角度出发,网格图的行数小于 3 3 3 行或列数小于 3 3 3 列,就不存在封闭岛屿。下面分为从里到外和从外到里两种写法。

解法1 DFS+出界标记

从不在边界的 0 0 0 出发,DFS访问四方向的 0 0 0 。DFS之前,设置全局变量 c l o s e d closed closed t r u e true true 。如果DFS中到达边界,设置 c l o s e d closed closed f a l s e false false ,意味着当前遍历的岛屿不是封闭岛屿。注意把访问过的 0 0 0 改成 1 1 1 ,避免重复访问

还要注意,每次DFS应当把「这个岛屿的非边界格子」都遍历完。如果在中途退出DFS,会导致某些格子没有遍历到,那么在后续以这个格子为起点DFS时,可能会误把它当作封闭岛屿上的格子,从而算出比预期结果更大的值。

递归结束时,如果 c l o s e d closed closed 仍然为 t r u e true true ,说明当前遍历的是一个封闭岛屿,答案加一。

class Solution {
    private boolean closed;
    private void dfs(int[][] g, int i, int j) {
        if (i == 0 || i == g.length - 1 || j == 0 || j == g[i].length - 1) {
            if (g[i][j] == 0) closed = false; // 到达边界
            return;
        }
        if (g[i][j] != 0) return;
        g[i][j] = 1; // 标记(i,j)被访问,避免重复访问
        dfs(g, i - 1, j);
        dfs(g, i + 1, j);
        dfs(g, i, j - 1);
        dfs(g, i, j + 1);
    }

    public int closedIsland(int[][] grid) {
        int m = grid.length, n = grid[0].length, ans = 0;
        if (m < 3 || n < 3) return 0; // 特判
        for (int i = 1; i + 1 < m; ++i) {
            for (int j = 1; j + 1 < n; ++j) {
                if (grid[i][j] == 0) {
                    closed = true;
                    dfs(grid, i, j);
                    if (closed) ++ans; 
                }
            }
        }
        return ans;
    }
}

复杂度分析:

  • 时间复杂度: O ( m n ) O(mn) O(mn),其中 m m m n n n 分别为 g r i d grid grid 的行数和列数。
  • 空间复杂度: O ( m n ) O(mn) O(mn)。递归最坏需要 O ( m n ) O(mn) O(mn) 的栈空间(想象一个蛇形的 0 0 0 连通块)。

解法2 DFS+先外后内

做法2是,既然关键是「边界」,那么不妨从边界(的 0 0 0 即岛屿格子)出发,先标记所有非封闭岛屿。标记完后,网格图内部的 0 0 0 就一定在封闭岛屿上,每次从一个新的 0 0 0 出发进行DFS,就是一个新的封闭岛屿。

从网格图的第一行、最后一行、第一列和最后一列的所有 0 0 0 出发,DFS访问四方向的 0 0 0 ,并把这些 0 0 0 标记成「访问过」。代码实现时可以直接把 0 0 0 修改成 1 1 1 。注意,此时将网格图外作为边界!

然后从剩下的 0 0 0 出发,按照同样的方式DFS访问四方向的 0 0 0 ,同时把 0 0 0 改成 1 1 1每次从一个新的 0 0 0 出发(起点),就意味着找到了一个新的封闭岛屿,答案加一

class Solution {
    private boolean closed;
    private void dfs(int[][] g, int i, int j) {
        if (i < 0 || i >= g.length || j < 0 || j >= g[i].length || g[i][j] != 0) 
            return; // 到达边界 
        g[i][j] = 1; // 标记(i,j)被访问,避免重复访问
        dfs(g, i - 1, j);
        dfs(g, i + 1, j);
        dfs(g, i, j - 1);
        dfs(g, i, j + 1);
    }

    public int closedIsland(int[][] grid) {
        int m = grid.length, n = grid[0].length, ans = 0;
        if (m < 3 || n < 3) return 0; // 特判
        for (int i = 0; i < m; ++i) {
            // 如果是第一行和最后一行,访问所有格子
            // 否则,只访问第一列和最后一列的格子
            int step = (i == 0 || i == m - 1) ? 1 : n - 1;
            for (int j = 0; j < n; j += step)
                dfs(grid, i, j);
        } 
        for (int i = 1; i + 1 < m; ++i) {
            for (int j = 1; j + 1 < n; ++j) {
                if (grid[i][j] == 0) { 
                    dfs(grid, i, j);
                    ++ans; // 一定是封闭岛屿 
                }
            }
        }
        return ans;
    }
}

复杂度分析:

  • 时间复杂度: O ( m n ) O(mn) O(mn),其中 m m m n n n 分别为 grid \textit{grid} grid 的行数和列数。
  • 空间复杂度: O ( m n ) O(mn) O(mn)。递归最坏需要 O ( m n ) O(mn) O(mn) 的栈空间(想象一个蛇形的 0 0 0 连通块)。

解法3 并查集

本题也可用并查集解决。由于岛屿由相邻的陆地连接形成,因此封闭岛屿的数目为不与边界相连的陆地组成的连通分量数。连通性问题可以使用并查集解决。假设可以对每个连通区域进行标记,如果该连通区域与边界连通,则该连通区域一定不是「封闭岛屿」,否则该连通区域为「封闭岛屿」

并查集初始化时,每个「不在边界上的陆地元素」分别属于不同的集合,为了方便处理,将所有在边界上的陆地元素归入同一个集合,称为边界集合,初始化时就将边界上的为 0 0 0 的元素全部纳入到集合 0 0 0 中。边界上的陆地元素的状态是与边界连通,其余单元格的状态都是不与边界连通,集合个数等于不在边界上的陆地元素个数

初始化之后,遍历每个元素(一定要遍历最后一行和最后一列),如果一个位置 ( x , y ) (x,y) (x,y) 是陆地元素、且其上边相邻位置 ( x − 1 , y ) (x - 1, y) (x1,y)左边相邻位置 ( x , y − 1 ) (x, y - 1) (x,y1) 是陆地元素,则将两个相邻陆地元素所在的集合做合并。因为所有在边界上的陆地元素都属于边界集合,每次合并都可能将一个「不在边界上的陆地元素」合并到边界集合。

遍历结束之后,利用哈希表,统计所有陆地元素构成的连通集合的数目为 t o t a l total total ,此时还需要检测边界集合 0 0 0 是否也包含在 total \textit{total} total 中,如果 total \textit{total} total 包含边界集合 0 0 0 中,则返回 total − 1 \textit{total} - 1 total1 ,否则返回 total \textit{total} total

class Solution {
    public int closedIsland(int[][] grid) {
        int m = grid.length, n = grid[0].length;
        UnionFind uf = new UnionFind(m * n);
        for (int i = 0; i < m; ++i) { // 第一列和最后一列
            if (grid[i][0] == 0) uf.merge(i * n, 0);
            if (grid[i][n - 1] == 0) uf.merge(i * n + n - 1, 0);
        }
        for (int j = 1; j < n - 1; ++j) { // 第一行和最后一行
            if (grid[0][j] == 0) uf.merge(j, 0);
            if (grid[m - 1][j] == 0) uf.merge((m - 1) * n + j, 0);
        }
        for (int i = 0; i < m; ++i) {
            for (int j = 0; j < n; ++j) {
                if (grid[i][j] == 0) { // 如果一个陆地上和左有陆地,则连通
                    if (i > 0 && grid[i - 1][j] == 0)
                        uf.merge(i * n + j, (i - 1) * n + j);
                    if (j > 0 && grid[i][j - 1] == 0)
                        uf.merge(i * n + j, i * n + j - 1);
                }
            }
        }
        var cnt = new HashSet<Integer>();
        for (int i = 0; i < m; ++i)
            for (int j = 0; j < n; ++j)
                if (grid[i][j] == 0)
                    cnt.add(uf.find(i * n + j));
        int total = cnt.size();
        if (cnt.contains(uf.find(0))) --total;
        return total;
    }
}
class UnionFind {
    private int[] parent;
    private int[] rank;
    public UnionFind(int n) {
        this.parent = new int[n];
        for (int i = 0; i < n; ++i) parent[i] = i;
        this.rank = new int[n]; // 每个集合的秩
    }
    public void merge(int x, int y) {
        int rx = find(x), ry = find(y);
        if (rx != ry) {
            if (rank[rx] > rank[ry]) parent[ry] = rx;
            else if (rank[rx] < rank[ry]) parent[rx] = ry;
            else { // 高度相同时
                parent[ry] = rx;
                ++rank[rx]; // 高度+1
            }
        }
    }
    public int find(int x) {
        return (parent[x] != x) ? (parent[x] = find(parent[x])) : parent[x];
    }
}

复杂度分析:

  • 时间复杂度: O ( m n × α ( m n ) ) O(mn \times \alpha(mn)) O(mn×α(mn)) ,其中 m m m n n n 分别是矩阵的行数和列数, α \alpha α 是反阿克曼函数。并查集的初始化需要 O ( m × n ) O(m \times n) O(m×n) 的时间,然后遍历 m × n m \times n m×n 个元素,最多执行 m × n m \times n m×n 次合并操作,这里的并查集使用了路径压缩和按秩合并,单次操作的时间复杂度是 O ( α ( m × n ) ) O(\alpha(m \times n)) O(α(m×n)) ,因此并查集合并的操作的时间复杂度是 O ( m n × α ( m n ) ) O(mn \times \alpha(mn)) O(mn×α(mn)) ,总时间复杂度是 O ( m n + m n × α ( m n ) ) = O ( m n × α ( m n ) ) O(mn + mn \times \alpha(mn)) = O(mn \times \alpha(mn)) O(mn+mn×α(mn))=O(mn×α(mn))
  • 空间复杂度: O ( m n ) O(mn) O(mn) ,其中 m , n m,n m,n 分别为矩阵的行数与列数。并查集需要 O ( m n ) O(mn) O(mn) 的空间用来存储集合关系。

http://www.niftyadmin.cn/n/447329.html

相关文章

如何打造创意百变的虚拟直播场景?

场景对于直播来说是直接呈现给观众的&#xff0c;也是直播带货的“直接”的视觉冲击的价值核心&#xff0c;所以场景的设计十分重要。今天&#xff0c;我们就一起来看看如何低成本搭建一个网红同款直播间吧&#xff01; 直播间类型 直播间大体可以分为三种类型&#xff1a;虚拟…

中阳期货最重要的是心态:静心、细心、宽心

路要自己走&#xff0c;苦要自己受&#xff0c;心要自己磨。功夫要自己练&#xff0c;经验要自己积&#xff0c;智慧要自己开悟。万物皆可悟心&#xff0c;心却需以技为根会心于技&#xff0c;方为实用&#xff1b;重剑无锋&#xff0c;大巧不工。见好能收&#xff0c;知进退、…

25k字图文解读YOLOv8及实例分割(附python代码)

学习使用 未经详细专业审核 目录 0.引言1.概述1.1 Backbone1.2 Head1.3 Loss1.4 Train 2.模型结构2.1 Backbone和Neck的具体变化2.2 Head的具体变化 3.Loss计算3.1 正负样本分配策略3.2 Loss计算 4.训练数据增强5.训练策略6.模型推理过程7.网络模型解析7.1 卷积神经单元&#x…

SpringCloud: SpringCloud面试题 ④

前言&#xff1a;面试题是一个以信息整合性看技术特性的一个手段。 1、什么是SpringCloud&#xff1f; springCloud是一系列框架的有序整合。目的在于大规模、分布式、微服务应用部署的解决方案。 2、什么是微服务&#xff1f; 微服务是将一个大而全的业务系统按照一定的业务…

HBase入门(一)

第1章 HBase简介 1.1 HBase定义 HBase是一种分布式、可扩展、支持海量数据存储的NoSQL数据库。 1.2 HBase数据模型 逻辑上&#xff0c;HBase的数据模型同关系型数据库很类似&#xff0c;数据存储在一张表中&#xff0c;有行有列。但从HBase的底层物理存储结构&#xff08;…

Writing for Engineers(作为工程师应该如何写作) —— Stemwede

Writing for Engineers&#xff08;作为工程师应该如何写作&#xff09; —— Stemwede 1. 写作前1.1. 有好的信息1.2 写作不是学习1.3 了解你的受众1.4 做好准备1.5 趁热打铁1.6 快速进入一个主题中 2. 写作时2.1 提纲挈领2.2 内容优于润色2.3 让文字可以略读2.4 提供摘要 3. …

玩转电脑|带你了解如何快速查看电脑开关机时间

目录 前言 1、打开管理 2、打开事件查看器 3、打开windows日志 5、获取开机事件 6、获取关机事件 7、保存事件 8、保存事件文件 9、打开事件文件 前言 最近因为一些原因作者想要查看自己电脑每天的的开关机时间记录&#xff0c;但是不知道怎么进行查看&#xff0c;于是在网…

深入解析Spring源码系列:Day 25 - Spring中的微服务

深入解析Spring源码系列&#xff1a;Day 25 - Spring中的微服务 欢迎阅读《深入解析Spring源码系列》的第25天&#xff01;在今天的文章中&#xff0c;我们将深入探讨Spring框架中微服务的概念、特点以及如何使用Spring构建和管理微服务。 什么是微服务架构 微服务架构是一种…