羊羊刷题笔记Day39/60 | 第九章 动态规划P2 | 62.不同路径、63. 不同路径 II

116 阅读3分钟

62 不同路径

思路

机器人从(0 , 0) 位置出发,到(m - 1, n - 1)终点。
按照动规五部曲来分析:

  1. 定义dp数组(dp table)

dp[i][j] :表示从(0 ,0)出发,到(i, j) 有dp[i][j]条不同的路径。

  1. 确定递推公式

想要求dp[i][j],只能有两个方向来推导出来,即dp[i - 1][j] (左边)和 dp[i][j - 1](上边)。
此时在回顾一下定义, dp[i - 1][j] 表示啥,是从(0, 0)的位置到(i - 1, j)有几条路径,dp[i][j - 1]同理。
那么很自然,dp[i][j] = dp[i - 1][j] + dp[i][j - 1],因为dp[i][j]只有这两个方向过来。
同样不要以为是dp[i][j] = dp[i - 1][j] + dp[i][j - 1] ~~+ 1~~_ 要时刻明确dp的定义!_)

  1. dp数组的初始化

如何初始化呢,首先dp[i][0]一定都是1,因为从(0, 0)的位置到(i, 0)的路径只有一条,那么dp[0][j]也同理。
所以初始化代码为:

for (int i = 0; i < m; i++) dp[i][0] = 1;
for (int j = 0; j < n; j++) dp[0][j] = 1;
  1. 确定遍历顺序

这里要看一下递推公式dp[i][j] = dp[i - 1][j] + dp[i][j - 1],dp[i][j]都是从其上方和左方推导而来,那么从左到右一层一层遍历就可以了。
这样就可以保证推导dp[i][j]的时候,dp[i - 1][j] 和 dp[i][j - 1]一定是有数值的。

  1. 举例推导dp数组

如图所示:
image.png
以上动规五部曲分析完毕,代码如下:

public int uniquePaths(int m, int n) {
    int[][] dp = new int[m][n];

    // 初始化 - 第一行 第一列都是1
    // 第一列初始化
    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];


}
  • 时间复杂度:O(m × n)
  • 空间复杂度:O(m × n)

空间优化

其实用一个一维数组(也可以理解是滚动数组)就可以了,如下图:
一维数组本质就是利用本位的上一层数和左边一层数相加,类似斜相加
Screenshot_20230805_174344_com.newskyer.draw.png

一维数组一定程度可以优化点空间,代码如下:

class Solution {
public int uniquePaths(int m, int n) {
        int[] dp = new dp[n];
        // 初始化
    	for (int i = 0; i < n; i++) dp[i] = 1;
        // 递推
        for (int j = 1; j < m; j++) {
            for (int i = 1; i < n; i++) {
                dp[i] += dp[i - 1];
            }
        }
        return dp[n - 1];
    }
};
  • 时间复杂度:O(m × n)
  • 空间复杂度:O(n)

63 不同路径Ⅱ

这道题相对于 62.不同路径 就是有了障碍。

思路

动规五部曲:

  1. 确定dp数组(dp table)以及下标的含义

dp[i][j] :表示从(0 ,0)出发,到(i, j) 有dp[i][j]条不同的路径。

  1. 确定递推公式

递推公式和 62.不同路径 一样,dp[i][j] = dp[i - 1][j] + dp[i][j - 1]。
但这里需要注意一点,因为有了障碍,(i, j)如果就是障碍的话那么到(i,j)无路可达,按照定义则设为0
所以代码为:

if (obstacleGrid[i][j] != 0) { // 当(i, j)没有障碍的时候,再推导dp[i][j]
    dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
}
  1. dp数组如何初始化

一般的没有障碍,初始化如下:

for (int i = 0; i < m; i++) dp[i][0] = 1;
for (int j = 0; j < n; j++) dp[0][j] = 1;

但如果(i, 0) 这条边有了障碍之后,障碍之后(包括障碍)都是走不到的位置了,所以障碍之后的dp[i][0]应该还是初始值0。
如图:
image.png
下标(0, j)的初始化情况同理。
所以本题初始化代码为:

// 初始化 - 有障碍的自此之后都是0
// 行 - m - [m][...]
for (int i = 0;i < m && obstacleGrid[i][0] != 1;i++)
    dp[i][0] = 1;

// 列 - n - [...][n]
for (int j = 0;j < n && obstacleGrid[0][j] != 1;j++)
    dp[0][j] = 1;

注意代码里for循环的终止条件,一旦遇到obstacleGrid[i][0] == 1的情况就停止dp[i][0]的赋值1的操作,dp[0][j]同理

  1. 确定遍历顺序

从递归公式dp[i][j] = dp[i - 1][j] + dp[i][j - 1] 中可以看出,一定是从左到右然后一层一层遍历,这样保证推导dp[i][j]的时候,dp[i - 1][j] 和 dp[i][j - 1]一定是有数值。
代码如下:

// 递推公式 - [1][1]开始
for (int i = 1;i < m; i++)
    for (int j = 1;j < n;j++) {
        // 赋值同时判断是否有障碍 - 有障碍的赋值为零 - 也不影响受障碍影响的格子
        dp[i][j] = (obstacleGrid[i][j] == 0)? dp[i - 1][j] + dp[i][j - 1] : 0;
    }
  1. 举例推导dp数组

拿示例1来举例如题:
image.png
对应的dp table 如图:
image.png
根据测试结果,对比是否于预期推导一致
动规五部分分析完毕,对应代码如下:

public int uniquePathsWithObstacles(int[][] obstacleGrid) {
    int m = obstacleGrid.length; // 行
    int n = obstacleGrid[0].length; // 列
    int[][] dp = new int[m][n];

    // 障碍在起点和终点
    if (obstacleGrid[0][0] == 1 || obstacleGrid[m - 1][n - 1] == 1){
        return 0;
    }

    // 1. 初始化 - 有障碍的自此之后都是0
    // 行 - m - [m][...]
    for (int i = 0;i < m && obstacleGrid[i][0] != 1;i++)
        dp[i][0] = 1;

    // 列 - n - [...][n]
    for (int j = 0;j < n && obstacleGrid[0][j] != 1;j++)
        dp[0][j] = 1;

    // 2. 递推公式 - [1][1]开始
    for (int i = 1;i < m; i++)
        for (int j = 1;j < n;j++) {
            // 赋值同时判断是否有障碍 - 有障碍的赋值为零 - 也不影响受障碍影响的格子
            dp[i][j] = (obstacleGrid[i][j] == 0)? dp[i - 1][j] + dp[i][j - 1] : 0;
        }
    return dp[m - 1][n - 1];
}
  • 时间复杂度:O(n × m),n、m 分别为obstacleGrid 长度和宽度
  • 空间复杂度:O(n × m)

空间优化

同样和上一题思路一样,只不过遇到障碍就赋值为 0
代码如下

class Solution {
public:
    int uniquePathsWithObstacles(int[][] obstacleGrid) {
        if (obstacleGrid[0][0] == 1)
            return 0;
        int[] dp = new int[obstacleGrid[0].length];
        for (int j = 0; j < dp.length; j++)
            if (obstacleGrid[0][j] == 1)
                dp[j] = 0;
            else if (j == 0)
                dp[j] = 1;
            else
                dp[j] = dp[j-1];

        for (int i = 1; i < obstacleGrid.length; i++)
            for (int j = 0; j < dp.length; j++){
                dp[j] = (obstacleGrid[i][j] == 1) ? 0 : dp[j] + dp[j - 1]
            }
        return dp.back();
    }
};
  • 时间复杂度:O(n × m),n、m 分别为obstacleGrid 长度和宽度
  • 空间复杂度:O(m)

总结

本题是 62.不同路径 的障碍版,整体思路大体一致。
其实只要考虑到,遇到障碍dp[i][j]保持0就可以了。
也有一些小细节,例如:初始化的部分,很容易忽略了障碍之后应该都是0的情况。

学习资料:

62.不同路径

63. 不同路径 II