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

示例 2

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

解题思路

此类问题通常可使用 深度优先搜索(DFS)广度优先搜索(BFS) 解决。在介绍 DFS / BFS 算法之前,为提升计算效率,首先讲述两项前置工作: 数位之和计算搜索方向简化

数位之和计算:
  • 设一数字 xx ,向下取整除法符号 //,求余符号⊙ ,则有:
    x⊙10 :得到 xx 的个位数字;
    x//10 : 令 xx 的十进制数向右移动一位,即删除个位数字。
  • 因此,可通过循环求得数位和 ss ,数位和计算的封装函数如下所示:
def sums(x):
    s = 0
    while x:
        s += x % 10
        x = x // 10
    return s
  • 由于机器人每次只能移动一格(即只能从 x 运动至 x±1),因此每次只需计算 x 到 x±1 的数位和增量。本题说明 1 ≤ n , m ≤ 1001 ≤ n , m ≤ 100 1 \leq n,m \leq 1001≤n,m≤100 1n,m1001n,m100 ,以下公式仅在此范围适用。
  • 数位和增量公式: 设 xx 的数位和为 s x s_x sx, x+1的数位和为 s x + 1 s_{x+1} sx+1
    1. (x+1)⊙ 10 = 0时: s x + 1 = s x − 8 s_{x+1} = s_x - 8 sx+1=sx8,例如19,20的数位和分别为10,2
    2. (x+1)⊙ 10 ≠ 0时: s x + 1 = s x + 1 s_{x+1} = s_x + 1 sx+1=sx+1,例如1,2的数位和分别为1,2

以下代码为增量公式的三元表达式写法,将整合入最终代码中。

pythonjava
s x s_x sx + 1 if (x + 1) % 10 else s x − 8 s_x - 8 sx8(x + 1) % 10 == 1 ? s x + 1 : s x − 8 s_x + 1 : s_x - 8 sx+1:sx8
搜索方向简化:
  • 数位和特点:根据数位和增量公式得知,数位和每逢进位突变一次。
  • 解的三角形结构:
    1. 根据数位和特点,矩阵中满足数位和的解构成的几何形状如多个等腰直角三角形,每个三角形的直角顶点位于0,10,20,…等数位和突变的矩阵索引处。
    2. 三角形内的解虽然都满足数位和要求,但由于机器人每步只能走一个单元格,而三角形间不一定是连通的,因此机器人不一定能到达,称之为 不可达解;同理,可到达的解称为可达解
  • 结论:根据可达解的结构,易推出机器人可仅通过向右和向下移动,访问所有可达解
    1. 三角形内部:全部连通,易证;
    2. 两三角形连通处:若某三角形内的解为可达解,则比其左边或上边的三角形连通(即相交),即机器人必可从左边或上边走进此三角形。

图例展示了 n , m = 20 , k ∈ [ 6 , 19 ] 的 可 达 解 、 不 可 达 解 、 非 解 , 以 及 连 通 性 的 变 化 n,m = 20,k \in [6,19]的可达解、不可达解、非解,以及连通性的变化 nm=20k[6,19]

在这里插入图片描述

方法一:深度优先遍历 DFS
  • 深度优先搜索:可以理解为暴力模拟机器人在矩阵中所有的路径。DFS通过递归,先朝一个方向搜到底,再回溯至上个节点,沿另一个方向搜索,以此类推。
  • 剪枝:在搜索中,遇到数位和超出目标值、此元素已访问,即应立即返回,称之为可行性剪枝
算法解析:
  • 递归参数:当前元素在矩阵中行列索引ij,两者的数位和sisj
  • 终止条件:当 ① 行列索引越界或 ② 数位和超出目标值k或 ③ 当前元素已访问过 时,返回 00 ,代表不计入可达解。
  • 递推工作
    1. 标记当前单元格:将索引(i,j)存入Setvisited中,代表此单元格已被访问过。
    2. 搜索下一单元格:计算当前元素的下、右两个方向元素的数位和,并开启下层递归。
  • 回溯返回值:返回1 + 右方搜索的可达解总数 + 下方搜索的可达解总数,代表从本单元格递归搜索的可达解总数。
    在这里插入图片描述
