[LeetCode面试题13] 机器人运动范围,详解图的BFS和DFS,都是套路。

**图的深度优先遍历和广度优先遍历**

1、 BFS和DFS

图(Graph)也是一种重要的数据存储结构,图是由顶点的有穷非空集合和顶点之间边的集合组成,顶点与顶点之间可以是有方向,也可以是无方向的。有方向用箭头表示,对应图为有向图,无方向用直线表示,对应图为无向图。为了方便表示图的结构,引入二维邻接矩阵的概念,即顶点之间连接关系以矩阵的形式表达出来。矩阵中的每一点[ i , j ]表示 i 这个顶点能否到达顶点 j , 0表示不能到达,1表示能够到达且距离是1 。如下示意图:

在这里插入图片描述

以顶点1为例,1可以到达顶点0和顶点2,所以[1 , 0] 和[1 , 2]值是1。这里的1表示顶点之间的距离。邻接矩阵把复杂的图数字化为矩阵表示,这样可以方便后续的计算。对图操作最频繁就是图的遍历。访问邻接矩阵中值大于0的每个顶点。

图常用遍历方法有两种,广度优先遍历(Breadth_First_Search BFS)和深度优先遍历(Depth_First_Search DFS)。顾名思义,图的遍历以广度为优先级或者深度为优先级。广度意思是从一个点出发尽可能访问的范围广,也就是尽量把这个点周围的点都访问,而深度意思是从一个点出发尽可能访问的远,也就是尽量找一个点一直走下去直到这个点是终点,如下图所示,都是从a出发, 广度优先遍历,先访问b和c这两个点,再从b出发,只能访问d。c出发到e。然后d出发可以到达f和e(e已经访问过)。我们可以理解为一层一层的访问,b和c相对a点都在同一层,所以访问b和c,d和e相对于a在第二层,再访问d和e。也就是访问范围很广泛;深度优先遍历,以深度为主,尽可能地往下访问,从a出发,先访问b,b继续向后访问d,再访问e,e到了头。回到d,可以接着访问f,f到了头。回到d,d也没有可以访问结点了。回到b,b也没有,回到a,a可以访问c,这样再一直访问下去,直到所有的点遍历完。

在这里插入图片描述
我们可以发现,访问过程中,有个记忆化的过程,也就是访问过点记录下来,下次还来到这个点就知道访问过了,不用再访问了。理解访问思想后就要代码实现了。

2、代码实现

根据广度优先遍历和深度优先遍历性质,借助于数据结构实现这两种遍历方式。广度优先遍历用队列实现,深度优先遍历用栈实现。队列是先进先出的数据结构,当把a点附近两个点b和c入队,访问b后,b的周围点d再入队(不包括已经访问到的点)。结束b访问后,下一个就是访问c了,因为c先入队的。所以这就体现了广度为先的思想。而若选择栈,栈是先进后出的数据结构,那么结束b访问后,就下一个出栈的是d,因为d比c更晚入栈,所以先访问d,d的后面元素再继续入栈,直到没有了元素再回到b访问,这就体现了深度优先的思想。

例题: [LeetCode面试题13] 机器人运动范围

    地上有一个m行n列的方格,从坐标 [0,0] 到坐标 [m-1,n-1] 。
一个机器人从坐标 [0, 0] 的格子开始移动,它每次可以向左、右、上、下
移动一格(不能移动到方格外),也不能进入行坐标和列坐标的数位之和
大于k的格子。例如,当k为18时,机器人能够进入方格 [35, 37] ,
因为3+5+3+7=18。但它不能进入方格 [35, 38],因为3+5+3+8=19。
请问该机器人能够到达多少个格子?

示例1:

输入:m = 2 , n = 3 , k = 1

输出:3

刷题过程中,我们经常碰见,在一个棋盘或者方格中移动的问题。这些问题基本都涉及到图的遍历。棋盘和方格也就是二维数组的形象化,我们都可以数字化为一个二维数组,也就是在数组中遍历。访问也就是左移、右移、上移、下移这个动作,也就是横纵坐标加减变化。所以一般我习惯先准备两个数组,一个是方向偏移数组,方便计算四个方向移动中坐标的相加减,另一个是访问数组,记录每个点是否已经访问过。数组值数值类型是布尔型,true表示访问过这个点,false表示没有访问过。

int[][] offsetArray = new int[][]{{-1 , 0} , {0 , 1} , {1 , 0} , {0 , -1}};
boolean[][] visit = new boolean[row][len];

定义一个私有方法判断当前点是否在界内:

private boolean inBoard(int i , int j , int[][] grid) {
    return i >= 0 && i < grid.lenght && j >= 0 && j < grid[0].length;
}

