动态规划进阶 学习笔记

· · 个人记录

贴一篇很好的解题技巧分享博客:https://www.cnblogs.com/C202044zxy/p/15126199.html

0.DP 概述

动态规划具体是什么不多赘述。

面对一道动态规划的问题,我们要考虑的无非以下几点:转移对象,转移顺序,转移边界 \dots

其中有很多设计状态,转移方程推理,优化复杂度的技巧,本笔记将一一展开。

关于转移方程,最简单无脑的办法就是假设我有无穷大的空间,开一个很多很多维的数组写转移方程,再考虑降维优化。

1.背包问题进阶

1.1.基础背包问题回顾

之前已经接触过各种背包问题,如 0-1 背包,完全背包等。

接下来给出这些基本模型的转移方程,具体不多赘述。

1.1.1:0-1 背包

通过滚动数组优化空间复杂度:$f[v]=\max\{f[v],f[v-c[i]]+w[i]\}$. 还有一种优化技巧,由于 $f[i]$ 的值仅由 $f[i-1]$ 决定,因此,可以把第一维压缩成 $2$ 个单位,转移如下: $f[i\%2][v]=max\{f[(i+1)\%2][v],f[(i+1)\%2][v-c[i]]+w[i]\}$. 由于直接 $\bmod$ 运算常数较大,所以可以用位运算优化: $f[i \oplus 1][v]=max\{f[(i+1) \oplus 1][v],f[(i+1)\oplus 1][v-c[i]]+w[i]\}$. 看着比滚动数组傻逼一点,但是这个技巧还是实用的。 举一个例子:[P2679 [NOIP 2015 提高组] 子串](https://www.luogu.com.cn/problem/P2679)。 ~~虽然这题好像根 0-1 背包没什么关系。~~ #### 1.1.2:完全背包 和 0-1 背包只有转移顺序不同,伪代码如下: ```cpp for i=1..N for v=cost..V f[v]=max{f[v],f[v-cost]+weight} ``` #### 1.1.3:多重背包 使用二进制拆分算法,代码如下: ```cpp for (int i = 1; i <= n; i++) { for (int weight = W; weight >= w[i]; weight--) { // 多遍历一层物品数量 for (int k = 1; k * w[i] <= weight && k <= cnt[i]; k++) { dp[weight] = max(dp[weight], dp[weight - k * w[i]] + k * v[i]); } } } ``` ### 1.2.进阶背包问题 #### 1.2.1:混合背包 这类问题的特点是结合了以上三类基础背包问题。 这类问题也不算复杂,就是结合了一下三类背包的,代码也只需要特殊判断是哪一类的物品并且使用对应的方程即可。 伪代码如下: ``` for (循环物品种类) { 是 0 - 1 背包 ? 套用 0 - 1 背包代码; : 是完全背包 ? 套用完全背包代码; : 是多重背包 ? 套用多重背包代码; } ``` 以下是 AC 代码: ```cpp #include <bits/stdc++.h> using namespace std; const int maxn = 310; int vm,n,dp[maxn+100],w[maxn+100],p[maxn+100],v[maxn+100]; int main(){ ios::sync_with_stdio(0);cin.tie(0);cout.tie(0); cin >> vm >> n; for(int i = 1;i <= n;i++) cin >> w[i] >> v[i] >> p[i]; for(int i = 1;i <= n;i++) if(p[i] == 0) for(int j = w[i];j <= vm;j++) dp[j] = max(dp[j],dp[j-w[i]]+v[i]); else for(int j = 1;j <= p[i];j++) for(int k = vm;k >= w[i];k--) dp[k] = max(dp[k],dp[k-w[i]]+v[i]); cout << dp[vm]; return 0; } ``` #### 1.2.2:分组背包 这个问题中,物品被划分为若干组,每组中的物品互相冲突,最多选一件。求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。 相比 0-1 背包,这题其实是从「在所有物品中选择一件」变成了「从当前组中选择一件」,于是就对每一组进行一次 0-1 背包就可以了。 再说一说如何进行存储。我们可以将 $tt_{k,i}$ 表示第 k 组的第 $i$ 件物品的编号是多少,再用 $\mathit{cnt}_k$ 表示第 $k$ 组物品有多少个。 AC 代码如下: ```cpp #include <bits/stdc++.h> using namespace std; const int maxn = 205; int vm,n,t,v[maxn+100],c[maxn+100],p,cnt[maxn+100],tt[maxn+10][maxn+10],dp[maxn+10]; int main(){ memset(cnt,0,sizeof(cnt)); ios::sync_with_stdio(0);cin.tie(0);cout.tie(0); cin >> vm >> n >> t; for(int i = 1,p;i <= n;i++) {cin >> v[i] >> c[i] >> p; cnt[p]++,tt[p][cnt[p]] = i;} //本题精髓所在 for(int k = 1;k <= t;k++) //循环每一组 for(int i = vm;i >= 0;i--) //循环背包容量 for(int j = 1;j <= cnt[k];j++) //循环每一个物品 if(i >= v[tt[k][j]]) //如果容量充足 dp[i] = max(dp[i],dp[i - v[tt[k][j]]] + c[tt[k][j]]); //转移 cout << dp[vm]; return 0; } ``` ## 2.常见背包问题优化 ### 1.二进制优化 一个例子:POI 2005 Bank Notes 都是提高组的选手了,您一定知道,一个数一定可以拆分为若干个 $2$ 的整数次幂相加。 具体地说就是令 $A_{i,j}\left(j\in\left[0,\lfloor \log_2(k_i+1)\rfloor-1\right]\right)$ 分别表示由 $2^{j}$ 个单个物品「捆绑」而成的大物品。特殊地,若 $k_i+1$ 不是 $2$ 的整数次幂,则需要在最后添加一个由 $k_i-2^{\lfloor \log_2(k_i+1)\rfloor-1}$ 个单个物品「捆绑」而成的大物品用于补足。 举几个例子: $$6=1+2+3$$ $$8=1+2+4+1$$ $$18=1+2+4+8+3$$ $$31=1+2+4+8+16$$ 显然,通过上述拆分方式,可以表示任意 $\le k_i$ 个物品的等效选择方式。将每种物品按照上述方式拆分后,使用 $0-1$ 背包的方法解决即可。 ### 2.四边形不等式优化 如果对于任意的 $a1 \le a2 < b1 \le b2$,有$m[a1,b1]+m[a2,b2] \le m[a1,b2]+m[a2,b1]$,那么m[i,j]满足四边形不等式。 ### 3.斜率优化 ### 4.单调队列优化