复杂度分析:

M,N 分别为矩阵行列大小。

  • 时间复杂度 O(MN): 最差情况下,机器人遍历矩阵所有单元格,此时时间复杂度为 O(MN)。
  • 空间复杂度 O(MN): 最差情况下,Setvisited 内存储矩阵所有单元格的索引,此时时间复杂度为 O(MN)。
代码:

Java代码中visited为辅助矩阵,Python中为Set。

# Python DFS实现方式
class Solution:
    def movingCount(self, m: int, n: int, k: int) -> int:
        def dfs(i, j, si, sj):
            if not 0 <= i < m or not 0 <= j < n or k < si + sj or (i, j) in visited: return 0
            visited.add((i,j))
            return 1 + dfs(i + 1, j, si + 1 if (i + 1) % 10 else si - 8, sj) + dfs(i, j + 1, si, sj + 1 if (j + 1) % 10 else sj - 8)

        visited = set()
        return dfs(0, 0, 0, 0)
// java DFS实现方式
class Solution {
    int m, n, k;
    boolean[][] visited;
    public int movingCount(int m, int n, int k) {
        this.m = m; this.n = n; this.k = k;
        this.visited = new boolean[m][n];
        return dfs(0, 0, 0, 0);
    }
    public int dfs(int i, int j, int si, int sj) {
        if(i < 0 || i >= m || j < 0 || j >= n || k < si + sj || visited[i][j]) return 0;
        visited[i][j] = true;
        return 1 + dfs(i + 1, j, (i + 1) % 10 != 0 ? si + 1 : si - 8, sj) + dfs(i, j + 1, si, (j + 1) % 10 != 0 ? sj + 1 : sj - 8);
    }
}
方法二:广度优先遍历 BFS
  • BFS/DFS:两者目标都是遍历整个矩阵,不同点在于搜索顺序不同。DFS 是朝一个方向走到底,再回退,以此类推;BFS 则是按照“平推”的方式向前搜索。
  • BFS 实现:通常利用队列实现广度优先遍历。
算法解析:
  • 初始化:将机器人初始点(0,0)加入队列queue
  • 迭代终止条件queue为空。代表已遍历完所有可达解。
  • 迭代工作
    1. 单元格出队:将队首单元格的索引、数位和弹出,作为当前搜索单元格。
    2. 判断是否跳过: 若 ① 行列索引越界 或 ② 数位和超出目标值 k 或 ③ 当前元素已访问过 时,执行 continue
    3. 标记当前单元格:将单元格索引(i,j)存入 Setvisited中,代表此单元格已被访问过
    4. 单元格入队:将当前元素的下方、右方单元格的索引、数位和加入queue
  • 返回值:Setvisited的长度len(visited),即可达解的数量。

Java使用了辅助变量 res统计可达解数量;Python直接返回Set的元素数即可。

在这里插入图片描述

复杂度分析:

M,N 分别为矩阵行列大小。

  • 时间复杂度 O(MN): 最差情况下,机器人遍历矩阵所有单元格,此时时间复杂度为 O(MN)。
  • 空间复杂度 O(MN): 最差情况下,Setvisited 内存储矩阵所有单元格的索引,此时时间复杂度为 O(MN)。

Java代码中visited为辅助矩阵,Python中为Set。

# Python BFS实现方式
class Solution:
    def movingCount(self, m: int, n: int, k: int) -> int:
        queue, visited,  = [(0, 0, 0, 0)], set()
        while queue:
            i, j, si, sj = queue.pop(0)
            if not 0 <= i < m or not 0 <= j < n or k < si + sj or (i, j) in visited: continue
            visited.add((i,j))
            queue.append((i + 1, j, si + 1 if (i + 1) % 10 else si - 8, sj))
            queue.append((i, j + 1, si, sj + 1 if (j + 1) % 10 else sj - 8))
        return len(visited)
