动态规划 | 青训营笔记

105 阅读5分钟

这是我参与「第五届青训营 」伴学笔记创作活动的第 5 天。

今天看的视频课内容比较多,笔记还没整理好,先发一个学习动态规划的笔记吧,明后天补上今天笔记。

闫式DP法

动态规划问题本质:有限集最值问题。 动态规划:

  • 状态表示 - 化零为整

    • 集合 f(i, j)

      • 一般描述:所有只考虑前i个物品,且总体积不超过j的选法的集合
    • 属性:集合中每个方案的 max / min / count 值

  • 状态计算 - 化整为零

    • 划分为若干个不重复、不遗漏的子集来求解

    划分依据:寻找最后一个不同点

例题

01背包问题

2. 01背包问题 - AcWing题库

朴素版本

动态规划过程:

  • 状态表示 - 化零为整

    • 集合 f(i, j)

      • 一般描述:所有只考虑前i个物品,且总体积不超过j的选法的集合
    • 属性:max,当前情况下背包内物品价值 所以集合f(i, j)划分为:

      1. 所有不选第i个物品的方案 -- 1到i-1物品,总体积不超过j。f(i, j) = f(i-1, j)
      2. 所有选择第i个物品的方案 -- 1到i物品,总体积不超过j。因为1到i-1已经不变了,所以关键是选出第i个使得值最大f(i, j) = f(i-1, j-vi) + wi 即二维数组迭代公式1:f(i, j) = max (f(i-1, j), f(i-1, j-v_i) + w_i)
  • 状态计算 - 化整为零

    • 划分为若干个不重复、不遗漏的子集来求解
 #include<iostream>
 ​
 using namespace std;
 ​
 const int N = 1010;
 int n, m;
 int v[N], w[N];
 int f[N][N];
 ​
 int main()
 {
     cin >> n >> m;
 ​
     for(int i = 1; i <= n; ++ i)
         cin >> v[i] >> w[i];
 ​
     for(int i = 1; i <= n; ++ i)
         for(int j = 0; j <= m; j++) // j表示已经容纳物品的体积<=j
         {
             f[i][j] = f[i-1][j];
             if (j - v[i] >= 0)
                 f[i][j] = max(f[i][j], f[i-1][j-v[i]] + w[i]);
         }
 ​
     cout << f[n][m] << endl;
 ​
     return 0;
 }

优化方法(空间优化)

优化方式:f(i, j)的二维数组可以优化成一维数组 观察式子可以发现,第i行的值只依赖于第i-1行的数据,所以可以采用滚动数组的方式(通过一维数组更新存储数据)。 同时可以发现,第j列的值依赖于第j列和第j-vi列的数据,那么j可以从大到小遍历,这样由于滚动数组更新从大到小进行,所以遍历到j的时候,j-vi列的数据还是第i-1行时的数据。 优化后方程f[j] = max(f[j], f[j-v[i]] + w[i]);

 #include<iostream>
 ​
 using namespace std;
 ​
 const int N = 1010;
 int n, V;
 int v[N], w[N];
 int f[N];
 ​
 int main()
 {
     cin >> n >> V;
 ​
     for(int i = 1; i <= n; ++ i)
         cin >> v[i] >> w[i];
 ​
     for(int i = 1; i <= n; ++ i)
         for(int j = V; j >= 0; j--) // j表示已经容纳物品的体积<=j
         {
             f[j] = f[j];
             if (j - v[i] >= 0)
                 f[j] = max(f[j], f[j-v[i]] + w[i]);
         }
 ​
     cout << f[V] << endl;
 ​
     return 0;
 }

完全背包问题

3. 完全背包问题 - AcWing题库

  • 状态表示 - 化零为整

    • 集合 f(i, j)

      • 一般描述:所有只考虑前i个物品,且总体积不超过j的选法的集合
    • 属性:max,当前情况下背包内物品价值 所以集合f(i, j)划分为:

      1. 所有不选第i个物品的方案 -- 1到i-1物品,总体积不超过j。f(i, j) = f(i-1, j)
      2. 选择1个第i个物品的方案 -- 1到i物品,总体积不超过j。因为1到i-1已经不变了,所以关键是选出第i个使得值最大f(i, j) = f(i-1, j-vi) + wi
      3. 选择2个第i个物品的方案
      4. 选择3个第i个物品的方案
      5. ...
      6. 选择k个第i个物品的方案 即二维数组迭代公式2:
f(i,j)=max(f(i1,j),f(i1,jvi)+wi,f(i1,j2vi)+2wi),...,f(i1,jkvi)+kwi)f(i, j) = max (f(i-1, j), f(i-1, j-v_i) + w_i, f(i-1, j-2*v_i) + 2*w_i), ..., f(i-1, j-k*v_i) + k*w_i)
  • 状态计算 - 化整为零

    • 划分为若干个不重复、不遗漏的子集来求解

由于存在三重循环,估算可知,可能会TLE,所以需要优化时间。

优化时间

由二维数组迭代公式可以,当j = j - v时,可以得到公式3:

f(i,jvi)=max(f(i1,jvi),f(i1,j2vi)+wi),...,f(i1,j(k+1)vi)+kwi)f(i, j - v_i)=max(f(i-1, j-v_i), f(i-1, j-2*v_i) + w_i), ..., f(i-1, j-(k+1)*v_i) + k*w_i)

由于,由公式3得到j - k * v_i>0,所以f(i-1, j-(k+1)v_i) + kw_i最后一项不用考虑,因此这个公式变为:

f(i,jvi)=max(f(i1,jvi),f(i1,j2vi)+wi),...,f(i1,jkvi)+(k1)wi)f(i, j - v_i)=max(f(i-1, j-v_i), f(i-1, j-2*v_i) + w_i), ..., f(i-1, j-k*v_i) + (k-1)*w_i)

可以发现,他与公式2后半部分只差了一个w_i,所以可以优化公式3得到公式4:

f(i,j)=max(f(i1,j),f(i,jvi)+wi)f(i, j)=max(f(i-1, j), f(i, j - v_i) + w_i)

公式表面意思是:划分为两个子集,第一个子集不取第i个物品,第二个子集表示至少取一个第i个物品。

同时近似01背包问题,优化空间以后可以得到最终公式5:

f(j)=max(f(j),f(jvi)+wi)f(j) = max(f(j), f(j-v_i)+w_i)

不过,此时的j要从小到大遍历,因为第二项是f(i, j-v_i)即更新后的数。

 #include<iostream>
 ​
 using namespace std;
 ​
 const int N = 1010;
 int n, V;
 int v[N], w[N];
 int f[N];
 ​
 int main()
 {
     cin >> n >> V;
 ​
     for(int i = 1; i <= n; ++ i)
         cin >> v[i] >> w[i];
 ​
     for(int i = 1; i <= n; ++ i)
         for(int j = v[i]; j <= V; j++) // j表示已经容纳物品的体积<=j
         {
             f[j] = max(f[j], f[j- v[i]] + w[i]);
         }
 ​
     cout << f[V] << endl;
 ​
     return 0;
 }

石子合并问题(区间型DP)

282. 石子合并 - AcWing题库 题目分析:

  1. 由于所有中合并情况共计!n种方法,即是有限的(虽然很大),而问题求解的是最小代价。所以有限集的最值问题 —>DP
  2. 由于每次只能合并相邻的两堆石子,所以对于一个区间而言,最后一次合并一定是左右两个区间进行合并,DP要从这里入手。 DP:
  • 状态表示 - 化零为整(用一个集合包含非常多的情况)

    • 集合 f(i, j)

      • 一般描述:所有将[i, j]这个区间内的石子合并成一堆的方案的集合
    • 属性:min

  • 状态计算 - 化整为零(对集合中情况分为不同子集来计算)

    • 划分为若干个不重复、不遗漏的子集来求解。以分解点为划分依据:

    1. 最后合并的两堆:[i, i+1] [i+1, j]
    2. 最后合并的两堆:[i, i+2] [i+2, j]
    3. ...
    4. 最后合并的两堆:[i, j-1] [j-1, j] 即我们的任务是让最后合并的两堆分别求min,然后最后整体min即为其之和。

区间型DP问题: 一般来说,两个循环,第一层循环遍历区间长度,第二层循环从区间最左侧元素开始往后遍历。

 // https://www.acwing.com/problem/content/description/284/
 // 石子合并
 #include <climits>
 #include <iostream>
 ​
 using namespace std;
 ​
 const int N = 340;
 int s[N]; // 质量前缀和
 int f[N][N];
 ​
 int main()
 {
     int n;
 ​
     cin >> n;
     for (int i = 1; i <= n; ++i)
         cin >> s[i], s[i] += s[i - 1];
 ​
     for (int len = 2; len <= n; ++len)
     {
         for (int i = 1; i + len - 1 <= n; i++)
         {
             int j = i + len - 1;
             f[i][j] = INT_MAX;
             // 分界点 k
             for (int k = i; k < j; ++k)
             {
                 f[i][j] = min(f[i][j], f[i][k] + f[k + 1][j] + s[j] - s[i - 1]);
                 // s[i]为前i个元素质量之和   s[j] - s[i-1]可以得到合并 i到k 和 k+1到j 两堆的代价
             }
         }
     }
 ​
     cout << f[1][n] << endl;
 ​
     return 0;
 } 

最长公共子序列

剑指 Offer II 095. 最长公共子序列 - 力扣(LeetCode) (leetcode-cn.com) DP:

  • 状态表示 f(i, j)

    • 集合 所有A[1到i]与B[1到j]的公共子序列的集合

    1. A[i-1]和B[j-1]的最大公共子序列 不包含A的第i个元素和B的第j个元素情况

    2. A[i-1]和B[j]的最大公共子序列 不包含A的第i个元素

    3. A[i]和B[j-1]的最大公共子序列 不包含B的第j个元素

    4. A[i]和B[j]的一定包含A的第i个元素和B的第j个元素的最大公共子序列 if(A[i] ==B[j]) A[i-1]和B[j-1] + 1

    • 属性 max
  • 状态计算