岛屿问题(DFS)

news2025/1/10 22:19:27

DFS的基本结构

网格结构比二叉树结构稍微复杂一点,它其实是一种简化版的图的结构。要写好网格上的DFS遍历,我们首先要理解二叉树上的DFS遍历方法,再类比写出网格结构上的DFS遍历。我们写的二叉树DFS遍历一般是:

public void traverse(TreeNode root) {
    //判断 base case
    if(root == null) {
        return;
    }
    //访问两个相邻结点:左子节点、右子节点
    traverse(root.left);
    traverse(root.right);
}

由此得出,二叉树的DFS有两个要素:【访问相邻结点】和【判断 base case】。

第一个要素是访问相邻结点。二叉树的相邻结点非常简单,只有左子结点和右子节点两个。二叉树本身就是一个递归定义的结构:一棵二叉树,它的左子树和右子树也是一棵二叉树。那么我们的DFS遍历只需要递归调用左子树和右子树即可。

第二个要素是判断base case。一般来说,二叉树遍历的base case是root == null。这样一个条件判断其实有两个含义:一方面,这表示 root 指向的子树为空,不需要再往下遍历了。另一方面,在root == null 的时候即时返回,可以让后面的 root.left 和 root.right 操作不会出现空指针异常。

对于网格上的DFS,我们完全可以参考二叉树的DFS,写出网格DFS的两个要素:

首先,网格结构中的格子 上下左右 四个相邻节点。对于格子 (r, c) 来说 (r 和 c 分别代表行坐标和列坐标),四个相邻的格子分别是 (r-1, c)、(r+1, c)、(r, c-1)、(r, c+1)。

其次,网格DFS中的base case是什么?从二叉树的base case 对应过来,应该是网格中不需要继续遍历、grid[r][c] 会出现数组下标越界异常的格子,也就是那些超出网格范围的格子。

这一点稍微有些反直觉,坐标竟然可以临时超出网格范围?这种方法我称为【先污染后治理】 ———甭管当前是在哪个格子,先往四个方向走一步再说,如果发现走出了网格范围再赶快返回。这跟二叉树的遍历方法是一样的,先递归调用,发现 root == null 再返回。

这样,我们得到了网格DFS遍历的框架代码:

public void dfs(int[][] grid,int r, int c) {
    //判断 base case
    //如果坐标(r, c)超过了网格范围,直接返回
    if(!inArea(grid, r, c)) {
        return;
    }
    //访问上、下、左、右四个相邻结点
    dfs(grid, r-1, c);
    dfs(grid, r+1, c);
    dfs(grid, r, c-1);
    dfs(grid, r, c+1);
}

public void inArea(int[][] grid, int r, int c) {
    return 0 <= r && r < grid.length && 0 <= c && c < grid[0].length;
}

如何避免重复遍历

网格结构的DFS与二叉树的DFS最大的不同之处在于,遍历中可能遇到遍历过的结点。这是因为,网格结构本质上是一个图,我们可以把每个格子看成图中的结点,每个结点有向上下遍历时边。在图中遍历时,自然可能遇到重复遍历结点。

这时候,DFS可能会不停地”兜圈子“,永远停不下来。

避免重复遍历,可以把已经遍历过的格子做一下标记。以岛屿问题为例,我们需要在所有值为1的陆地格子上做DFS遍历。每走过一个陆地格子,就把格子的值改为2,这样当我们遇到2的时候,就知道这是遍历过的格子 。就是说,每个格子可能取三个值:

  • 0 —— 海洋格子
  • 1 —— 陆地格子(未遍历过)
  • 2 —— 陆地格子(已遍历过)

加入避免重复遍历的语句之后:

