《代码随想录》专题:动态规划


  • 母题清单
    • 509. 斐波那契数(第一次浅浅尝试使用动态规划)
    • 70. 爬楼梯(第一次将动态规划用于实战)
      • 746. 使用最小花费爬楼梯(递归公式进一步增加了难度,注意理解)
      • 62.不同路径(第一次使用二维动态规划,注意初始化的时候是初始几个一维数值,这道题目还可以用数论来解决,可以尝试一下)
      • 63. 不同路径 II(放上障碍之后会影响初始化以及后续dp数组的赋值,注意增加对应的判断条件,题解中有对起点和终点障碍物的判断,其实这部分可以不写)
      • 343. 整数拆分(题目中的递归式推导更加复杂了)
      • 96.不同的二叉搜索树(递推式的获取更加隐晦,难度进一步增加)
    • 01背包理论基础(这里仅仅是理论部分,注意一般采用的是一维数组,这样简单一些)
      • 416. 分割等和子集(将背包体积定义为总量的1/2,检查背包是否能被装满)
      • 1049. 最后一块石头的重量 II(尽量将石头一分为二,看看当背包的容量为总石头重量的一半时,最多能装多少重量的石头,然后将剩余的石头与这堆石头作差,便是结果)。
      • 494.目标和(将数组中的元素分成符号为“+”的和符号为“-”的,两者相减即为目标值。通过简单的计算求出符号为"+"的数的总和,求能得到该总和的情况数。)
      • 474. 一和零(这道题其实是01背包典型问题的变体,背包的重量其实就是目标01的个数m和n,而石头的重量就是子串中01的个数,石头的价值就是1。难点是如果把本是一维的背包问题转换为二维的)
    • 完全背包问题(和01背包不同的点是:物品可以取无限次。背包问题又可以分为经典的背包问题、排列问题和组合问题(前一种dp[0]初始化为0,后两种dp[0]初始化为1),每中问题其实都是有对应的套路的,即便有套路,也要记得勤加思考。)
      • 518.零钱兑换II(这道题是组合问题,而非排列问题,所以需要先对背包进行遍历然后再遍历物品)
      • 377. 组合总和 Ⅳ(这道题是排列问题而非组合问题,所以需要先遍历物品再对背包进行遍历)
      • 322. 零钱兑换(这道题是求凑成总金额最少硬币的个数,所以这不是一道排列组合题,不用在乎先遍历背包还是物品。其次这道题求的是最小值,所以需要初始化背包的元素为INT_MAX,然后取min,具体的递推公式比较典型,这里不作介绍)
      • 139.单词拆分(这道题也属于完全背包问题,但自我感觉没有消化透,一定要再多练习一下。)
    • 股票问题(类似于状态机)
    • 子序列问题


1、动态规划

  • 题目链接:509. 斐波那契数
  • 题解:这道题目可以用递归的方法简单地实现,当然这里主要是要介绍动态规划,下面介绍动规五部曲。
    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];中可以看出,dp[i]是依赖 dp[i - 1]dp[i - 2],那么遍历的顺序一定是从前到后遍历的。
    5. 举例推导dp数组
      按照这个递推公式dp[i] = dp[i - 1] + dp[i - 2],我们来推导一下,当N10的时候,dp数组应该是如下的数列:0 1 1 2 3 5 8 13 21 34 55。如果代码写出来,发现结果不对,就把dp数组打印出来看看和我们推导的数列是不是一致的。
  • 代码如下:
    class Solution {
    public:
        int fib(int N) {
        	/* 如果不加这行,假定N=0或者N=1,在初始化或者遍历的时候会出现数组越界问题。*/
            if (N <= 1) return N; 
            vector<int> dp(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];
        }
    };
    

2、爬楼梯

  • 题目链接:70. 爬楼梯
  • 题解:使用动态规划五部曲
    1. 确定dp数组以及下标的含义
      dp[i]: 爬到第i层楼梯,有dp[i]种方法
    2. 确定递推公式
      dp[i] = dp[i - 1] + dp[i - 2]。首先是dp[i - 1],上i-1层楼梯,有dp[i - 1]种方法,那么再一步跳一个台阶不就是dp[i]了么。还有就是dp[i - 2],上i-2层楼梯,有dp[i - 2]种方法,那么再一步跳两个台阶不就是dp[i]了么。
    3. dp数组如何初始化
      dp[1] = 1,dp[2] = 2
    4. 确定遍历顺序
      从递推公式dp[i] = dp[i - 1] + dp[i - 2];中可以看出,遍历顺序一定是从前向后遍历的
    5. 举例推导dp数组
      举例当n5的时候,dp tabledp数组)应该是这样的。
      在这里插入图片描述
  • 代码如下:
    class Solution {
    public:
        int climbStairs(int n) {
            if (n <= 1) return n; // 因为下面直接对dp[2]操作了,防止空指针
            vector<int> dp(n + 1);
            dp[1] = 1;
            dp[2] = 2;
            for (int i = 3; i <= n; i++) { // 注意i是从3开始的
                dp[i] = dp[i - 1] + dp[i - 2];
            }
            return dp[n];
        }
    };
    

