本节内容
62.不同路径※ 建议 :
题目链接: https://leetcode.cn/problems/unique-paths/ 文章讲解: https://programmercarl.com/0062.%E4%B8%8D%E5%90%8C%E8%B7%AF%E5%BE%84.html
题目分析
方案一 1 2 3 4 5 6 7 8 9 10 11 12 13 14 class Solution { public int uniquePaths (int m, int n) { int [][] path = new int [m][n]; for (int i = 0 ; i < m; i++) { for (int j = 0 ; j < n; j++) { if (i == 0 ) path[i][j] = 1 ; else if (j == 0 ) path[i][j] = 1 ; else path[i][j] = path[i - 1 ][j] + path[i][j - 1 ]; } } return path[m -1 ][n - 1 ]; } }
结果 解答成功: 执行耗时:0 ms,击败了100.00% 的Java用户 内存消耗:38.1 MB,击败了80.01% 的Java用户
分析 时间复杂度: O( n * m )
空间复杂度: O( n * m )
代码随想录 https://programmercarl.com/0062.%E4%B8%8D%E5%90%8C%E8%B7%AF%E5%BE%84.html
深搜 这道题目,刚一看最直观的想法就是用图论里的深搜,来枚举出来有多少种路径。
注意题目中说机器人每次只能向下或者向右移动一步,那么其实机器人走过的路径可以抽象为一棵二叉树,而叶子节点就是终点!
如图举例:
此时问题就可以转化为求二叉树叶子节点的个数,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 class Solution {private : int dfs (int i, int j, int m, int n) { if (i > m || j > n) return 0 ; if (i == m && j == n) return 1 ; return dfs (i + 1 , j, m, n) + dfs (i, j + 1 , m, n); }public : int uniquePaths (int m, int n) { return dfs (1 , 1 , m, n); } };
大家如果提交了代码就会发现超时了!
来分析一下时间复杂度,这个深搜的算法,其实就是要遍历整个二叉树。
这棵树的深度其实就是m+n-1(深度按从1开始计算)。
那二叉树的节点个数就是 2^(m + n - 1) - 1。可以理解深搜的算法就是遍历了整个满二叉树(其实没有遍历整个满二叉树,只是近似而已)
所以上面深搜代码的时间复杂度为O(2^(m + n - 1) - 1),可以看出,这是指数级别的时间复杂度,是非常大的。
动态规划 机器人从(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数组的初始化
如何初始化呢,首先dp[i][0]一定都是1,因为从(0, 0)的位置到(i, 0)的路径只有一条,那么dp[0][j]也同理。
所以初始化代码为:
1 2 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数组
如图所示:
以上动规五部曲分析完毕,C++代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 class Solution {public : int uniquePaths (int m, int n) { vector<vector<int >> dp (m, vector <int >(n, 0 )); for (int i = 0 ; i < m; i++) dp[i][0 ] = 1 ; for (int j = 0 ; j < n; j++) dp[0 ][j] = 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)
其实用一个一维数组(也可以理解是滚动数组)就可以了,但是不利于理解,可以优化点空间,建议先理解了二维,在理解一维,C++代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 class Solution {public : int uniquePaths (int m, int n) { vector<int > 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)
数论方法 在这个图中,可以看出一共m,n的话,无论怎么走,走到终点都需要 m + n - 2 步。
在这m + n - 2 步中,一定有 m - 1 步是要向下走的,不用管什么时候向下走。
那么有几种走法呢? 可以转化为,给你m + n - 2个不同的数,随便取m - 1个数,有几种取法。
那么这就是一个组合问题了。
那么答案,如图所示:
求组合的时候,要防止两个int相乘溢出! 所以不能把算式的分子都算出来,分母都算出来再做除法。
例如如下代码是不行的。
1 2 3 4 5 6 7 8 9 10 11 12 class Solution {public : int uniquePaths (int m, int n) { int numerator = 1 , denominator = 1 ; int count = m - 1 ; int t = m + n - 2 ; while (count--) numerator *= (t--); for (int i = 1 ; i <= m - 1 ; i++) denominator *= i; return numerator / denominator; } };
需要在计算分子的时候,不断除以分母,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 class Solution {public : int uniquePaths (int m, int n) { long long numerator = 1 ; int denominator = m - 1 ; int count = m - 1 ; int t = m + n - 2 ; while (count--) { numerator *= (t--); while (denominator != 0 && numerator % denominator == 0 ) { numerator /= denominator; denominator--; } } return numerator; } };
计算组合问题的代码还是有难度的,特别是处理溢出的情况!
代码实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 public static 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※ 建议 :
题目链接: https://leetcode.cn/problems/unique-paths-ii/ 文章讲解: https://programmercarl.com/0063.%E4%B8%8D%E5%90%8C%E8%B7%AF%E5%BE%84II.html
题目分析
方案一 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 class Solution { public int uniquePathsWithObstacles (int [][] obstacleGrid) { if (obstacleGrid[0 ][0 ] == 1 ) return 0 ; obstacleGrid[0 ][0 ] = 1 ; for (int i = 1 ; i < obstacleGrid.length; i++) { if (obstacleGrid[i][0 ] == 0 ) obstacleGrid[i][0 ] = obstacleGrid[i - 1 ][0 ]; else obstacleGrid[i][0 ] = 0 ; } for (int i = 1 ; i < obstacleGrid[0 ].length; i++) { if (obstacleGrid[0 ][i] == 0 ) obstacleGrid[0 ][i] = obstacleGrid[0 ][i - 1 ]; else obstacleGrid[0 ][i] = 0 ; } for (int i = 1 ; i < obstacleGrid.length; i++) { for (int j = 1 ; j < obstacleGrid[0 ].length; j++) { if (obstacleGrid[i][j] == 1 ) obstacleGrid[i][j] = 0 ; else { obstacleGrid[i][j] = obstacleGrid[i - 1 ][j] + obstacleGrid[i][j - 1 ]; } } } return obstacleGrid[obstacleGrid.length - 1 ][obstacleGrid[0 ].length - 1 ]; } }
结果 解答成功: 执行耗时:0 ms,击败了100.00% 的Java用户 内存消耗:39.2 MB,击败了97.68% 的Java用户
分析 时间复杂度: O( n * m )
空间复杂度: O( 1 )
代码随想录 https://programmercarl.com/0063.%E4%B8%8D%E5%90%8C%E8%B7%AF%E5%BE%84II.html
思路 这道题相对于 上一题 就是有了障碍。
第一次接触这种题目的同学可能会有点懵,这有障碍了,应该怎么算呢?
上一题中我们已经详细分析了没有障碍的情况,有障碍的话,其实就是标记对应的dp table(dp数组)保持初始值(0)就可以了。
动规五部曲:
确定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)如果就是障碍的话应该就保持初始状态(初始状态为0)。
所以代码为:
1 2 3 if (obstacleGrid[i][j] == 0 ) { dp[i][j] = dp[i - 1 ][j] + dp[i][j - 1 ]; }
dp数组如何初始化
不同路径下的初始化:
1 2 3 vector<vector<int >> dp (m, vector <int >(n, 0 )); for (int i = 0 ; i < m; i++) dp[i][0 ] = 1 ;for (int j = 0 ; j < n; j++) dp[0 ][j] = 1 ;
因为从(0, 0)的位置到(i, 0)的路径只有一条,所以dp[i][0]一定为1,dp[0][j]也同理。
但如果(i, 0) 这条边有了障碍之后,障碍之后(包括障碍)都是走不到的位置了,所以障碍之后的dp[i][0]应该还是初始值0。
如图:
下标(0, j)的初始化情况同理。
所以本题初始化代码为:
1 2 3 vector<vector<int >> dp (m, vector <int >(n, 0 ));for (int i = 0 ; i < m && obstacleGrid[i][0 ] == 0 ; i++) dp[i][0 ] = 1 ;for (int j = 0 ; j < n && obstacleGrid[0 ][j] == 0 ; 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 2 3 4 5 6 for (int i = 1 ; i < m; i++) { for (int j = 1 ; j < n; j++) { if (obstacleGrid[i][j] == 1 ) continue ; dp[i][j] = dp[i - 1 ][j] + dp[i][j - 1 ]; } }
举例推导dp数组
拿示例1来举例如题:
对应的dp table 如图:
如果这个图看不懂,建议再理解一下递归公式,然后照着文章中说的遍历顺序,自己推导一下!
动规五部分分析完毕,对应C++代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 class Solution {public : int uniquePathsWithObstacles (vector<vector<int >>& obstacleGrid) { int m = obstacleGrid.size (); int n = obstacleGrid[0 ].size (); if (obstacleGrid[m - 1 ][n - 1 ] == 1 || obstacleGrid[0 ][0 ] == 1 ) return 0 ; vector<vector<int >> dp (m, vector <int >(n, 0 )); for (int i = 0 ; i < m && obstacleGrid[i][0 ] == 0 ; i++) dp[i][0 ] = 1 ; for (int j = 0 ; j < n && obstacleGrid[0 ][j] == 0 ; j++) dp[0 ][j] = 1 ; for (int i = 1 ; i < m; i++) { for (int j = 1 ; j < n; j++) { if (obstacleGrid[i][j] == 1 ) continue ; dp[i][j] = dp[i - 1 ][j] + dp[i][j - 1 ]; } } return dp[m - 1 ][n - 1 ]; } };
时间复杂度:O(n × m),n、m 分别为obstacleGrid 长度和宽度
空间复杂度:O(n × m)
同样给出空间优化版本:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 class Solution {public : int uniquePathsWithObstacles (vector<vector<int >>& obstacleGrid) { if (obstacleGrid[0 ][0 ] == 1 ) return 0 ; vector<int > dp (obstacleGrid[0 ].size()) ; for (int j = 0 ; j < dp.size (); ++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.size (); ++i) for (int j = 0 ; j < dp.size (); ++j){ if (obstacleGrid[i][j] == 1 ) dp[j] = 0 ; else if (j != 0 ) dp[j] = dp[j] + dp[j-1 ]; } return dp.back (); } };
时间复杂度:O(n × m),n、m 分别为obstacleGrid 长度和宽度
空间复杂度:O(m)
代码实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 class Solution { public int uniquePathsWithObstacles (int [][] obstacleGrid) { int m = obstacleGrid.length; int n = obstacleGrid[0 ].length; int [][] dp = new int [m][n]; 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 ; } for (int j = 0 ; j < n && obstacleGrid[0 ][j] == 0 ; j++) { dp[0 ][j] = 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 ]; } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 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 ]; } }