(动态规划、回溯)62. 不同路径

该博客讨论了一个机器人在网格中从左上角移动到右下角的路径计数问题。通过组合数学的方法,计算在给定的步数中选择向下移动次数的方案数,实现了求解不同路径的数量。示例展示了不同尺寸网格的路径总数,并提供了相应的代码实现,代码中用长整型变量进行乘法运算以避免溢出。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

62. 不同路径

题目描述

一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为 “Start” )。

机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish” )。

问总共有多少条不同的路径?

示例 1:

在这里插入图片描述
输入:m = 3, n = 7
输出:28

示例 2:

输入:m = 3, n = 2
输出:3
解释:从左上角开始,总共有 3 条路径可以到达右下角。

  1. 向右 -> 向下 -> 向下
  2. 向下 -> 向下 -> 向右
  3. 向下 -> 向右 -> 向下

示例 3:

输入:m = 7, n = 3
输出:28

示例 4:

输入:m = 3, n = 3
输出:6

思路

网上有很多算法,常见的可能就两种

一种是数学公式(排列组合),如下:

  1. 从左上角到右下角的过程中,一共需要移动 m+n-2 次
    1. m-1次向下移动
    2. n-1次向右移动
  2. 路径的总数,等于从 m+n-2 次移动中选择 m-1 次向下移动的方案
    在这里插入图片描述

另外一种便是使用动态规划解决,思路如下:

  • 首先,定义状态dp[i][j]表示,到达 i, j 最多路径
  • 构建状态转移方程:dp[i][j] = dp[i-1][j] + dp[i][j-1]

    到达 i,j 相当于计算:到达上一步的位置的两种情况路径总和
    后退一步包含两种情况(通过题目意思可知):向左后退一步(i-1),向上后退一步(j-1)
    从坐标 (0,0) 到坐标 (i,j) 的路径总数的值只取决于 从坐标 (0,0) 到坐标 (i−1,j) 的路径总数 和 从坐标 (0,0) 到坐标 (i,j−1) 的路径总数,即 f(i,j) 只能通过 f(i−1,j) 和 f(i,j−1) 转移得到

  • 构建初始边界条件:对于第一行 dp[0][j],或者第一列 dp[i][0],由于都是在边界,所以只能为 1

    看题,要求如下,所以只能是1

    1. 机器人每次只能向下或者向右移动一步
    2. 求总共有多少条不同的路径
  • 最后,确定计算顺序,自底向上(从最小子问题逐步推导,最终得到全局解)

    从1这个下标开始依次计算,最后返回要求值dp[m-1][n-1]

代码

数学排列组合

class Solution {
    public int uniquePaths(int m, int n) {
        long ans = 1;
        //进行组合数运算
        for(int x=n, y=1; y<m; x++, y++){
            ans = ans * x / y;
        }
        return (int)ans;
    }
}

动态规划

class Solution {
    public int uniquePaths(int m, int n) {
        int[][] dp = new int[m][n];
        for (int i = 0; i < m; ++i) {
            dp[i][0] = 1;
        }
        for (int i = 0; i < n; ++i) {
            dp[0][i] = 1;
        }
        for (int i = 1; i < m; ++i) {
            for (int j = 1; j < n; ++j) {
                dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
            }
        }
        return dp[m - 1][n - 1];
    }
}

63. 不同路径 II

题目描述

给定一个 m x n 的整数数组 grid。一个机器人初始位于 左上角(即 grid[0][0])。机器人尝试移动到 右下角(即 grid[m - 1][n - 1])。机器人每次只能向下或者向右移动一步。

网格中的障碍物和空位置分别用 1 和 0 来表示。机器人的移动路径中不能包含 任何 有障碍物的方格。

返回机器人能够到达右下角的不同路径数量。

测试用例保证答案小于等于 2 * 109

示例 1:

在这里插入图片描述
输入:obstacleGrid = [[0,0,0],[0,1,0],[0,0,0]]
输出:2
解释:3x3 网格的正中间有一个障碍物。
从左上角到右下角一共有 2 条不同的路径:

  1. 向右 -> 向右 -> 向下 -> 向下
  2. 向下 -> 向下 -> 向右 -> 向右

示例 2:

在这里插入图片描述
输入:obstacleGrid = [[0,1],[0,0]]
输出:1

提示:

m == obstacleGrid.length
n == obstacleGrid[i].length
1 <= m, n <= 100
obstacleGrid[i][j] 为 0 或 1

思路

还是使用动态规划

  • 首先,定义状态dp[i][j]表示,到达 i, j 最多路径
  • 其次,构建状态转移方程,有两种情况:
    • 不是障碍物:dp[i][j] = dp[i-1][j] + dp[i][j-1]

      和上面的62一样的原因

    • 是障碍物(当前坐标不可达):0
    • 空间优化:f(i,j) 只与 f(i−1,j) 和 f(i,j−1) 相关,所以可以使用滑动数组的概念,我们只需要使用一维数组保存数量即可

      要求值,因为我们每次只需要 dp[i-1][j],dp[i][j-1],所以我们只要记录这两个数,而因为边界只是会在第一次运算时,被使用到
      比如:在计算 dp[1][1] 时,会用到 dp[0][1],但是之后就不再用到了。所以可以把 dp[1][1] 记到 dp[0][1] 中,这样对于 dp[1][2](dp[0][2]+dp[1][1]) 来说,它需要的数据就在 dp[0][1] 和 dp[0][2] 中。dp[1][2] 算完后也可以同样记到 dp[0][2] 中。dp[2][2]=dp[1][2]+dp[2][1]=dp[0][2]+dp[1][1]+dp[2][0]=dp[0][2]+dp[0][1]+dp[2][0]
      ‌解释‌:因为是到末尾,所以变为一维数组后

      • dp[j] 在更新前存储的是 dp[i-1][j](上一行的值)。
      • dp[j-1] 存储的是 dp[i][j-1](当前行已计算的值)。
      • 更新 dp[j] 时,相当于 dp[i][j] = dp[i-1][j] + dp[i][j-1]。
  • 之后,构建初始边界条件:第一行和第一列值的计算,不是障碍物坐标值就为1,如果有障碍物,后面的是不可达的
  • 最后,选择自底向上迭代获取每一个坐标

这个方法,可以直接使用上面没有障碍物的情况的步骤一致,只需要有两点需要补充

  • 初始化条件需要多一个:dp[0][0] 这个开始点就是障碍
  • 在进行计算dp的时候,还需要去判断当前坐标值必须为0才可以

代码

使用二维数组存储

class Solution {
    public int uniquePathsWithObstacles(int[][] obstacleGrid) {
        int xc=obstacleGrid[0].length, yr=obstacleGrid.length;
        int[][] dp = new int[yr][xc];
        for(int i=0;i<xc;++i){
            if(obstacleGrid[0][i]==1) break;
            dp[0][i]=1;
        }
        for(int i=0;i<yr;++i){
            if(obstacleGrid[i][0]==1) break;
            dp[i][0]=1;
        }
        dp[0][0]=obstacleGrid[0][0]==1?0 : 1;
        for(int i=1;i<yr;++i){
            for(int j=1;j<xc;++j){
                if(obstacleGrid[i][j]==0){
                    dp[i][j]=dp[i-1][j]+dp[i][j-1];
                }
            }
        }
        return dp[yr-1][xc-1];

    }
}

优化,使用一维数组存储

class Solution {
    public int uniquePathsWithObstacles(int[][] obstacleGrid) {
        int xc = obstacleGrid[0].length, yr = obstacleGrid.length;
        int[] dp = new int[xc];
        dp[0] = obstacleGrid[0][0] == 1 ? 0 : 1;
        for (int i = 0; i < yr; ++i) {
            for (int j = 0; j < xc; ++j) {
                if (obstacleGrid[i][j] == 1) {
                    dp[j] = 0;
                    continue;
                }
                if (j - 1 >= 0 && obstacleGrid[i][j - 1] == 0) {
                    dp[j] += dp[j - 1];
                }
            }
        }
        return dp[xc - 1];

    }
}

63. 不同路径 III(回溯)

题目描述

在二维网格 grid 上,有 4 种类型的方格:

1 表示起始方格。且只有一个起始方格。
2 表示结束方格,且只有一个结束方格。
0 表示我们可以走过的空方格。
-1 表示我们无法跨越的障碍。
返回在四个方向(上、下、左、右)上行走时,从起始方格到结束方格的不同路径的数目。

每一个无障碍方格都要通过一次,但是一条路径中不能重复通过同一个方格。

示例 1:

