动态规划专题

概述

  • 该博客记录了Yukiii学习dp的过程,主要是从 AcWing代码随想录 中学习动态规划的解题思路,同时把做过的的题目记录下来方便复习,之后可能会不定期更新。
  • 所有dp问题全都遵循5步走的方法,首先用闫氏分析法确定状态表示状态计算,再初始化dp数组,接着根据状态转移的状态依赖关系确定遍历顺序,最后如果出问题举例推导,并把dp数组打印出来,看是代码实现出问题还是分析出问题。
  • 学习y总的思路,从集合的角度思考问题,如果最优解只可能出现在划分出的一边那就是贪心,如果都有可能出现最优解那就是dp。

背包 dp

  • 背包问题是一种选择问题,在出现选择哪些物品的问题可以考虑使用背包模型。
  • 因此首先看题目是不是选择问题,再看每个物品可以用几次,判断是01背包还是完全背包等等,关键在于能否看出是背包模型
  • AcWing 2. 01背包问题

    • 为什么两个for循环的嵌套顺序是先遍历物品再遍历背包容量,反过来行不行

      这需要理解递推公式本质和递推方向,dp[i][j]dp[i - 1][j]dp[i - 1][j - w[i]] 得到,它们都在 dp[i][j] 的左上(正上)方向,两种for循环(逐行和逐列)都能保证 dp[i][j] 可以被推导得到。

      在遍历背包容量维度的时候可以正序也可以倒序,因为它依赖的是上一层 [i - 1] 层的数据,此时已经全被计算出来。

    • 二维如何变成一维滚动数组,为什么能变,两个for循环顺序反过来行不行

      优化掉物品维度,也就是变成 dp[j],这是因为 dp[i][j] 的计算都是依赖于上一层 dp[i - 1][...] 的结果,因此可以理解成直接把上一层拷贝到这一层进行覆盖,变成滚动数组。

      优化之后由于 dp[j] 是由 dp[j - w[i]] 得来的,在计算 dp[j] 的时候 dp[j - w[i]] 应该保留的是 [i - 1] 那一层的结果,所以遍历背包容量的顺序应该倒序遍历避免覆盖 [i - 1] 层的结果。

      并且滚动数组做的时候必须要先遍历物品再遍历背包容量,这是因为如果先遍历背包容量,且必须是倒序遍历(即从最右侧自上而下遍历),此时可以发现递推公式中的 dp[j - w[i]] 永远是 0,也就是说背包只能放一个物品。

      for (int i = 1; i <= n; i ++)
          for (int j = m; j >= w[i]; j --)
              	dp[j] = max(dp[j], dp[j - w[i]] +v[i]);
      
    • 如何确定当前状态选择了哪些物品

      当时复旦保研机试就考了这题,我居然栽在了这里 TAT

      for(int i = 1; i <= n; i ++)
          for (int j = m; j >= w[i]; j --)
              if (dp[j] <= dp[j - w[i]] + v[i])
              {
                  dp[j] = dp[j - w[i]] + v[i];
                  path[i][j] = true;
              }
          
      for (int i = n, j = m; i && j; i --)
          if (path[i][j] == 1)
          {
              cout << i << ' ' << w[i] << ' ' << v[i] << endl;
              j -= w[i];
          }
      
  • LeetCode 416.分割等和子集

  • LeetCode 1049. 最后一块石头的重量Ⅱ

  • LeetCode 494. 目标和

  • LeetCode 474. 一和零

  • AcWing 3. 完全背包问题

    • 单纯的完全背包问题能不能颠倒for循环顺序

      可以,不管哪个 for 循环在外都能保证在计算 dp[j] 时依赖的状态已经计算出来。

      // 01背包   dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - w[i]] + v[i]);
      // 完全背包  dp[i][j] = max(dp[i - 1][j], dp[i][j - w[i]] + v[i]);   [这式子是通过等价代换得到的]
      
      for (int i = 1; i <= n; i ++)
      	for (int j = w[i]; j <= m; j ++)
          	dp[j] = max(dp[j], dp[j - w[i]] + v[i]);
      
  • LeetCode 518. 零钱兑换Ⅱ (装满完全背包的组合数)

    LeetCode 377. 组合求和Ⅳ (装满完全背包的排列数)

    • 求装满完全背包组合数和排列数的区别,为什么会有这样区别

      组合数外层遍历物品,内层遍历背包;求排列数外层遍历背包,内层遍历物品。

      以本题为例,求组合数先遍历物品,一个个硬币遍历只会出现 {coin[0], coin[1]},而求排列数内层遍历物品,因此包含 {coin[0], coin[1]}, {coin[1], coin[0]} 的情况。

    • 求解完全背包问题要区分求的是组合数还是排列数

  • LeetCode 322. 零钱兑换

    LeetCode 279. 完全平方数

  • LeetCode 139. 单词拆分

    状态表示: 子串 s(1, i) 能否由词典中的词构成。

    状态转移: 子串 s(1, i) 能构成且 s(i, j) 也存在于词典之中,则 s(1, j) 也能够构成。

  • AcWing 900. 整数划分

