62 不同路径
思路
机器人从(0 , 0) 位置出发,到(m - 1, n - 1)终点。
按照动规五部曲来分析:
- 定义dp数组(dp table)
dp[i][j] :表示从(0 ,0)出发,到(i, j) 有dp[i][j]条不同的路径。
- 确定递推公式
想要求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的定义!_)
- 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;
- 确定遍历顺序
这里要看一下递推公式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]一定是有数值的。
- 举例推导dp数组
如图所示:
以上动规五部曲分析完毕,代码如下:
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)
空间优化
其实用一个一维数组(也可以理解是滚动数组)就可以了,如下图:
一维数组本质就是利用本位的上一层数和左边一层数相加,类似斜相加
一维数组一定程度可以优化点空间,代码如下:
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.不同路径 就是有了障碍。
思路
动规五部曲:
- 确定dp数组(dp table)以及下标的含义
dp[i][j] :表示从(0 ,0)出发,到(i, j) 有dp[i][j]条不同的路径。
- 确定递推公式
递推公式和 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];
}
- 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。
如图:
下标(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]同理
- 确定遍历顺序
从递归公式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;
}
- 举例推导dp数组
拿示例1来举例如题:
对应的dp table 如图:
根据测试结果,对比是否于预期推导一致
动规五部分分析完毕,对应代码如下:
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的情况。
学习资料: