当前位置:   article > 正文

代码随想录day32--动态规划理论基础

代码随想录day32--动态规划理论基础

什么是动态规划

动态规划简称DP,如果某一问题有很多重叠子问题,使用动态规划是最有效的

所以动态规划中每一个状态一定是由上一个状态推导出来的,这一点一定要和贪心区别出来,贪心没有状态推导,而是直接从局部直接选择最优。

在贪心中,有一个例子是背包问题。

eg:由N件物品和一个最多能背重量为W的背包。第i件物品的重量是weight[i],得到的价值是value[i]。每件物品只能使用一次,求解将哪些物品装进背包里物品价值总和最大。

动态规划中dp[j]是由dp[j-weight]推导出的,然后取max(dp[j],dp[j-weight[i]+value[i])。

但如果是使用贪心,每次拿物品只会选择一个最大的或者最小的,和之前的状态没有什么关系。所以贪心解决不了动态规划的问题。

大家只需要明白动态规划是由前一个状态推导出来的,而贪心是局部直接选出最优的。


动态规划的解题步骤

做动态规划的题目的时候,很多同学包括我自己都有一个误区,就算将状态转移公式背下,然后照葫芦画瓢,然后根据公式进行代码书写,无论题目是否通过,都不清楚我们使用的dp[i]到底有什么用。

状态转移公式(递推公式)是很重要,但是仔细了解过后,或发现,动态规划不仅仅由递推公式。

对于动态规划,将其拆分为以下五部曲,这五步都搞懂,就基本可以把动态规划掌握:

1.确定dp数组以及其下标的含义

2.确定递推公式

3.dp数组如何初始化

4.确定遍历顺序

5.举例推导dp数组

*一定不要忽略初始化,因为一些情况是递推公式决定了dp数组要如何初始化


动态规划应该如何debug

绝大部分同学,对于动态规划的题目是,看题解感觉会了,然后照葫芦画瓢,如果能过那就没关系,但是如果没过,那么怎么改都过不去,对于之前说的dp数组的初始化,递推公式,遍历顺序,都是处于一种什么都不明白的的状态。

找到问题的最好办法就是将dp数组打印出来,看看究竟是不是按照自己的思路推导的

做动态规划的题目,写代码之前一定要把状态转移在dp数组的上具体情况模拟一遍,心中有数,最后确定推出的是否是想要的结果。

然后再写代码,如果代码没有通过,那就打印dp数组,看看是不是自己预先推导的哪里有出入。

如果打印出和自己预先模拟推导的一样,那么就算自己的递归公式、初始化或者遍历顺序有问题。如何还是不一样,那就是代码实现的细节有问题了。

这才是一个完整的思考过程,而不是代码出现了问题,就毫无头绪的乱改,最后依旧过不了,或者是稀里糊涂的过

从现在开始,写动态规划的题目出现问题或者无法通过,自己可以先思考这三个问题:

1.这道题我距离推导状态公式了吗

2.我们打印dp数组了吗

3.打印出的dp数组和我想的一样吗

如果这三个问题实现了,那么基本上动态规划的题目也就是解决了

亦或者是可以更清晰的明白自己究竟是哪一点不明白,是状态转移不明白,还是具体代码不知道该怎么写,还是不理解遍历dp数组的顺序。这样带着目的性的问题,就很好了。


LeetCode509.斐波那契数

题目描述:

斐波那契数 (通常用 F(n) 表示)形成的序列称为 斐波那契数列 。该数列由 0 和 1 开始,后面的每一项数字都是前面两项数字的和。也就是:

F(0) = 0,F(1) = 1
F(n) = F(n - 1) + F(n - 2),其中 n > 1

给定 n ,请计算 F(n) 。

示例 1:

输入:n = 2
输出:1
解释:F(2) = F(1) + F(0) = 1 + 0 = 1

示例 2:

输入:n = 3
输出:2
解释:F(3) = F(2) + F(1) = 1 + 1 = 2

示例 3:

输入:n = 4
输出:3
解释:F(4) = F(3) + F(2) = 2 + 1 = 3

解题思路:

·这道题大家再熟悉不过了,但是就是因为这题比较简单,所以大家可能并没有做什么分析,就顺手写过了,我们使用动态规划五部曲进行分析

1.确定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],那么遍历顺序,就算一定是从前到后遍历

5.举例推导dp数组

按照递推公式,我们推导一下,当n为10时,数列应该是:0 1 1 2 3 5 8 13 21 34 55

如果最终代码无法通过,我们就将dp数组打印出,观察与我们推导的数列是不是一致的

以上,使用动态规划的方法全部分析完毕,代码如下:

  1. class Solution {
  2. public:
  3. int fib(int n) {
  4. if(n <= 1) return n;
  5. vector<int> dp(n+1);
  6. dp[0] = 0;
  7. dp[1] = 1;
  8. for(int i = 2;i <= n;i++){
  9. dp[i] = dp[i-1]+dp[i-2];
  10. }
  11. return dp[n];
  12. }
  13. };

·时间复杂度:O(n)

·空间复杂度:O(n)

总结:这道题虽然说非常的基础,但是我们严格按照动态规划五部曲进行分析,发现其实分析过程也并不会复杂,我们使用简单题目用于掌握方法,接下来的方法会越来越重要。

LeetCode70.爬楼梯

题目描述:

假设你正在爬楼梯。需要 n 阶你才能到达楼顶。

每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?

示例 1:

输入:n = 2
输出:2
解释:有两种方法可以爬到楼顶。
1. 1 阶 + 1 阶
2. 2 阶

示例 2:

输入:n = 3
输出:3
解释:有三种方法可以爬到楼顶。
1. 1 阶 + 1 阶 + 1 阶
2. 1 阶 + 2 阶
3. 2 阶 + 1 阶

解题思路:

·这题仔细分析,多举几个例子,就会发现其规律。爬一层楼梯有一种方法,爬两层楼梯有两种方法,爬三层楼梯就可以根据前两层的规律进行推导

我们来进行动态规划五部曲的分析:

1.确定dp数组以及下标的含义

dp[i]:爬到第i层楼梯,有dp[i]种方法

2.确定递推公式

这道题怎么样才能推导出dp[i]呢?

从dp[i]的定义可以看出,dp[i]可以有两个方向可以推导出

首先是dp[i-1],上i-1层楼梯,有dp[i-1]种方法,那么再一步跳一个台阶不就是dp[i]了

dp[i-2]也是一样的思想,有dp[i-2]种方法,那么再一步跳两个个台阶不就是dp[i]了

所以就可以得出结论 dp[i]就算dp[i-1]和dp[i-2]之和

在推导dp[i]的时候,一定要时刻想着dp[i]的定义,这就体现出确定dp数组以及下标的含义的重要性

3.dp数组如何初始化

dp[0] = 1,因为不动也是一种移动方法

dp[1] = 1,只有一步可以走

dp[2]  = 2 ,有一步和两步可以走,所以我们就可以从i=3开始递推

4.确定遍历顺序

从递推公式dp[i] = dp[i-1]+dp[i-2]可以看出,遍历顺序一定是从前向后的

5.举例推导dp数组

我们会发现这个数组推导出来就算斐波那契,如果代码出现问题,那么就把dp数组打印出来,看看是否和自己推导的一样

代码如下:

  1. class Solution {
  2. public:
  3. int climbStairs(int n) {
  4. if(n <= 1) return n;
  5. vector<int> dp(n+1);
  6. dp[0] = 1;
  7. dp[1] = 1;
  8. for(int i = 2;i <= n;i++){
  9. dp[i] = dp[i-1]+dp[i-2];
  10. }
  11. return dp[n];
  12. }
  13. };

 时间复杂度:O(n)

空间复杂度:O(n) 

总结:这道题其实和斐波那契数题目是一样的,但是动态规划五部曲,却比上一题复杂了那么多

关键就算斐波那契题目描述中,已经把递归公式和如何初始化都给出来了,剩下的就很好推出

但是本题需要逐个分析,大家应该初步感受出了动态规划的五部曲了

LeetCode746.使用最小花费爬楼梯

题目描述:

给你一个整数数组 cost ,其中 cost[i] 是从楼梯第 i 个台阶向上爬需要支付的费用。一旦你支付此费用,即可选择向上爬一个或者两个台阶。

你可以选择从下标为 0 或下标为 1 的台阶开始爬楼梯。

请你计算并返回达到楼梯顶部的最低花费。

示例 1:

输入:cost = [10,15,20]
输出:15
解释:你将从下标为 1 的台阶开始。
- 支付 15 ,向上爬两个台阶,到达楼梯顶部。
总花费为 15 。

示例 2:

输入:cost = [1,100,1,1,1,100,1,1,100,1]
输出:6
解释:你将从下标为 0 的台阶开始。
- 支付 1 ,向上爬两个台阶,到达下标为 2 的台阶。
- 支付 1 ,向上爬两个台阶,到达下标为 4 的台阶。
- 支付 1 ,向上爬两个台阶,到达下标为 6 的台阶。
- 支付 1 ,向上爬一个台阶,到达下标为 7 的台阶。
- 支付 1 ,向上爬两个台阶,到达下标为 9 的台阶。
- 支付 1 ,向上爬一个台阶,到达楼梯顶部。
总花费为 6 。

解题思路:

1.确定dp数组以及下标的含义

使用动态规划,需要使用一个数组来记录状态,本题只需要一个一维数组dp[i]就可以了

dp[i]的定义:达到第i个台阶所花费的最少体力为dp[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]+dp[i-2]

按照题目要求,所以我们选择最小的,dp[i] = min(dp[i-1]+cost[i-1],dp[i-2]+cost[i-2])

3.dp数组如何初始化

看了递推公式,所以我们就可以知道需要初始化dp[i-1]和dp[i-2],也就是dp[0]dp[1]

题目中说可以选择下标为0或下标为1的台阶开始爬楼梯,也就是说到达第0个台阶和第一个台阶是不花费的,所以dp[0] = dp[1] = 0

4.确定遍历顺序

因为是模拟台阶,而且dp[i]由dp[i-1]和dp[i-2]推出,所以是从前到后遍历cost数组即可\

5.举例推导dp数组

我们使用示例2进行举例

如果代码出现问题,就将dp数组打印出来,看看和如上推导是不是一样的

代码如下:

  1. class Solution {
  2. public:
  3. int minCostClimbingStairs(vector<int>& cost) {
  4. vector<int> dp(cost.size()+1);
  5. dp[0] = 0;
  6. dp[1] = 0;
  7. for(int i = 2;i <= cost.size();i++){
  8. dp[i] = min(dp[i-2]+cost[i-2],dp[i-1]+cost[i-1]);
  9. }
  10. return dp[cost.size()];
  11. }
  12. };

·时间复杂度:O(n)

·空间复杂度:O(n)

总结:这题和爬楼梯也是类似,只是多了一个需要花费的部分,所以并没有什么难理解的地方

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/盐析白兔/article/detail/387699
推荐阅读
相关标签
  

闽ICP备14008679号