0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

如何用DFS算法来秒杀岛屿系列问题

算法与数据结构 来源:labuladong 作者:labuladong 2021-11-16 17:13 次阅读

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

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

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

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

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

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

//二维矩阵遍历框架
voiddfs(int[][]grid,inti,intj,boolean[]visited){
intm=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);//上
dfs(grid,i+1,j);//下
dfs(grid,i,j-1);//左
dfs(grid,i,j+1);//右
//后序:离开节点(i, j)
// visited[i][j]=true;
}

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

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

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

voiddfs(int[][]grid,inti,intj,boolean[]visited){
intm=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){
intnext_i=i+d[0];
intnext_j=j+d[1];
dfs(grid,next_i,next_j);
}
//离开节点(i,j)
// visited[i][j]=true;
}

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

岛屿数量

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

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

intnumIslands(char[][]grid);

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

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

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

//从(i,j)开始,将与之相邻的陆地都变成海水
voiddfs(char[][]grid,inti,intj){
intm=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,就可以起到不走回头路的作用。

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

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

封闭岛屿的数量

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

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

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

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

函数签名如下:

intclosedIsland(int[][]grid)

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

a6addc6e-46b3-11ec-b939-dac502259ad0.png

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

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

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

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

//从(i,j)开始,将与之相邻的陆地都变成海水
voiddfs(int[][]grid,inti,intj){
intm=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);
}

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

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

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

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

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

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

returnres;
}

//和之前的实现类似
voiddfs(int[][]grid,inti,intj){
//...
}

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

岛屿的最大面积

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

intmaxAreaOfIsland(int[][]grid)

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

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

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

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

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

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

returndfs(grid,i+1,j)
+dfs(grid,i,j+1)
+dfs(grid,i-1,j)
+dfs(grid,i,j-1)+1;
}

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

子岛屿数量

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

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

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

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

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

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

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

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

//从(i,j)开始,将与之相邻的陆地都变成海水
voiddfs(int[][]grid,inti,intj){
intm=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)岛屿数量,函数签名如下:

intnumDistinctIslands(int[][]grid)

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

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

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

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

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

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