线性 dp

状态机 dp

// dp[i][j]: 第i天状态j所剩的最大现金 (一共5种状态)
// 0. 不持有股票
// 1. 持有股票
// 2. 不持有股票,且完成1次买卖
// 3. 持有股票,且完成1次买卖
// 4. 不持有股票,且完成2次买卖

// 初始化要注意dp[0][3]的初始化,相当于是第0天买卖完一次再次买入
dp[0][1] = -prices[0];
dp[0][3] = -prices[0];

for (int i = 1; i < n; i ++)
{
	dp[i][0] = dp[i - 1][0];
	dp[i][1] = max(dp[i - 1][1], dp[i - 1][0] - prices[i]); 
	dp[i][2] = max(dp[i - 1][2], dp[i - 1][1] + prices[i]);
	dp[i][3] = max(dp[i - 1][3], dp[i - 1][2] - prices[i]);
	dp[i][4] = max(dp[i - 1][4], dp[i - 1][3] + prices[i]);
}

return dp[n - 1][4];

区间 dp

状态表示: 所有合并 (i, j) 中的石子的方案的集合,属性: min 代价。

划分方式: 按照最后一次合并的位置进行划分。

初始化: 所有自己合并自己的代价为0,dp[i][i] = 0,其余代价为 0x3f3f3f3f

在这里插入图片描述

memset(dp, 0x3f, sizeof dp);
for (int i = 1; i <= n; i ++) dp[i][i] = 0;
    
for (int len = 2; len <= n; len ++)
    for (int l = 1; l + len - 1 <= n; l ++)
    {
        int r = l + len - 1;
        for (int k = l; k < r; k ++)
            dp[l][r] = min(dp[l][r], dp[l][k] + dp[k + 1][r] + a[r] - a[l - 1]);
    }
    
cout << dp[1][n] << endl;

区分相似题: AcWing 2875. 超级胶水AcWing 148. 合并果子

超级胶水就是把代价从求和变成求乘积,合并果子则是不需要合并相邻两堆。

树形dp

感觉树形dp的套路都差不多,递归计算dp数组的值,通过儿子的信息计算父亲的信息,或者通过父亲的信息计算儿子的信息。

  • AcWing 285. 没有上司的舞会

    状态表示: dp(u, 0) dp(u, 1) 所有以u 为根节点的子树,且不选/选u的方案的集合。属性: Max欢乐度。

    状态计算:

    在这里插入图片描述

    dfs(root);
    
    int dfs(int x)
    {
        dp[x][1] = happy[x];
        
        for (int i = 0; i < u[x].size(); i ++ ) // 用的 vector<int> u[N] 的邻接表存储树
        {
            int j = u[x][i];
            
            dfs(j); // 递归先计算儿子
            
            dp[x][0] += max(dp[j][0], dp[j][1]);
            dp[x][1] += dp[j][0];
        }
    }
        
    cout << max(dp[root][0], dp[root][1]) << endl;
    
  • LeetCode 337. 打家劫舍Ⅲ

  • 14
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值