public void dfs(int[][] grid, int r, int c) {
    //判断 base case
    if(!inArea(grid, r, c)) {
        return;
    }
    //如果这个格子不是岛屿,直接返回
    if(grid[r][c] != 1) {
        return;
    }
    grid[r][c] = 2; //将格子标记为[已遍历过]

    //访问上下左右四个相邻结点
    dfs(grid, r-1, c);
    dfs(grid, r+1, c);
    dfs(grid, r, c-1);
    dfs(grid, r, c+1);
}
//判断坐标是否在网格中
public boolean inArea(int[][] grid, int r, int c) {
    return 0 <= r && r < grid.length && 0 <= c && c < grid[0].length;
}

例题

1、岛屿的最大面积

695. 岛屿的最大面积 - 力扣(LeetCode)

给定一个包含了一些0和1的非空二维数组 grid,一个岛屿是一组相邻的1(代表陆地),这里的相邻要求两个1必须在水平或者竖直方向上相邻。你可以假设 grid 的四个边缘都被0(代表海洋)包围着。

找到给定的二维数组中最大的岛屿面积。如果没有岛屿,则返回面积为0。

 这道题只需要对每个岛屿做DFS遍历,求出每个岛屿的面积就可以了。求岛屿面积的方法也很简单,代码如下:

public int maxAreaOfIsland(int[][] grid) {
    int res = 0;
    for(int r = 0; r < grid.length; r++) {
        for(int c = 0; c < grid[0].length; r++) {
            if(grid[r][c] == 1) {
                int a = area(grid, r, c);
                res = Math.max(res, a);
            }
        }
    }
    return res;
}

public int area(int[][] grid, int r, int c) {
    if(!inArea(grid, r, c)) {
        return 0;
    }
    if(grid[r][c] != 1) {
        return 0;
    }
    grid[r][c] = 2;
    return 1
        + area(grid, r-1, c)
        + area(grid, r+1, c)
        + area(grid, r, c-1)
        + area(grid, r, c+1);
}

public boolean inArea(int[][] grid, int r, int c) {
    return 0 <= r && r < grid.length
        && 0 <= c && c < grid[0].length;
}

2、填海造陆问题

827. 最大人工岛 - 力扣(LeetCode)

这道题是岛屿最大面积的升级版。现在我们有填海造陆的能力,可以把一个海洋格子变成陆地格子,进而让两块岛屿连成一块。那么填海造陆之后,最大可能构造出多大的岛屿呢?

大致思路:我们先计算出所有岛屿的面积,在所有的格子上标记出岛屿的面积。然后搜索哪个海洋格子相邻的两个岛屿面积最大。例如下图中红色方框内的海洋格子,上边、左边都与岛屿相邻,我们可以计算出它变成陆地以后可以连接成的岛屿面积为7+1+2=10 

然后,这种做法可能遇到一个问题。如下图种红色方框内的海洋格子,它的上边、左边都与岛屿相邻,这时候连接成的岛屿面积难道是 7+1+7 ?!显然不是。这两个7来自同一个岛屿,所以填海造陆之后得到的岛屿面积应该只有 7+1=8 

 所以,我们要区分一个海洋格子相邻的两个7是不是来自同一个岛屿。所以,我们在方格中不能标记岛屿的面积,而应该标记岛屿的面积,如下图所示。这样我们就可以发现红色方框内的海洋格子,它的两个相邻的岛屿实际上是同一个。

可以看到,这道题实际上是对网格做了两边DFS:第一步遍历,计算面积并标记;第二遍DFS遍历,观察每个海洋相邻的陆地格子

class Solution {
    public int largestIsland(int[][] grid) {
        if (grid == null || grid.length == 0) return 1;
        int result = 0, index = 2;
        HashMap<Integer, Integer> areasMap = new HashMap();
        for (int i = 0; i < grid.length; i++)
            for (int j = 0; j < grid[0].length; j++)
                if (grid[i][j] == 1) areasMap.put(index, calculateAreas(index++, grid, i, j)); // 只计算未编号的岛屿

        if (areasMap.size() == 0) return 1; // 没有岛屿,全是海洋
        for (int i = 0; i < grid.length; i++) {
            for (int j = 0; j < grid[0].length; j++) {
                if (grid[i][j] == 0) {
                    Set<Integer> islands = getIslands(grid, i, j);
                    if (islands.size() == 0) continue; // 周围没有岛屿
                    result = Math.max(result, islands.stream().map(item -> areasMap.get(item)).reduce(Integer::sum).orElse(0) + 1);
                }
            }
        }
        if (result == 0) return areasMap.get(2); // 全是岛屿,没有海洋
        return result;
    }

