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

news2024/11/27 5:26:47

本文属于「征服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.coloradmin.cn/o/662328.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

单片机MCU如何实现让部分代码运行在RAM中

随着单片机硬件的发展&#xff0c;其中的RAM和flash越做越大。MCU在实际的使用中&#xff0c;通常程序都是运行在flash上的&#xff0c;RAM的高速空间并没有得到充分的利用&#xff0c;如果我们的程序需要运行的更快&#xff0c;系统有更好的实时性&#xff0c;我们可以考虑将这…

CSS查缺补漏之《常用长度单位(px、em、rem、%、vw/vh、vmin/vmax)》

此文内容较少&#xff0c;轻轻松松掌握&#xff0c;莫要有压力~ 正如现实生活中长度具有mm、dm、cm、m等&#xff0c;在css中&#xff0c;也具备多种长度单位&#xff0c;本文对常用的几种单位进行详细举例介绍~ px&#xff1a;像素单位 初学css时&#xff0c;px单位经常被使用…

【Leetcode60天带刷】day08字符串——344.反转字符串, 541. 反转字符串II,剑指Offer 05.替换空格,151.翻转字符串里的单词

题目&#xff1a; 344. 反转字符串 编写一个函数&#xff0c;其作用是将输入的字符串反转过来。输入字符串以字符数组 s 的形式给出。 不要给另外的数组分配额外的空间&#xff0c;你必须原地修改输入数组、使用 O(1) 的额外空间解决这一问题。 示例 1&#xff1a; 输入&…

基于SpringBoot+Vue的“漫画之家”系统设计与实现

博主介绍&#xff1a; 大家好&#xff0c;我是一名在Java圈混迹十余年的程序员&#xff0c;精通Java编程语言&#xff0c;同时也熟练掌握微信小程序、Python和Android等技术&#xff0c;能够为大家提供全方位的技术支持和交流。 我擅长在JavaWeb、SSH、SSM、SpringBoot等框架下…

新电脑机环境安装笔记

「Navicat_15.0.25_64bit_Setup.exe」 下载https://www.aliyundrive.com/s/b9xUw2JpuJb Navicat Keygen Patch v5.6.0 下载 https://www.aliyundrive.com/s/YYyE5BQMMuN 全程断网操作 patch 将安装目录选中 提示 check 64 mysql安装&#xff1a; https://baijiahao.baidu…

因子分析——SPSS实例分析

【续上篇主成分分析】 因子分析常用于通过可观测变量推断出其背后的公共因子&#xff08;也称为隐变量&#xff09;&#xff0c;样本在公共因子上的取值变化影响其在可观测变量上的取值&#xff0c;因为一般公共因子的个数小于可观测变量的数目&#xff0c;所以因子分析也可以…

渠道归因(一)传统渠道归因

渠道归因&#xff08;一&#xff09;传统渠道归因 小P&#xff1a;小H&#xff0c;我又来了。。。最近在做ROI数据&#xff0c;但是有个问题。。。 小H&#xff1a;什么问题&#xff0c;不就是收入/成本吗&#xff1f; 小P&#xff1a;是的&#xff0c;每个渠道的成本很容易计算…

基于html+css的图展示134

准备项目 项目开发工具 Visual Studio Code 1.44.2 版本: 1.44.2 提交: ff915844119ce9485abfe8aa9076ec76b5300ddd 日期: 2020-04-16T16:36:23.138Z Electron: 7.1.11 Chrome: 78.0.3904.130 Node.js: 12.8.1 V8: 7.8.279.23-electron.0 OS: Windows_NT x64 10.0.19044 项目…

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

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

CUDA共享内存详解

为什么需要共享内存&#xff1f; 共享内存的访问速度比访问全局速度快的多&#xff0c;因此对于多次访问全局内存的程序&#xff0c;特别是需要多次将全局内存的运算结果缓存到全局内存的运算&#xff0c;先将临时结果缓存到共享内存再做计算&#xff0c;会提高运算速度。 1、…

C语言使用Wininet库网络编程跳坑记 —— cookies篇

