1. 62.不同路径
1.1 思路
机器人从(0 , 0) 位置出发,到*(m - 1, n - 1)*终点。
按照动规五部曲来分析:
-
确定dp数组以及下标的含义
dp[i] [j] :从(0 ,0)出发,到(i, j) 有dp[i] [j]条不同的路径;ps: 数组不是
dp[m+1][n+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数组的初始化
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数组
1.2 代码实现
class Solution {
public int uniquePaths(int m, int n) {
//确定dp含义 dp[i][j]是移动到[i][j]位置有几种路径
//因为dp[i][0]和dp[0][j]算是数组中的了,所以设置的数组长度是[m][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;
}
//遍历顺序:从上往下,从左往右
//0行和0列已经遍历了,所以从1行和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),网格的大小为m x n ;遍历需要从左到右,从上到下,把所有格子都遍历一次,每次操作一个加法运算;
- 空间复杂度:O(m*n),dp数组是一个二维数组,大小和网格一样也为m x n;
2. 63.不同路径II
2.1 思路
本题相对于62 不同路径 就是多了障碍物。有障碍的话,标记对应的dp数组保持初始值0。
动态五部曲
-
定义dp数组以及下标含义
dp[i][j]
:表示从(0 ,0)出发,到(i, j) 有dp[i][j]
条不同的路径 -
确定递推公式
递推公式和 62.不同路径 一样,
dp[i][j] = dp[i - 1][j] + dp[i][j - 1],
但需要注意一点:因为有了障碍,(i, j)如果就是障碍的话应该就保持初始状态(初始状态为0)
if (obstacleGrid[i][j] == 0) { // 当(i, j)没有障碍的时候,再推导dp[i][j] dp[i][j] = dp[i - 1][j] + dp[i][j - 1]; }
-
初始化数组
在 62.不同路径 我们把第0行和第0列全部初始化为1;因为从(0, 0)的位置到(i, 0)的路径只有一条,所以
dp[i][0]
一定为1,dp[0][j]
也同理。需要注意的是:
-
在本题中,如果(i, 0) 这条边有了障碍之后,障碍之后(包括障碍)都是走不到的位置了,所以障碍之后的
dp[i][0]
应该还是初始值0;下标(0, j)的初始化情况同理; -
代码里for循环的终止条件,一旦遇到
obstacleGrid[i][0] == 1
的情况就停止dp[i][0]
的赋值1的操作,dp[0][j]
同理;
-
-
确定遍历顺序
从上到下,从左到右
-
举例倒推dp数组
2.2 代码实现
class Solution {
public int uniquePathsWithObstacles(int[][] obstacleGrid) {
int m = obstacleGrid.length;//网格长
int n = obstacleGrid[0].length;//网格宽度
//定义数组
int[][] dp = new int[m][n];//dp表示走到(i,j)位置共有的路径
//起点或终点出现障碍,返回0;
if (obstacleGrid[m - 1][n - 1] == 1 || obstacleGrid[0][0] == 1) {
return 0;
}
//初始化数组
//初始化行
for (int i = 0; i < m && obstacleGrid[i][0] == 0; i++) {
dp[i][0] = 1;//注意dp含义:是到(i,0)位置共有多少条路径
}
//初始化列
for (int i = 0; i < n && obstacleGrid[0][i]==0; i++) {
dp[0][i] = 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(m*n)
网格的大小为m x n ;for循环需要从左到右,从上到下,把所有格子都遍历一次,每次操作一个加法运算;
-
空间复杂度:O(m*n)
dp数组是一个二维数组,大小和网格一样也为m x n;
2.3 方法优化 ——一维数组
class Solution {
public int uniquePathsWithObstacles(int[][] obstacleGrid) {
int m = obstacleGrid.length;
int n = obstacleGrid[0].length;
int[] dp = new int[n];
//若有障碍物,则停止遍历,后面都是空,因为有障碍物的话,后面就到不了,直接停止遍历
for (int j = 0; j < n && obstacleGrid[0][j] == 0; j++) {
dp[j] = 1;
}
for (int i = 1; i < m; i++) {
for (int j = 0; j < n; j++) {
if (obstacleGrid[i][j] == 1) {
dp[j] = 0;
} else if (j != 0) {
dp[j] += dp[j - 1];//一个位置的路径数等于从上方和左侧到达该位置的不同路径数之和。
}
}
}
return dp[n - 1];
}
}
-
时间复杂度:O(m*n)
-
空间复杂度:O(n)。因为只用开辟了一个一维 dp 数组,这种滚动数组的优化方式常用于解决动态规划中的空间问题
这段代码主要的思路与之前的代码实现略微不同,不需要使用二维的 dp 数组来计算中间结果,而是采用==滚动数组==的方式来优化空间。
具体来说:
-
代码定义一个一维的 dp 数组
- 其中
dp[j]
表示到达(i,j)的不同路径数目 dp[j-1]
表示当前位置左侧的位置上的路径数dp[j]
表示当前位置上方位置的路径数
因此递推公式是:
dp[j] += dp[j - 1]
- 其中
-
对于第一行和第一列的位置,和之前的代码实现一样,到达这些位置的路径数只有一种
-
在循环处理非首行和非首列的位置时
- 若当前位置为障碍,则该位置的路径数必定为 0,
- 否则,该位置的路径数=它左侧位置的路径数+上方位置的路径数之和。
-
最后,返回 dp 数组的最后一项即可得到从起始节点到终止节点的路径数。
2.4 总结及思考
- 在动态规划问题中,可以采用滚动数组的优化方式于解决动态规划中的空间问题。
- 遍历总是从数组初始化的下一行,下一列开始的,所以i,j一般都是1开始
- 此题跟62.不同路径相似,但也有很多细节,比如初始化的部分,很容易忽略了障碍之后应该都是0的情况