算法学习(8):LeetCode刷题之dfs网格递归

前言

深度优先搜索DFS算法通常出现在树结构或者图结构上,上一篇文章我们已经讲了二叉树的深度优先遍历,本文我们将二叉树的深度遍历继续引申,拓展到网格型深度优先遍历。

首先我们先来统一一下网格的概念,网格是由m * n个小方格组成的网状结构,每个小方格与其上下左右4个方格都是相邻的,题目要求在这样的网格中进行某种搜索。

本文借leetcode上的几个岛屿问题来讲解DFS,一般地,岛屿问题题目中每个格子中的数字是0或1。数字0代表海水,1代表陆地,相邻的陆地就连成了一个岛屿。
在这里插入图片描述
在这个前提下,就会衍生出各种岛屿问题,包括岛屿的数量,面积等。这些问题都能用DFS算法模板解决。

正文

1、DFS算法模板

网格是一种简化的图结构,要写好网格DFS,我们先来回顾一下二叉树的DFS是怎么写的。

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

二叉树的DFS遍历有2大步骤:判断最小规模base case和访问相邻节点。

第一要素:Base Case。一般地,二叉树的base case是root == null,也就是问题的最小规模是一颗空树,同时也代表了当root指向的树为空时,不再需要往下遍历了。

第二要素:访问相邻节点。二叉树访问相邻节点非常简单,因为节点半身就定义一个指针指向自己的左右孩子,只需要root.left和root.right即可访问相邻节点。

那么,参考二叉树的2个要素,我们来写出DFS的两个要素:

第一要素:Base Case。应该是网格中不能再继续递归的条件,那就是超过了格子的界限。

第二要素:访问相邻节点。网格中的格子有几个相邻节点?答案是4个,对于格子(i,j)来说,4个相邻的格子分别是(i+1,j), (i-1,j),(i,j+1),(i,j-1)。其实和二叉树没什么区别,可以把网格看成是四叉树。
在这里插入图片描述

至此,我们可以写出DFS算法的框架:

void dfs(int[][] grid, int i, int j) {
    // 判断 base case
    // 如果坐标 (i, j) 超出了网格范围,直接返回
    int m = grid.length;
    int n = grid[0].length;
    if (i < 0 || i >= m || j < 0 || j >= n) {
        return;
    }
    // 访问上、下、左、右四个相邻结点
    dfs(grid, r - 1, c);
    dfs(grid, r + 1, c);
    dfs(grid, r, c - 1);
    dfs(grid, r, c + 1);
}

但是,还有一个问题,网格类的DFS不同于二叉树DFS,遍历时有可能遇到已经遍历过的点,如果不进行处理,就会出现不停地兜圈子,永远停不下来。

如何解决呢?答案就是标记已经遍历过的格子,下次再遇到的话就不再遍历了。于是我们加上解决重复访问的代码,最终的网格类DFS模板就是:

void dfs(int[][] grid, int i, int j) {
    // 判断 base case
    // 如果坐标 (i, j) 超出了网格范围,直接返回
    int m = grid.length;
    int n = grid[0].length;
    if (i < 0 || i >= m || j < 0 || j >= n) {
        return;
    }
    // 如果这个格子不是岛屿,直接返回
    if (grid[i][j] != 1) {
        return;
    }
    grid[i][j] = 2; // 将格子标记为「已遍历过」,或者别的值都可以,只要和0或1不同就行
    
    // 访问上、下、左、右四个相邻结点
    dfs(grid, r - 1, c);
    dfs(grid, r + 1, c);
    dfs(grid, r, c - 1);
    dfs(grid, r, c + 1);
}

2、LeetCode No. 200 岛屿数量

给你一个由 '1'(陆地)和 '0'(水)组成的的二维网格,请你计算网格中岛屿的数量。
岛屿总被水包围,并且每座岛屿只能由水平方向和/或竖直方向上相邻的陆地连接形成。
此外,你可以假设该网格的四条边均被水包围。

示例 1:
输入:grid = [
  ["1","1","1","1","0"],
  ["1","1","0","1","0"],
  ["1","1","0","0","0"],
  ["0","0","0","0","0"]
]
输出:1

这道题是非常经典的DFS题目,可快速按照上面的模板写出算法:

