动态规划基础知识
(参考卡哥代码随想录讲解)
什么是动态规划
如果某一问题有很多重叠子问题,使用动态规划是最有效的
所以动态规划中每一个状态一定是由上一个状态推导出来的(这一点就区分于贪心),贪心没有状态推导,而是从局部直接选最优的
动态规划的解题步骤
状态转移公式(递推公式)很重要,但动态规划不仅仅只有递推公式
对于动态规划问题,可以拆解为如下五部曲:
-
确定dp数组(dp table)以及下表的含义
-
确定递推公式
-
dp数组如何初始化
-
确定遍历顺序
-
举例推导dp数组
为什么要先确定递推公式,然后再考虑初始化呢
因为一些情况是递推公式决定了dp数组要如何初始化
后面代码的讲解都会围绕这五点来进行讲解
一. 斐波那契数
链接: 509.斐波那契数列
思路
不用写
代码
//递归
class Solution {
public int fib(int n) {
if (n == 0 || n == 1) return n;
return fib(n - 1) + fib(n - 2);
}
}
//用遍历模拟递归
class Solution {
public int fib(int n) {
if (n == 0 || n == 1) return n;
int[] dp = new int[n + 1];
dp[0] = 0;
dp[1] = 1;
for (int index = 2;index <= n;index++){
dp[index] = dp[index - 1] + dp[index - 2];
}
return dp[n];
}
}
二、爬楼梯
链接: 70.爬楼梯
思路
思考➕找规律
1阶 1种
2阶 2种
3阶 3种
4阶 5种
5阶 8种
不考虑范围
PS:每次只能走一阶或两阶,逆向思考,第n阶前可能是n - 1阶,也可能是n - 2阶;所以这两种方法相加得到总共方法数
虽然题目简单,但也要明确dp数组的含义,脑海中将五部曲走一遍
dp[i] = dp[i - 1] + dp[i - 2];
代码
class Solution {
public int climbStairs(int n) {
int[] dp = new int[n + 1];
dp[0] = 1;
dp[1] = 1;
for (int i = 2;i <= n;i++){
dp[i] = dp[i - 1] + dp[i - 2];
}
return dp[n];
}
}
三、使用最小花费爬楼梯
链接: 746.使用最小花费爬楼梯
思路
还是思考 ➕ 找规律
1.明确dp[i]的含义 到达第i个台阶时消耗的最小花费
2.递推公式
初始位置在0 or 1 的位置,可以每次上一个台阶或两个台阶
自上而下看,我们需要dp[i] 可以从i - 1上一个台阶,也可以从i - 2上两个台阶,需要的代价不同,故:
dp[i] = Math.min(dp[i - 1] + cost[i - 1] , dp[i - 2] + cost[i - 2]);
3.初始化
由递推公式可以看出,我们需要dp[0],dp[1]
4.确定遍历顺序
本题从前向后
5.举例验证
代码
class Solution {
public int minCostClimbingStairs(int[] cost) {
int[] dp = new int[cost.length + 1];
dp[0] = 0;
dp[1] = 0;
for (int i = 2;i <= cost.length;i++){
dp[i] = Math.min(dp[i - 1] + cost[i - 1] , dp[i - 2] + cost[i - 2]);
}
return dp[cost.length];
}
}
四、不同路径
链接: 62.不同路径
思路
本题思路挺简单的,是凭借自己能力做出来的哦
按照动态规划五部曲:
1.含义
本题因为是在平面上,所以我们创建二维数组
其实dp【i】【j】表示机器人到达第i行第j列时,总共有多少条路径
2.递推公式
从题意我们可知,机器人每次只能向下或者向右移动一步,在当前位置可由前一位置向下和向右
dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
3.初始化
我一开始想的是为了求m,n位置,所以二维数组大小为m + 1,n + 1
机器人一开始的位置在1,1
所以只用求dp在1,1;2,1;1,2;的方法即可
在举例推导时发现不行,i 和 j下标为1的都应该初始化为1
【所以说举例推导很重要!】
4.遍历顺序,从左往右
5.举例推导
代码
class Solution {
public int uniquePaths(int m, int n) {
int[][] dp = new int[m + 1][n + 1];
for (int i = 1;i < m + 1;i++){
dp[i][1] = 1;
}
for (int i = 1;i < n + 1;i++){
dp[1][i] = 1;
}
for (int i = 2;i < m + 1;i++){
for (int j = 2;j < n + 1;j++){
dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
}
}
return dp[m][n];
}
}
五、不同路径II
链接: 63.不同路径II
思路
代码算法自己是一点弯儿都不能绕啊
有障碍的话就不走这条路呗
对于初始化时,遇到障碍,之后的路也不能走了,只能是0,不需要再赋值为1
代码
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 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++){
if (obstacleGrid[i][j] == 0)
dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
}
}
return dp[m - 1][n - 1];
}
}
六、整数拆分
链接: 343.整数拆分
思路
本题不是那种一眼看出解法的问题
如果按照五部曲,其实是可以做出来的
1.含义
一般这个含义与题目有着直接的关系
dp[i] 在本题中指 i 被拆分后的最大乘积
2.递推公式
直接关系
dp[i] = j * dp[i - j]
本题求最大乘积
dp[i] = Math.max(dp[i],j * (i - j),j * dp[i - j]);
3.初始化
n,从2开始的
dp[0]= 0
dp[1] = 0
dp[2] = 1
4.遍历顺序
从左向右遍历
5.举例推导
代码
class Solution {
public int integerBreak(int n) {
int[] dp = new int[n + 1];
dp[0] = 0;
dp[1] = 0;
dp[2] = 1;
for (int i = 3;i < n + 1;i++){
for (int j = 1;j <= i / 2;j++){
dp[i] = Math.max(dp[i],Math.max(j * (i - j),j * dp[i - j]));
// 第二个Math.max()应注意。
// j * (i - j) 是单纯的把整数 i 拆分为两个数 也就是 i,i-j ,再相乘
//而j * dp[i - j]是将 i 拆分成两个以及两个以上的个数,再相乘。
}
}
return dp[n];
}
}
七、不同的二叉搜索树
链接: 96.不同的二叉搜索树
思路
本题属于我的盲区
1.dp[i]含义
dp[i]:指 i 个节点组成符合条件的二叉搜索树的种类
2.递推公式
【没想到】
观察n= 3的情况
1 开头,2开头 3开头
1开头的情况:
左0 * 右2
2开头的情况:
左1 * 右1
3开头的情况:
左2 * 右2
然后可以得出来
dp[i] += dp[j - 1] * dp[i - j]
1 <= j <= i
3.初始化
dp[0] = 1;(题目要求的范围不包含,为了便于计算)
dp[1] =1;
4.遍历顺序:正常从左向右的顺序
5.举例推导:略
代码
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 + 1;i++){
for (int j = 1;j <= i;j++){
dp[i] += dp[j - 1] * dp[i - j];
}
}
return dp[n];
}
}
总结
以上,除不同路径两道题外,即使循环是双重循环,但dp数组也都是一位数组,j的循环是一种辅助