    public int calculateAreas(int index, int[][] grid, int row, int column) {
        if (!isLegal(grid, row, column) || grid[row][column] != 1) return 0;
        grid[row][column] = index;
        return calculateAreas(index, grid, row + 1, column) + calculateAreas(index, grid, row - 1, column) + calculateAreas(index, grid, row, column - 1) + calculateAreas(index, grid, row, column + 1) + 1;
    }

    public boolean isLegal(int[][] grid, int row, int column) {
        return row >= 0 && row < grid.length && column >= 0 && column < grid[0].length;
    }

    public Set<Integer> getIslands(int[][] grid, int row, int column) {
        Set<Integer> result = new HashSet<>();
        if (isLegal(grid, row + 1, column) && grid[row + 1][column] != 0)
            result.add(grid[row + 1][column]);
        if (isLegal(grid, row - 1, column) && grid[row - 1][column] != 0)
            result.add(grid[row - 1][column]);
        if (isLegal(grid, row, column - 1) && grid[row][column - 1] != 0)
            result.add(grid[row][column - 1]);
        if (isLegal(grid, row, column + 1) && grid[row][column + 1] != 0)
            result.add(grid[row][column + 1]);
        return result;
    }
}

3、岛屿的周长

463. 岛屿的周长 - 力扣(LeetCode)

网格中的格子 水平和垂直 方向相连(对角线方向不相连)。整个网格被水完全包围,但其中恰好有一个岛屿(或者说,一个或多个表示陆地的格子相连组成的岛屿)。

岛屿中没有“湖”(“湖” 指水域在岛屿内部且不和岛屿周围的水相连)。格子是边长为 1 的正方形。网格为长方形,且宽度和高度均不超过 100 。计算这个岛屿的周长。

说实话,这道题用DFS来解并不是最优的方法。对于岛屿,直接用数学的方法求周长会更容易。不过这道题是一个很好理解DFS遍历过程的例题。

岛屿的周长是计算岛屿全部的边缘,而这些边缘就是我们在DFS遍历中,DFS函数返回的位置。观察题目示例,我们可以将岛屿的周长中的边分为两类,如下图所示。黄色的边是与网格边界相邻的周长,而蓝色的边是与海洋格子相邻的周长。

当我们的 dfs 函数因为坐标(r, c) 超出网格范围返回的时候,实际上就经过了一条黄色的边;而当函数因为当前格子是海洋格子返回的时候,实际上就经过了一条蓝色的边。这样,我们就把岛屿的周长跟DFS遍历联系起来了:

public int islandPerimeter(int[][] grid) {
    for(int r = 0; r < grid.length; r++) {
        for(int c = 0; c < grid[0].length; c++) {
            if(grid[r][c] == 1) {
                return dfs(grid, r, c);
            }
        }
    } 
    return 0;
}

public int dfs(int[][] grid, int r, int c) {
    if(!inArea(grid, r, c)) {
        return 1;
    }
    if(grid[r][c] == 0) {
        return 0;
    }
    grid[r][c] = 2;
    return dfs(grid, r+1, c)
        + dfs(grid, r-1, c)
        + dfs(grid, r, c+1)
        + dfs(grid, r, c-1);
}

