Day 34 动态规划 Part02
动态规划解题步骤
- 确定dp数组(dp table)以及下标的含义
- 确定递推公式
- dp数组如何初始化
- 确定遍历顺序
- 举例推导dp数组
[62. 不同路径](https://leetcode.cn/problems/fibonacci-number/)
动态规划的解法还是很好理解的。按照解题步骤来。
- 确定dp数组及其下标的含义。
dp[i][j]
代表到达[i, j]
位置处的路径数 - 确定递推公式。这里最重要的点就是
[i,j]
点只能从其上或右侧的点到达。因此dp[i][j] = dp[i-1][j] + dp[i][j-1]
。 dp
初值的确定。显然,第一行和第一列的所有节点都只有1种到达方式。- 按从上到下从左到右去遍历即可。
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];
}
}
class Solution { //滚动数组优化时间复杂度
public int uniquePaths(int m, int n) {
int[] dp = new int[n];
Arrays.fill(dp, 1);
for(int i = 1; i < m; i++){
for(int j = 1; j < n; j++){
dp[j] += dp[j-1];
}
}
return dp[n-1];
}
}
这道题还有数学的解法,其实我最开始也是这么想的,但可能是排列组合忘太久了,想到的思路并不正确。只能看到题解后,哦哦哦,原来是这样。其实就是组合数,从起点走到终点一共有m+n-2
步,其中一定有 m-1
步向下走(等价于一定有 n-1
步向下走),因此就是组合数$C_{m+n-2}^{m-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;
}
}
63. 不同路径 II
这道题就不能使用数学的方式了。只能使用 dp
的方法来做。基本上与上一题是一致的,但是需要处理遇到石头时的情况。这道题还可以使用 滚动数组
优化,第一种方式熟练之后,很容易写出第二种。
class Solution {
public int uniquePathsWithObstacles(int[][] obstacleGrid) {
int m = obstacleGrid.length;
int n = obstacleGrid[0].length;
int[][] dp = new int[m][n];
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];
}
}
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] != 1; 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];
}
}
343. 整数拆分
这道题的思路真的太牛逼,照着这个思路谁都能写出来,但这个思路想不到就是寄。对于一个数字,要么分成两个数字的乘积,要么分成多个数字的乘积,最大值一定是这两种情况之一,因此考虑用动态规划。但是怎么表示多个数字的乘积呢。这里我就直接搬来题解中的 dp
数组的定义了。
dp
数组的定义:dp[i]
代表数字i
使得乘积最大的划分方式得到的乘积。- 递推公式:对于一个数字
n
,显然他能拆分成(1 * n-1), (2 * n-2), (3 * n-3) ...
,这是对应于拆分成两个数字的乘积的部分,怎么表示其拆分成多个数字的乘积呢。这里就是最重要的部分了,数字n的拆分成多个数字乘积可以这样表示(1 * dp[n-1]), (2 * dp[n-2]), (3, dp[n-3])...
class Solution {
public int integerBreak(int n) {
int[] dp = new int[n+1];
dp[2] = 1;
for(int i = 3; i <= n; i++)
for(int j = 1; j <= i/2; j++)
dp[i] = Math.max(dp[i], Math.max(j * (i-j), j * dp[i-j])); //别忘了还要和dp[i]自己比较,留存最优结果
return dp[n];
}
}
96. 不同的二叉搜索树
和上一题的思路基本一致,只有在递推关系式部分有所不同。
dp
数组的含义:dp[i]
代表了i个节点的二叉搜索树的个数。- 递推公式:对于包含了n个节点的搜索树,显然,根节点一定是必不可少的,其余节点分配在两个子树上。以左子树为准,可以包含
0 ~ n-1
个节点,对应的右子树就包含了n-1 ~ 0
个节点,所以dp[左子树节点个数] * dp[右子树节点个数]
的累加和就是总的数量。
class Solution {
public int numTrees(int n) {
int[] dp = new int[n+1];
dp[0] = 1; dp[1] = 1;
for(int i = 2; i <= n; i++){
for(int j = 0; j < i; j++){ //j代表左子树上节点个数
dp[i] += dp[j] * dp[i-1-j];
}
}
return dp[n];
}
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步