笔者尝试C语言使用Wininet库进行网络编程时&#xff0c;我尝试使用 InternetSetCookieA() 或 HttpAddRequestHeadersA() 设置 cookie。 HttpAddRequestHeadersA(Request, headers, header_len, HTTP_ADDREQ_FLAG_ADD | HTTP_ADDREQ_FLAG_REPLACE); InternetSetCookieA(url, NU…

基于SpringBoot+Vue的电影分享平台

✌全网粉丝20W,csdn特邀作者、博客专家、CSDN新星计划导师、java领域优质创作者,博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ &#x1f345;文末获取项目下载方式&#x1f345; 一、项目背景介绍&#xff1a; 当代社会&#xff0c;…

Linux(环境准备)VMware与CentOS及XShell的安装

目录 第 1 章 VMware 1.1 VMware 安装 1.1.1 VMware Workstation Pro 15.5 安装包 ​1.2.2 欢迎界面 1.2.3 同意许可证 1.2.4 选择安装路径 1.2.5 用户体检计划 1.2.6 快捷方式 1.2.7 开始安装 1.2.8 等待安装完成 1.2.9 安装完成 1.2.10 输入许可证 1.2.11 VM…

工欲善其事,必先利其器--Vscode嵌入式Linux开发远程开发设置(适用于多平台)

点击上方“嵌入式应用研究院”&#xff0c;选择“置顶/星标公众号” 干货福利&#xff0c;第一时间送达&#xff01; 来源 | 嵌入式应用研究院 整理&排版 | 嵌入式应用研究院 最近搭了一台Ubuntu18.04版本的桌面PC&#xff0c;不得不说比起Window搭虚拟机搞起来爽多了&…

python文件首行

类似于一切脚本文件一样&#xff0c;首行可用于指定解释器用于执行文件&#xff1b; 常见的是linux系统下的各个解释器。比如&#xff1a; #!/bin/sh– 使用Bourne shell或兼容的 shell执行文件&#xff0c;假定位于 /bin 目录中#!/bin/bash– 使用Bash shell执行文件#!/usr/…

会声会影如何抠图换背景 会声会影抠图后人物变透明

抠图换背景&#xff0c;大家可能会在图片编辑上应用得比较多。实际上&#xff0c;视频也能通过抠图的方式换背景&#xff0c;其困难程度与背景类型有关。纯色背景会比较简单&#xff0c;非纯色背景会比较难&#xff0c;接下来&#xff0c;一起来看看会声会影如何抠图换背景&…

Cocos Creator3D:制作可任意拉伸的 UI 图像

推荐&#xff1a;将 NSDT场景编辑器 加入你的3D工具链 3D工具集&#xff1a; NSDT简石数字孪生 制作可任意拉伸的 UI 图像 UI 系统核心的设计原则是能够自动适应各种不同的设备屏幕尺寸&#xff0c;因此我们在制作 UI 时需要正确设置每个控件元素的尺寸&#xff08;size&#…

java项目之病人跟踪治疗信息管理系统(ssm+vue)

风定落花生&#xff0c;歌声逐流水&#xff0c;大家好我是风歌&#xff0c;混迹在java圈的辛苦码农。今天要和大家聊的是一款基于ssm的病人跟踪治疗信息管理系统。项目源码以及部署相关请联系风歌&#xff0c;文末附上联系信息 。 &#x1f495;&#x1f495;作者&#xff1a;风…

【C语言复习】第五篇、关于C语言变量,常量,字符串,转义字符的知识

目录 第一部分、关于变量 1、什么是变量&#xff1f;变量的分类&#xff1f; 2、变量的作用域&#xff1f;变量的生命周期&#xff1f; 第二部分、关于常量 1、什么是常量&#xff1f; 2、如何定义常量&#xff1f; 第三部分、关于字符串 1、什么是字符串&#xff1f; …

2023年春季学期NLP总结作业

自然语言处理&#xff08;Natural Language Processing&#xff0c;NLP&#xff09;是计算机科学&#xff0c;人工智能&#xff0c;语言学关注计算机和人类&#xff08;自然&#xff09;语言之间的相互作用的领域。自然语言处理是计算机科学领域与人工智能领域中的一个重要方向…