// java BFS实现方式
class Solution {
    public int movingCount(int m, int n, int k) {
        boolean[][] visited = new boolean[m][n];
        int res = 0;
        Queue<int[]> queue= new LinkedList<int[]>();
        queue.add(new int[] { 0, 0, 0, 0 });
        while(queue.size() > 0) {
            int[] x = queue.poll();
            int i = x[0], j = x[1], si = x[2], sj = x[3];
            if(i < 0 || i >= m || j < 0 || j >= n || k < si + sj || visited[i][j]) continue;
            visited[i][j] = true;
            res ++;
            queue.add(new int[] { i + 1, j, (i + 1) % 10 != 0 ? si + 1 : si - 8, sj });
            queue.add(new int[] { i, j + 1, si, (j + 1) % 10 != 0 ? sj + 1 : sj - 8 });
        }
        return res;
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
### 回答1: 好的,我来用中文回复这个链接:https://leetcode-cn.com/tag/dynamic-programming/ 这个链接是 LeetCode 上关于动态规划的题目集合。动态规划是一种常用的算法思想,可以用来解决很多实际问题,比如最长公共子序列、背包问题、最短路径等等。在 LeetCode 上,动态规划也是一个非常重要的题型,很多题目都需要用到动态规划的思想来解决。 这个链接里包含了很多关于动态规划的题目,按照难度从简单到困难排列。每个题目都有详细的题目描述、输入输出样例、题目解析和代码实现等内容,非常适合想要学习动态规划算法的人来练习和提高自己的能力。 总之,这个链接是一个非常好的学习动态规划算法的资源,建议大家多多利用。 ### 回答2: 动态规划是一种算法思想,通常用于优化具有重叠子问题和最优子结构性质的问题。由于其成熟的数学理论和强大的实用效果,动态规划在计算机科学、数学、经济学、管理学等领域均有重要应用。 在计算机科学领域,动态规划常用于解决最优化问题,如背包问题、图像处理、语音识别、自然语言处理等。同时,在计算机网络和分布式系统中,动态规划也广泛应用于各种优化算法中,如链路优化、路由算法、网络流量控制等。 对于算法领域的程序员而言,动态规划是一种必要的技能和知识点。在LeetCode这样的程序员平台上,题目分类和标签设置十分细致和方便,方便程序员查找并深入学习不同类型的算法。 LeetCode的动态规划标签下的题目涵盖了各种难度级别和场景的问题。从简单的斐波那契数列、迷宫问题到可以用于实际应用的背包问题、最长公共子序列等,难度不断递进且话题丰富,有助于开发人员掌握动态规划的实际应用技能和抽象思维模式。 因此,深入LeetCode动态规划分类下的题目学习和练习,对于程序员的职业发展和技能提升有着重要的意义。 ### 回答3: 动态规划是一种常见的算法思想,它通过将问题拆分成子问题的方式进行求解。在LeetCode中,动态规划标签涵盖了众多经典和优美的算法问题,例如斐波那契数列、矩阵链乘法、背包问题等。 动态规划的核心思想是“记忆化搜索”,即将中间状态保存下来,避免重复计算。通常情况下,我们会使用一张二维表来记录状态转移过程中的中间值,例如动态规划求解斐波那契数列问题时,就可以定义一个二维数组f[i][j],代表第i项斐波那契数列中,第j个元素的值。 在LeetCode中,动态规划标签下有众多难度不同的问题。例如,经典的“爬楼梯”问题,要求我们计算到n级楼梯的方案数。这个问题的解法非常简单,只需要维护一个长度为n的数组,记录到达每一级楼梯的方案数即可。类似的问题还有“零钱兑换”、“乘积最大子数组”、“通配符匹配”等,它们都采用了类似的动态规划思想,通过拆分问题、保存中间状态来求解问题。 需要注意的是,动态规划算法并不是万能的,它虽然可以处理众多经典问题,但在某些场景下并不适用。例如,某些问题的状态转移过程比较复杂,或者状态转移方程中存在多个参数,这些情况下使用动态规划算法可能会变得比较麻烦。此外,动态规划算法也存在一些常见误区,例如错用贪心思想、未考虑边界情况等。 总之,掌握动态规划算法对于LeetCode的学习和解题都非常重要。除了刷题以外,我们还可以通过阅读经典的动态规划书籍,例如《算法竞赛进阶指南》、《算法与数据结构基础》等,来深入理解这种算法思想。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

水花

您的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值