剑指 Offer 每日一题 | 10、机器人运动范围

421 阅读3分钟

这是我参与8月更文挑战的第17天,活动详情查看:8月更文挑战

一、前言

大家好,本文章属于《剑指 Offer 每日一题》中的系列文章中的第 10 篇。

在该系列文章中我将通过刷题练手的方式来回顾一下数据结构与算法基础,同时也会通过博客的形式来分享自己的刷题历程。如果你刚好也有刷算法题的打算,可以互相鼓励学习。我的算法基础薄弱,希望通过这两三个月内的时间弥补这块的漏洞。本次使用的刷题语言为 Java ,预计后期刷第二遍的时候,会采用 Python 来完成。

二、题目

地上有一个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

实例2:

 输入:m = 3, n = 1, k = 0
 输出:1

提示:

  • 1 <= n,m <= 100
  • 0 <= k <= 20

三、解题

1、如何计算行坐标 和列坐标的数位之和

对于一个整数 n,我们可以用下面这个模板来计算

  //计算两个坐标数字的和
     private static int sum(int i, int j) {
         int sum = 0;
         while (i != 0) {
             sum += i % 10;
             i /= 10;
         }
         while (j != 0) {
             sum += j % 10;
             j /= 10;
         }
         return sum;
     }

因此,给定行坐标和列坐标可以用这个方法来得出。

2、哪些格子在运动范围之内?

这里可能有同学想双重for循环来查找,但是这个题目的条件是有限制的,即格子必须是 能从 坐标(0,0)点通过上下左右能移动到的,如果有不能移动到的点,比如 m=5,n=10,k=3 那么 (2,10)可以满足,但是这个却不能从 (0,0)点移动获得,所以双重 for 被排除了。

3.1 思路1:DFS 深度遍历

深度优先遍历,可以理解为朝一个方向走到底,如果走不通了再退到上一个位置在从其他方向走到底,然后依次回退到上个节点。

算法流程,我们只需从 (0,0)点开始,从上下左右四个方向依次回溯搜索就行,如果查找到,就把该位置设为 已访问,下次就不会进入这个位置了。提升查询效率,这叫剪枝。因为从 (0,0)点开始查询,因此不用向上和向左遍历了。这也是优化。

3.1.1 代码
 class Solution {
     public int movingCount(int m, int n, int k) {
         boolean[][] memory = new boolean[m][n];// 初始化为false
         return dfs(m,n,k,memory,0,0);
     }
     private int dfs(int m, int n, int k,boolean[][] memory,int i,int j) {
       if(i>=m || i<0 ||j>=n || j<0 ||  (i/10 + i%10 + j/10 + j%10) > k || memory[i][j]){
            return 0;
        }
        memory[i][j]=true;
        return dfs(m,n,k,memory,i+1,j) +dfs(m,n,k,memory,i,j+1)+1;
     }
    
 }
3.1.2 执行效果

image-20210817214127257

3.1.3 复杂度分析
  • 时间复杂度:O ( m×n)。
  • 空间复杂度:O(m×n)。

3.2 思路2:BFS 广度遍历

广度遍历一般采用队列来实现,我们先把(0,0)加入队列,当队列不为空时,每次将队首坐标出队,加入到集合中,再将满足条件的坐标加入队尾,直到队列为空。

3.2.1 代码
  public static int movingCount(int m, int n, int k) {
         //临时变量visited记录格子是否被访问过
         boolean[][] visited = new boolean[m][n];
         int res = 0;
         //创建一个队列,保存的是访问到的格子坐标,是个二维数组
         Queue<int[]> queue = new LinkedList<>();
         //从左上角坐标[0,0]点开始访问,add方法表示把坐标
         // 点加入到队列的队尾
         queue.add(new int[]{0, 0});
         while (queue.size() > 0) {
             //这里的poll()函数表示的是移除队列头部元素,因为队列
             // 是先进先出,从尾部添加,从头部移除
             int[] x = queue.poll();
             int i = x[0], j = x[1];
             //i >= m || j >= n是边界条件的判断,k < sum(i, j)判断当前格子坐标是否
             // 满足条件,visited[i][j]判断这个格子是否被访问过
             if (i >= m || j >= n || k < sum(i, j) || visited[i][j])
                 continue;
             //标注这个格子被访问过
             visited[i][j] = true;
             res++;
             //把当前格子下边格子的坐标加入到队列中
             queue.add(new int[]{i + 1, j});
             //把当前格子右边格子的坐标加入到队列中
             queue.add(new int[]{i, j + 1});
         }
         return res;
     }
     //计算两个坐标数字的和
     private static int sum(int i, int j) {
         int sum = 0;
         while (i != 0) {
             sum += i % 10;
             i /= 10;
         }
         while (j != 0) {
             sum += j % 10;
             j /= 10;
         }
         return sum;
     }
3.2.2 执行效果

image-20210817220515000

3.2.3 复杂度分析
  • 时间复杂度:O ( m×n)。
  • 空间复杂度:O ( m×n*)。

四、小结

本题考察了对回溯法的理解,同时需要注意深度优先往往采用递归类似栈的结构来处理,广度优先往往采用队列的方式来处理。通常情况下,回溯法用来解决迷宫类的问题,在二维数组中的应用非常频繁,值得总结掌握。

今天的刷题就到这了,欢迎点赞评论交流,剑指 Offer 刷题之旅将继续展开!