输入:[[1,0,0,0],[0,0,0,0],[0,0,2,-1]]
输出:2
解释:我们有以下两条路径:

  1. (0,0),(0,1),(0,2),(0,3),(1,3),(1,2),(1,1),(1,0),(2,0),(2,1),(2,2)
  2. (0,0),(1,0),(2,0),(2,1),(1,1),(0,1),(0,2),(0,3),(1,3),(1,2),(2,2)

示例 2:

输入:[[1,0,0,0],[0,0,0,0],[0,0,0,2]]
输出:4
解释:我们有以下四条路径:

  1. (0,0),(0,1),(0,2),(0,3),(1,3),(1,2),(1,1),(1,0),(2,0),(2,1),(2,2),(2,3)
  2. (0,0),(0,1),(1,1),(1,0),(2,0),(2,1),(2,2),(1,2),(0,2),(0,3),(1,3),(2,3)
  3. (0,0),(1,0),(2,0),(2,1),(2,2),(1,2),(1,1),(0,1),(0,2),(0,3),(1,3),(2,3)
  4. (0,0),(1,0),(2,0),(2,1),(1,1),(0,1),(0,2),(0,3),(1,3),(1,2),(2,2),(2,3)

示例 3:

输入:[[0,1],[2,0]]
输出:0
解释:
没有一条路能完全穿过每一个空的方格一次。
请注意,起始和结束方格可以位于网格中的任意位置。

提示:1 <= grid.length * grid[0].length <= 20

思路

首先,选择路径‌:定义函数 dfs,表示当前 grid 状态下,从点 (x,y) 出发,还要经过 n 个点,走到终点的路径条数

每一个无障碍方格都要通过一次,所以必须要经过所有的点

其次,递归探索‌

  • 如果当前的点不为终点,则将当前的点标记为 −1,表示这条路径以后不能再经过这个点,
  • 然后继续在这个点往四个方向扩展,
  • 如果不超过边界且下一个点的值为 0 或者 2,则表示这条路径可以继续扩展

然后,确定终止条件‌:

  • 到达一个点时,如果当前的点为终点,且已经经过了 (n+1) 个点,那么就构成了一条合格的路径,否则就不构成

    如果 grid[x][y]=2,说明到达终点。如果此时 n=0 则返回 1 表示找到了一条合法路径;否则返回 0 表示不合法的路径。

  • 如果当前的点不为终点,则将当前的点标记为 −1,表示这条路径以后不能再经过这个点,

    如果出界,或者 grid[x][y]=−1,则返回 0,表示不合法的路径。

最后,回退与恢复‌:探测完四个方向后,需要将当前的点的值改为原来的值

代码实现时,可以把 grid[x][y] 改成 −1,表示这个格子访问过,在返回前改回 0(恢复现场)。

代码

class Solution {
    public int uniquePathsIII(int[][] grid) {
        int n = 0, x = -1, y = -1;
        for (int i = 0; i < grid.length; ++i) {
            for (int j = 0; j < grid[i].length; ++j) {
                if (grid[i][j] == 0) {
                    // 每一个无障碍方格都要通过一次,计算所有无障碍方格
                    ++n;
                }
                if (grid[i][j] == 1) {
                    // 计算起始点
                    x = i;
                    y = j;
                }
            }
        }
        // n+1 算上起点,也是必须要经过的点
        return dfs(grid, x, y, n + 1);
    }

    private int dfs(int[][] grid, int x, int y, int n) {
        if (x < 0 || x >= grid.length || y < 0 || y >= grid[x].length || grid[x][y] < 0) {
            // 不合法数据
            return 0;
        }
        if (grid[x][y] == 2) {
            // 到终点,需要判断是否所有结点都被经过,有没有经过的返回0
            return n == 0 ? 1 : 0;
        }
        // 将当前结点设置为已被使用,后续路径便不能再使用此坐标
        grid[x][y] = -1;
        // 上、下、左、右移动 并都 n-1,计算所有和
        int ans = dfs(grid, x - 1, y, n - 1) + dfs(grid, x, y - 1, n - 1) +
                dfs(grid, x + 1, y, n - 1) + dfs(grid, x, y + 1, n - 1);
        // 恢复(回溯)
        grid[x][y] = 0;
        return ans;
    }

}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

?abc!

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

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

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

打赏作者

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

抵扣说明:

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

余额充值