public int numIslands(char[][] grid) {
        if (grid.length == 0) {
            return 0;
        }
        int ret = 0;
        for (int i = 0; i < grid.length; i++) {
            for (int j = 0; j < grid[i].length; j++) {
            	// 如果发现一个格子是陆地,则遇到一个岛屿,数量加一
                if (grid[i][j] == '1') {
                    ret++;
                    // 然后使用DFS将与这个格子相邻的格子置为“已访问”
                    dfs(grid, i , j);
                }
            }
        }
        return ret;
    }

	// 从(i,j)开始,将与之相邻的“陆地”置为“已访问”
    void dfs(char[][] grid, int i, int j) {
    	// 超过边界即返回
        if (i < 0 || i >= grid.length || j < 0 || j >= grid[0].length) {
            return;
        }
        // 如果不是陆地,直接返回
        if (grid[i][j] != '1') {
            return;
        }
        // 将该格子置为“已访问”
        grid[i][j] = '2';
        // 递归访问上下左右4个方向的格子
        dfs(grid, i + 1, j);
        dfs(grid, i, j + 1);
        dfs(grid, i - 1, j);
        dfs(grid, i, j - 1);
    }

3、LeetCode No. 1254 封闭岛屿的数量

这道题跟上一道题的区别在于,上一题规定二维数组的四周可以认为是被海水包围,所以靠近边界的陆地也算岛屿。而这道题规定上下左右全部被海水(用1表示,跟上题相反)包围的陆地才算作是岛屿,即题目中所说的封闭岛屿。

那么如何求封闭岛屿呢,只要将上一题的所有岛屿,减去靠边界的岛屿即可。代码如下:

public int closedIsland(int[][] grid) {
        int m = grid.length;
        int n = grid[0].length;
        // 先将靠近边界的岛屿DFS置为“已访问”
        for (int i = 0; i < m; i++) {
            dfs(grid, i, 0);
            dfs(grid, i, n -1);
        }
        for (int j = 0; j < n; j++) {
            dfs(grid, 0, j);
            dfs(grid, m - 1, j);
        }
		
		// 再进行岛屿统计
        int ret = 0;
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                if (grid[i][j] == 0) {
                    ret++;
                    dfs(grid, i, j);
                }
            }
        }
        return ret;
    }

	// dfs算法同上
    void dfs(int[][] grid, int i, int j) {
        if (i < 0 || i >= grid.length || j < 0 || j >= grid[0].length) {
            return;
        }
        if (grid[i][j] != 0) {
            return;
        }
        grid[i][j] = 2;
        dfs(grid, i + 1, j);
        dfs(grid, i, j + 1);
        dfs(grid, i - 1, j);
        dfs(grid, i, j - 1);
    }
  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
/* * (有向)深度优先遍历算法模板 */ package dsa; public abstract class DFS extends GraphTraverse { //变量 protected static int clock = 0;//遍历过程中使用的计时钟 //构造方法 public DFS(Graph g) { super(g); } //深度优先遍历算法 protected Object traverse(Vertex v, Object info) {//从顶点v出发,做深度优先查找 if (UNDISCOVERED != v.getStatus()) return null;//跳过已访问过的顶点(针对非连通) v.setDStamp(clock++); v.setStatus(DISCOVERED); visit(v, info);//访问当前顶点 for (Iterator it = v.outEdges(); it.hasNext();) {//检查与顶点v Edge e = (Edge)it.getNext();//通过边e = (v, u) Vertex u = (Vertex)e.getVPosInV(1).getElem();//相联的每一顶点u switch (u.getStatus()) {//根据u当前的不同状态,分别做相应处理 case UNDISCOVERED ://若u尚未被发现,则 e.setType(TREE);//e被归类为“树边” traverse(u, info);//从u出发,继续做深度优先查找 break; case DISCOVERED ://若u已经被发现,但对其访问尚未结束,则 e.setType(BACKWARD);//将e归类为“后向跨边” break; default ://VISITED,即对u的访问已经结束 if (u.getDStamp() < v.getDStamp())//若相对于v,u被发现得更早,则 e.setType(CROSS);//将e归类为“横跨边” else//否则 e.setType(FORWARD);//将e归类为“前向跨边” break; } }//至此,v的所有邻居都已访问结束,故 v.setFStamp(clock++); v.setStatus(VISITED);//将v标记为VISITED return null;//然后回溯 } }

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值