voiddfs(int[][]grid,inti,intj){
//递归顺序:
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函数,添加一些函数参数以便记录遍历顺序:

voiddfs(int[][]grid,inti,intj,StringBuildersb,intdir){
intm=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函数就好办了,我们可以直接写出最后的解法代码:

intnumDistinctIslands(int[][]grid){
intm=grid.length,n=grid[0].length;
//记录所有岛屿的序列化结果
HashSetislands=newHashSet<>();
for(inti=0;i< m; i++) {
        for(intj=0;j< n; j++) {
            if(grid[i][j]==1){
//淹掉这个岛屿,同时存储岛屿的序列化结果
StringBuildersb=newStringBuilder();
//初始的方向可以随便写,不影响正确性
dfs(grid,i,j,sb,666);
islands.add(sb.toString());
}
}
}
//不相同的岛屿数量
returnislands.size();
}

这样,这道题就解决了,至于为什么初始调用dfs函数时的dir参数可以随意写,这里涉及 DFS 和回溯算法的一个细微差别,前文图算法基础有写,这里就不展开了。

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


声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • 算法
    +关注

    关注

    23

    文章

    4454

    浏览量

    90747
  • 数组
    +关注

    关注

    1

    文章

    409

    浏览量

    25593
  • DFS
    DFS
    +关注

    关注

    0

    文章

    24

    浏览量

    9105

原文标题:DFS 算法秒杀五道岛屿问题

文章出处:【微信号:TheAlgorithm,微信公众号:算法与数据结构】欢迎添加关注!文章转载请注明出处。

收藏 人收藏

    评论

    相关推荐

    RTThread4.1.1在spiflash上挂dfs文件系统报互斥锁错误的原因?

    最近使用gd32f450vg芯片,在SPI4接口上挂了gd25q32,想使用dfs文件系统,gd25q32能够正常的识别,显示文件系统挂载正常,但是只要操作文件系统就会出现报错,看像是互斥锁的问题,请问这个要从哪个方向查原因
    发表于 03-05 07:39

    请问CYW43012支持DFS/雷达吗?

    CYW43012是否支持DFS/雷达? 通过字符串命令检查了 fmac 包中的所有固件后,它似乎不支持。 我在固件标签中找不到-dfsradar。
    发表于 03-01 09:33

    CYW4373是否支持 ETSI EN301893中指定的 DFS(动态频率选择)?

    CYW4373是否支持 ETSI EN301893中指定的 DFS(动态频率选择)?
    发表于 03-01 08:32

    java结合redis秒杀功能

    近年来,随着电子商务的快速发展,各大电商平台都推出了各种促销活动来吸引用户。秒杀活动作为一种高效的促销方式,能够在很短的时间内促成大量的交易。然而,高并发场景下的秒杀活动也给系统带来了巨大的压力
    的头像 发表于 12-04 11:06 255次阅读

    DFS分布式存储论坛在徐成功举办,优网中分融合成果正式发布

    11月8日,备受瞩目的DFS2023分布式存储论坛(徐州)暨中分融合成果发布会在徐州举行。来自商界、政界、学界等领域与会嘉宾共同探讨分布式存储发展所面临的挑战和机遇,提振发展信心,最大限度凝聚共识
    的头像 发表于 11-08 15:44 354次阅读
    <b class='flag-5'>DFS</b>分布式存储论坛在徐成功举办,优网中分融合成果正式发布

    5G-DFS最新动态-产品不在需要走FCC官方测试

    改变是将带有雷达侦测功能的DFS(Dynamic Frequency Selection)产品从PAG清单中移除。这意味着未来具备雷达侦测功能的Master或Slave产品不再需要走PAG程序,从而
    的头像 发表于 11-06 17:00 590次阅读

    何用热敏电阻测量温度?

    何用热敏电阻测量温度
    发表于 11-03 06:01

    华为云应用中间件 DCS 系列 | Redis 实现(电商网站)秒杀抢购示例

    云服务、API、SDK,调试,查看,我都行  阅读短文您可以学习到:应用中间件系列之 Redis 实现(电商网站)秒杀抢购示例 什么是 DEVKIT  华为云开发者插件(Huawei Cloud
    的头像 发表于 10-25 22:42 247次阅读
    华为云应用中间件 DCS <b class='flag-5'>系列</b> | Redis 实现(电商网站)<b class='flag-5'>秒杀</b>抢购示例

    何用FPGA实现FFT算法

    长度N的平方成正比。当N较大时,因计算量太大,直接用DFT算法进行谱分析和信号的实时处理是不切实际的。快速傅立叶变换(Fast Fourier Transformation,简称FFT)使DFT运算效率
    的头像 发表于 10-09 14:30 550次阅读

    stc89c52怎么加入傅里叶算法测量体温脉搏?

    毕业设计题目是基于单片机的体温脉搏测量系统,请教大神怎样加入傅里叶算法测量体温脉搏,并且得到结果后又该用什么方法后者算法分析得到的结果
    发表于 10-08 06:39

    智慧矿山ai算法系列解析 堵料检测算法功能优势

    智慧矿山AI算法系列中的堵料检测算法的功能优势,了解其重要性和带来的价值
    的头像 发表于 09-28 18:48 356次阅读
    智慧矿山ai<b class='flag-5'>算法系列</b>解析 堵料检测<b class='flag-5'>算法</b>功能优势

    DFS头文件与源文件不对应如何解决?

    dfs_fd这个结构体的地方出错,全部集中在dfs_lfs.c中在使用,如下图所示: 3、查看了dfs_file.h中的内容,并不存在struct dfs_fd这个结构体,而是另外两
    发表于 09-08 16:53

    如何控制秒杀商品页面购买按钮的点亮

    1 秒杀业务分析 正常电子商务流程   (1)查询商品;(2)创建订单;(3)扣减库存;(4)更新订单;(5)付款;(6)卖家发货   秒杀业务的特性   (1)低廉价格;(2)大幅推广;(3)瞬时
    的头像 发表于 06-29 11:12 507次阅读
    如何控制<b class='flag-5'>秒杀</b>商品页面购买按钮的点亮

    何用远程协助软件控制对方电脑?

    何用远程协助软件控制对方电脑?
    的头像 发表于 05-23 17:49 785次阅读
    如<b class='flag-5'>何用</b>远程协助软件控制对方电脑?

    F103的片内FLASH不能用DFS

    因为socket通讯想用非阻塞的select,但是看要使用select是依赖于DFS的,想想加个文件系统也是可以的,但是搜了很多资料,貌似都是在SD Card、SPI Flash、Nand
    发表于 05-11 11:16