public boolean inArea(int[][] grid, int r, int c) {
    return 0 <= r && r < grid.length
            && 0 <= c && c < grid[0].length;
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1388473.html

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

相关文章

我自己总结记忆的23种设计模式

1&#xff0c; 对23种设计模式&#xff0c;大家的第一个印象就是抽象繁琐&#xff0c;记不住&#xff01;&#xff01;不常用&#xff1f;&#xff1f; 其实设计模式是非常有用的&#xff0c;大家只要理解设计模式了&#xff0c;思想上就能有质的飞跃&#xff01; 但是&#…

有哪些好用的视频剪辑工具?这几款剪辑必备

有哪些好用的视频剪辑工具&#xff1f;随着社交媒体的兴起&#xff0c;视频内容已经成为人们获取信息、娱乐和交流的重要方式。对于许多内容创作者来说&#xff0c;选择一款合适的视频剪辑工具是至关重要的。今天&#xff0c;我们就来介绍几款视频剪辑工具&#xff0c;它们各具…

uniapp中,子组件给父页面传值(父组件)

前言 最近在做的一个小程序项目中&#xff0c;有一个身份切换的功能&#xff0c;点击切换按钮时&#xff0c;子组件向父组件传递身份信息&#xff0c;父页面依据这个身份信息对页面进行显示与隐藏。 具体实现 子组件中定义一个点击事件,在这里是identitySwitching()方法 &l…

flink1.15 维表join guava cache和mysql方面优化

优化前 mysql响应慢,导致算子中数据输出追不上输入,导致显示cpu busy:100% 优化后效果两个图对应两个时刻: - - -- 优化前 select l.id,JSON_EXTRACT(r.msg,$$.key1) as msgv (select id,uid from tb1 l where id?) join (select uid,msg from tb2) r on l.uidr.uid;-- 优化…

@Transactional注解导致@DS切换数据源失效

原因 spring 的Transactional声明式事务管理时通过动态代理实现的。 删除事物的注解 增加其他数据库的事务注解 Transactional(rollbackFor Exception.class, propagation Propagation.REQUIRES_NEW)

YOLOv8改进 | 检测头篇 | 利用DBB重参数化模块魔改检测头实现暴力涨点 (支持检测、分割、关键点检测)

一、本文介绍 本文给大家带来的改进机制是二次创新的机制,二次创新是我们发表论文中关键的一环,本文给大家带来的二次创新机制是通过DiverseBranchBlock(DBB)模块来改进我们的检测头形成一个新的检测头Detect_DBB,其中DBB是一种重参数化模块,其训练时采用复杂结构,推理时…

基于apriori关联规则的商品推荐系统

项目背景&#xff1a; 随着电子商务的快速发展&#xff0c;用户在购物平台上面临了海量商品选择的问题。为了帮助用户更好地找到自己感兴趣的商品&#xff0c;提供个性化的推荐服务变得至关重要。基于Apriori关联规则的商品推荐系统可以根据用户的历史购买记录进行分析&#x…

分布式限流要注意的问题

本文已收录至我的个人网站&#xff1a;程序员波特&#xff0c;主要记录Java相关技术系列教程&#xff0c;共享电子书、Java学习路线、视频教程、简历模板和面试题等学习资源&#xff0c;让想要学习的你&#xff0c;不再迷茫。 为什么需要匀速限流 同学们回想一下在Guava小节里…

如何部署Wagtail CMS并结合cpolar内网穿透实现远程访问管理界面

文章目录 前言1. 安装并运行Wagtail1.1 创建并激活虚拟环境 2. 安装cpolar内网穿透工具3. 实现Wagtail公网访问4. 固定的Wagtail公网地址 前言 Wagtail是一个用Python编写的开源CMS&#xff0c;建立在Django Web框架上。Wagtail 是一个基于 Django 的开源内容管理系统&#xf…

电商新趋势:解析养号的必要性及海外云手机运用攻略

在电商领域&#xff0c;什么最为关键&#xff1f;答案无疑是流量&#xff01;然而&#xff0c;如何以较低成本获取大量流量成为了许多电商从业者头疼的问题。虽然直接投放广告是一种方式&#xff0c;但在内卷的情况下效果越来越难以令人满意&#xff0c;高昂的广告费用也原来越…

编译与链接(C/C++)

在C/C中关于代码的运行需要经过.c文件到.exe文件&#xff0c;而其中走过这些步骤这需要对原始的.c文件进行编译与链接。对于编译与链接主要构成了翻译环境&#xff0c;经过翻译环境之后生成.exe文件&#xff0c;然后在通过运行环境输出对应的结果。本篇主要讲解编译与链接。 以…

动手搓一个kubernetes管理平台(1)-需求和框架

先拍个脑袋 市面上对于kubernetes集群的管理平台其实不算少&#xff0c;但常用的就那么几个&#xff0c;比如厚重的rancher&#xff0c;比如老而弥坚的kube-dashboard&#xff0c;以及集成了很多其他功能的kubeSphere等&#xff0c;但和其他开源项目一样&#xff0c;为了满足大…

火爆!大厂流出的接口版本号规约,速度收藏

在实际项目开发中&#xff0c;API的版本号控制不仅仅是一个数字游戏&#xff0c;它的使用需遵循语义版本控制&#xff08;Semantic Versioning&#xff09;原则&#xff0c;确保代码的每一次更改都能通过版本号的变化得到准确的体现&#xff0c;本篇文章对版本号如何使用做了详…

八款常用uml用例图-画图干货

随着软件开发和系统设计的复杂度不断提升&#xff0c;UML已经成为业界标准。UML用例图作为其中一种重要图表&#xff0c;能够清晰地展示系统功能和用户需求。本文将为你介绍八款常用的UML用例图&#xff0c;助你轻松搞定画图难题&#xff01; 一、费用报销系统UML用例图 二、登…

使用vite框架封装vue3插件,发布到npm

目录 一、vue环境搭建 1、创建App.vue 2、修改main.ts 3、修改vite.config.ts 二、插件配置 1、创建插件 2、开发调试 3、打包配置 4、package.json文件配置 上一篇文章讲述使用vite《如何使用vite框架封装一个js库&#xff0c;并发布npm包》封装js库&#xff0c;本文将…

Qt中QGraphicsView架构下实时鼠标绘制图形

上一章节介绍了关于QGraphicsView的基础讲解&#xff0c;以及简单的类图创建&#xff0c;由上一章节中最后展示的动画效果来看&#xff0c;今年主要讲述如何在QGraphicsView架构下&#xff0c;实时拖动鼠标绘制图形&#xff01; 今天主要以矩形为例&#xff0c;再来看一下展示…

通过浏览器URL地址,5分钟内渗透你的网站!很刑很可拷!

今天我来带大家简单渗透一个小破站&#xff0c;通过这个案例&#xff0c;让你深入了解为什么很多公司都需要紧急修复各个中间件的漏洞以及进行URL解析拦截等重要操作。这些措施的目的是为了保护网站和系统的安全性。如果不及时升级和修复漏洞&#xff0c;你就等着被黑客攻击吧&…

SAP不同语言开发

文章目录 1 Please write English Nmae2 go to goto menu and translation3 Write your target language .4 Please input Chinese5 Summary 1 Please write English Nmae 2 go to goto menu and translation 3 Write your target language . 4 Please input Chinese 5 Summary…

【Docker】Docker基础教程

&#x1f996;我是Sam9029&#xff0c;一个前端 &#x1f431;‍&#x1f409;&#x1f431;‍&#x1f409;恭喜你&#xff0c;若此文你认为写的不错&#xff0c;不要吝啬你的赞扬&#xff0c;求收藏&#xff0c;求评论&#xff0c;求一个大大的赞&#xff01;&#x1f44d; 基…

Unity中URP下的SimpleLit顶点着色器

文章目录 前言顶点着色器1、GPU Instance 相关2、顶点输入数据相关3、雾效混合因子4、对 uv 进行 Tilling 和 Offset 的应用 及 把顶点的坐标信息传给输出结构体5、把法线相关的结果&#xff0c;传给输出结构体6、光照贴图相关7、额外灯相关计算8、阴影相关 前言 在上一篇文章…