以文章开头的例题为例,这题我觉得用DFS或者BFS更直接,不会考虑太多因素的影响,因为这题有坑,比如输入数据是38 , 15 , 9时,第一行所有的元素是可以都访问到的,但是第二行呢,[1 , 9 ]不能访问,而[1 , 10 ]又能访问,所以用数学遍历数组方法计算考虑因素太多,不如深度遍历或者广度遍历来的干脆。下面分别用广度优先和深度优先遍历实现:

广度优先遍历(BFS):


class Solution {
    public int movingCount(int m, int n, int k) {
        //方向偏移数组
        int[][] offsetArray = new int[][]{{0 , 1} , {1 , 0}};
        //记忆数组
        boolean[][] visit = new boolean[m][n];  
        Queue<int[]> queue = new LinkedList<>();
        
        int res = 1;
        queue.add(new int[]{0 , 0});
        while (!queue.isEmpty()) {
            int[] data = queue.poll();
            visit[data[0]][data[1]] = true;
            for (int i = 0 ; i < 2 ; i ++) {
                int newx = offsetArray[i][0] + data[0];
                int newy = offsetArray[i][1] + data[1];
                if (inBoard(m , n , newx , newy) && !visit[newx][newy] && moreThanK(newx , newy , k)) {
                    queue.add(new int[]{newx , newy});
                    visit[newx][newy] = true;
                    res++;
                }
            }
        }
        return res;
    }
    //判断[i , j]是否在界内
    private boolean inBoard(int m , int n , int i , int j) {
        return i >= 0 && i < m && j >= 0 && j < n;
    }
    //判断[i , j] 行列和是否小于等于k
    private boolean moreThanK(int i , int j , int k) {
        return (i%10 + i/10 + j%10 + j/10) <= k;
    }
}

每次访问到一个点,就计算这个点行列各个位数之和是否小于等于k值,符合要求就入队,记忆数组标记为已访问。这里有个小技巧,因为是从[ 0 , 0 ]原点开始访问的,每次访问顺序只有右移或者下移两个数组,所以方向偏移数组定义两个数即可。for循环就是遍历起点的各个方向,符合要求就入队,标记为访问过。

深度优先遍历:

class Solution {
    public int movingCount(int m, int n, int k) {
        //方向偏移数组
        int[][] offsetArray = new int[][]{{0 , 1} , {1 , 0}};
        //记忆数组
        boolean[][] visit = new boolean[m][n];  
        Stack<int[]> stack = new Stack<>();

        int res = 1;
        stack.push(new int[]{0 , 0});
        while (!stack.isEmpty()) {
            int[] data = stack.pop();
            visit[data[0]][data[1]] = true;
            for (int i = 0 ; i < 2 ; i ++) {
                int newx = offsetArray[i][0] + data[0];
                int newy = offsetArray[i][1] + data[1];
                if (inBoard(m , n , newx , newy) && !visit[newx][newy] && moreThanK(newx , newy , k)) {
                    stack.push(new int[]{newx , newy});
                    visit[newx][newy] = true;
                    res++;
                }
            }
        }
        return res;
    }
    //判断[i , j]是否在界内
    private boolean inBoard(int m , int n , int i , int j) {
        return i >= 0 && i < m && j >= 0 && j < n;
    }
    //判断[i , j] 行列和是否小于等于k
    private boolean moreThanK(int i , int j , int k) {
        return (i%10 + i/10 + j%10 + j/10) <= k;
    }
}

虽然只是换了个数据结构,代码没什么变化,但是访问顺序发生了很大的变化。

这题特殊在于起点是固定的,从[ 0 , 0 ] 开始,一般起点不确定情况,或者说需要比较各个起点下的最优结果,这时候遍历一遍二位数组,以各个点为起点即可。当然也不会每个点都会遍历到,会有剪枝的过程,比如只遍历值为1的点。

两种方法时间复杂度是O(mn) ,最好情况只访问原点,最坏情况是二维数组都会访问到,平摊下来mn级别,即与数组宽度和长度有关。空间复杂度都是O(mn) ,因为定义了记忆数组,需要mn级别的空间内存。

3、套路总结

一看到在棋盘或者很多方格中移动,或者在二维数组中寻找某些特定的值,就会涉及到图的遍历,根据题意选择广度优先遍历或者深度优先遍历。广度优先遍历以广度为主,访问越多越好;深度优先遍历,访问越远越好。这里的多或者远都是相对起点而言。广度优先遍历选择队列实现,深度优先遍历选择栈实现。然后准备方向偏移数组(一般为顺时针四个)和记忆数组(大小和给定数组一致)。首先把起点入队(栈),只要队列(栈)不为空,一直循环。每次出队(栈)元素作为新的起点,然后用一个for循环遍历周围四个方向的点,符合题意要求就入队(栈)。直到所有队列(栈)元素遍历完。
LeetCode上类似的题目有:
200题,岛屿数量
695题,岛屿最大面积
1162题,地图分析

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值