⚡刷题计划day27 动态规划(一)开始,第三期后是背包专题,可以点个免费的赞哦~
往期可看专栏,关注不迷路,
您的支持是我的最大动力~
目录
什么是动态规划
动态规划的解题步骤
题目一:509. 斐波那契数
题目二:70. 爬楼梯
题目三:746. 使用最小花费爬楼梯
动态规划,英文:Dynamic Programming,简称DP,如果某一问题有很多重叠子问题,使用动态规划是最有效的。
特征:一个问题,可以拆分成一个一个的子问题,解决子问题,顺带解决这个问题
核心思想:拆分子问题,记住过程,减少重复运算。
例子:
1+1+1+1+1+1=? 等于6
在上面式子的在加上一个“1+”。答案是多少?
直接通过之前计算过的答案,再加1
做动规题目的时候,很多同学会陷入一个误区,就是以为把状态转移公式背下来,照葫芦画瓢改改,就开始写代码,甚至把题目AC之后,都不太清楚dp[i]表示的是什么。
这就是一种朦胧的状态,然后就把题给过了,遇到稍稍难一点的,可能直接就不会了,然后看题解,然后继续照葫芦画瓢陷入这种恶性循环中。
状态转移公式(递推公式)是很重要,但动规不仅仅只有递推公式。
dp五步曲:
确定dp数组(dp table)以及下标的含义
确定递推公式
dp数组如何初始化
确定遍历顺序
举例推导dp数组
509. 斐波那契数
(https://leetcode.cn/problems/fibonacci-number/description/)
斐波那契数大家应该是非常熟悉了,虽然这题简单,但我们目的是用这题加深对解题方法论的理解的。
动规五部曲:
1.确定dp数组以及下标的含义
这里我们要用一个一维dp数组来保存递归的结果,
dp[i]的定义为:第i个数的斐波那契数值是dp[i]
2.确定递推公式
题目已经直接把递推公式给我们了,状态转移方程:
dp[i] = dp[i - 1] + dp[i - 2];
3.dp数组如何初始化
如何初始化题目也是给了的,
dp[0] = 0;
dp[1] = 1;
4.确定遍历顺序
从递归公式dp[i] = dp[i - 1] + dp[i - 2];中可以看出,dp[i]是依赖 dp[i - 1] 和 dp[i - 2],那么遍历的顺序一定是从前到后遍历的
5.举例推导dp数组
按照这个递推公式dp[i] = dp[i - 1] + dp[i - 2],我们来推导一下,当N为10的时候,dp数组应该是如下的数列:
0 1 1 2 3 5 8 13 21 34 55
如果代码写出来,发现结果不对,就把dp数组打印出来看看和我们推导的数列是不是一致的。
法一,代码如下:
class Solution {
public int fib(int n) {
if(n<2){
return n;
}
int[] dp =new int[n+1];
dp[0] = 0;
dp[1] = 1;
for(int i=2;i<=n;i++){
dp[i] = dp[i-1]+dp[i-2];
}
return dp[n];
}
}
法二,其实我们只需维护两个数值就可以了,不用记录整个序列。代码如下:
class Solution {
public int fib(int n) {
if(n<2){
return n;
}
int[] dp = new int[2];
dp[0]=0;
dp[1]=1;
for(int i=2;i<=n;i++){
int sum = dp[0]+dp[1];
dp[0]=dp[1];
dp[1]=sum;
}
return dp[1];
}
}
70. 爬楼梯
(https://leetcode.cn/problems/climbing-stairs/description/)
这题也十分经典。
爬到第一层楼梯有一种方法,爬到二层楼梯有两种方法。
那么第一层楼梯再跨两步就到第三层 ,第二层楼梯再跨一步就到第三层。
所以到第三层楼梯的状态可以由第二层楼梯 和 到第一层楼梯状态推导出来,那么就可以想到动态规划了。
动规五部曲:
1.确定dp数组以及下标的含义
dp[i]:爬到第i层楼梯,有dp[i]种方法
2.确定递推公式
从dp[i]的定义可以看出,dp[i] 可以有两个方向推出来。
首先是dp[i - 1],上i-1层楼梯,有dp[i - 1]种方法,那么再一步跳一个台阶不就是dp[i]了么。
还有就是dp[i - 2],上i-2层楼梯,有dp[i - 2]种方法,那么再一步跳两个台阶不就是dp[i]了么。
那么dp[i]就是 dp[i - 1]与dp[i - 2]之和!
所以dp[i] = dp[i - 1] + dp[i - 2] 。
在推导dp[i]的时候,一定要时刻想着dp[i]的定义,否则容易跑偏。
3.dp数组如何初始化
再回顾一下dp[i]的定义:爬到第i层楼梯,有dp[i]种方法。
讨论第0层也没什么意义,根据题目可以从1、2层开始:
第1层:dp[1] = 1
第2层:dp[2] = 2
4.确定遍历顺序
从递推公式dp[i] = dp[i - 1] + dp[i - 2];中可以看出,遍历顺序一定是从前向后遍历的
5.举例推导dp数组
如果代码出问题了,就把dp table 打印出来,看看究竟是不是和自己推导的一样。
完整代码如下:
class Solution {
public int climbStairs(int n) {
if(n<=2){
return n;
}
int[] dp = new int[n+1];
dp[1]=1;
dp[2]=2;
for(int i=3;i<=n;i++){
dp[i] = dp[i-1]+dp[i-2];
}
return dp[n];
}
}
746. 使用最小花费爬楼梯
(https://leetcode.cn/problems/min-cost-climbing-stairs/description/)
注意题目理解,
是可以选择从下标为 0
或下标为 1
的台阶开始爬楼梯。然后你当你往上爬时才需要支付费用。
动规五步曲:
1.确定dp数组以及下标的含义
dp[i]:到达第i台阶所支付的最少费用。
2.确定递推公式
可以有两个途径得到dp[i],一个是dp[i-1] 一个是dp[i-2]。
dp[i - 1] 跳到 dp[i] 需要花费 dp[i - 1] + cost[i - 1]。
dp[i - 2] 跳到 dp[i] 需要花费 dp[i - 2] + cost[i - 2]。
那么究竟是选从dp[i - 1]跳还是从dp[i - 2]跳呢?
一定是选最小的,所以dp[i] = min(dp[i - 1] + cost[i - 1], dp[i - 2] + cost[i - 2]);
3.dp数组如何初始化
题目“你可以选择从下标为 0 或下标为 1 的台阶开始爬楼梯。” 也就是说 到达 第 0 个台阶是不花费的,但从 第0 个台阶 往上跳的话,需要花费 cost[0]。
所以初始化 dp[0] = 0,dp[1] = 0;
4.确定遍历顺序
本题的遍历顺序其实比较简单,简单到很多同学都忽略了思考这一步直接就把代码写出来了。
因为是模拟台阶,而且dp[i]由dp[i-1]dp[i-2]推出,所以是从前到后遍历cost数组就可以了。
其实后面稍微有些难度的动态规划,比如背包问题等等,其遍历顺序可能就没那么容易确定下来了,所以还是需要养成考虑遍历顺序的习惯。
5.举例推导dp数组
如果大家代码写出来有问题,就把dp数组打印出来,看看和自己的推导的是不是一样的。
代码如下:
class Solution {
public int minCostClimbingStairs(int[] cost) {
int n = cost.length;
int[] dp = new int[n+1];
dp[0]=0;// 默认第一步都是不花费体力的
dp[1]=0;
for(int i=2;i<=n;i++){
dp[i] = Math.min(dp[i-1]+cost[i-1],dp[i-2]+cost[i-2]);
}
return dp[n];
}
}
赞赞~