3、动态规划:01背包理论基础(滚动数组)

  • 注意:该部分仅介绍01背包问题的一维版本。

  • 模型原型
    n件物品和一个最多能背重量为w 的背包。第i件物品的重量是weight[i],得到的价值是value[i]。每件物品只能用一次,求解将哪些物品装入背包里物品价值总和最大。
    在这里插入图片描述

    • 本例中背包最大重量为4,物品为:
      在这里插入图片描述
  • 直接使用动规五部曲分析:

    1. 确定dp数组的定义
      在一维dp数组中,dp[j]表示:容量为j的背包,所背的物品价值可以最大为dp[j]

    2. 一维dp数组的递推公式

      • dp[j]可以通过dp[j - weight[i]]推导出来,dp[j - weight[i]]表示容量为j - weight[i]的背包所背的最大价值。
      • dp[j - weight[i]] + value[i] 表示 容量为 j - 物品i重量 的背包 加上 物品i的价值。(也就是容量为j的背包,放入物品i了之后的价值即:dp[j]
      • 此时dp[j]有两个选择,一个是取自己dp[j] 相当于不放物品i,一个是取dp[j - weight[i]] + value[i],即放物品i,指定是取最大的,毕竟是求最大价值。
      • 最后得到递推公式
        dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
        
    3. 一维dp数组如何初始化
      关于初始化,一定要和dp数组的定义吻合,这里全部初始化为0即可。

    4. 一维dp数组遍历顺序
      代码如下:

      for(int i = 0; i < weight.size(); i++) { // 遍历物品
          for(int j = bagWeight; j >= weight[i]; j--) { // 遍历背包容量
              dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
          }
      }
      

      一维dp遍历的时候,背包是从大到小。倒序遍历是为了保证物品i只被放入一次!但如果一旦正序遍历了,那么物品0就会被重复加入多次!

      • 举一个例子:物品0的重量weight[0] = 1,价值value[0] = 15
        • 如果正序遍历
          dp[1] = dp[1 - weight[0]] + value[0] = 15
          dp[2] = dp[2 - weight[0]] + value[0] = 30
          
          此时dp[2]就已经是30了,意味着物品0,被放入了两次,所以不能正序遍历
        • 如果倒序遍历
          dp[2] = dp[2 - weight[0]] + value[0] = 15 (dp数组已经都初始化为0)
          dp[1] = dp[1 - weight[0]] + value[0] = 15
          
    5. 举例推导dp数组

      • 一维dp,分别用物品0,物品1,物品2 来遍历背包,最终得到结果如下:
        在这里插入图片描述
  • 最后得出最终源代码

    void test_1_wei_bag_problem() {
        vector<int> weight = {1, 3, 4};
        vector<int> value = {15, 20, 30};
        int bagWeight = 4;
    
        // 初始化
        vector<int> dp(bagWeight + 1, 0);
        for(int i = 0; i < weight.size(); i++) { // 遍历物品
            for(int j = bagWeight; j >= weight[i]; j--) { // 遍历背包容量
                dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
            }
        }
        cout << dp[bagWeight] << endl;
    }
    
    int main() {
        test_1_wei_bag_problem();
    }
    

4、买卖股票的最佳时机

  • 题目链接:121. 买卖股票的最佳时机

  • 题解:使用动规五部曲

    1. 确定dp数组(dp table)以及下标的含义
      • dp[i][0] 表示第i天持有股票所得最多现金,很显然这个值只能是负数,该值越大证明买入的成本越低
      • dp[i][1] 表示第i天不持有股票所得最多现金
    2. 确定递推公式
      • i天持有股票:dp[i][0] = max(dp[i - 1][0], -prices[i]);
        • i-1天就持有股票,那么就保持现状,即dp[i - 1][0]
        • i天买入股票,财产为-prices[i]
      • i天不持有股票:dp[i][1] = max(dp[i - 1][1], prices[i] + dp[i - 1][0]);
        • i-1天就不持有股票,那么就保持现状,即dp[i - 1][1]
        • i天卖出股票,所得现金就是按照今天股票价格卖出后所得现金,即prices[i] + dp[i - 1][0]
    3. dp数组如何初始化
      • 0天持有股票,肯定是第0天买入股票,即dp[0][0] -= prices[0]
      • 0天不持有股票,那么dp[0][1] = 0
    4. 确定遍历顺序
      • 从递推公式可以看出dp[i]都是由dp[i - 1]推导出来的,那么一定是从前向后遍历。
    5. 举例推导dp数组
      以输入[7,1,5,3,6,4]为例,dp数组状态如下:
      在这里插入图片描述
  • 最后得到代码:

    class Solution {
    public:
        int maxProfit(vector<int>& prices) {
            int len = prices.size();
            if (len == 0) return 0;
            vector<vector<int>> dp(len, vector<int>(2));
            dp[0][0] -= prices[0];
            dp[0][1] = 0;
            for (int i = 1; i < len; i++) {
                dp[i][0] = max(dp[i - 1][0], -prices[i]);
                dp[i][1] = max(dp[i - 1][1], prices[i] + dp[i - 1][0]);
            }
            return dp[len - 1][1];
        }
    };
    
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Elec Liu

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值