简单dp 学习笔记
1. 背包
1.1 背包模型的概述
有 \(n\) 种物品,每种物品有若干个。拿一件物品付出 \(w_i\) 代价获得 \(v_i\) 价值,问最多花费 \(V\) 的代价能获得的最大价值。
1.2 0/1背包
考虑每种物品只有一个的情况。
我们设 \(f_{i,j}\) 表示前 \(i\) 个物品,花费了 \(j\) 的最大价值。
于是得出 dp 方程:\(f_{i,j}=\max(f_{i-1,j},f_{i-1.j-w_i}+v_i)\)。
我们发现,\(f_{i,j}\) 都是从 \(f_{i-1,j}\) 转移而来,所以考虑压掉 \(i\)。需要发现,在转移时要倒序转移,使得每个物体只被选一次。
for(int i=1; i<=n; i++)
for(int j=V; j>=w[i]; j--)
f[j]=max(f[j],f[j-w[i]]+v[i]);
1.3 完全背包问题
即每种物品有无数个。
考虑同样的 dp 转移,只是将倒序枚举改为正序,这样就变成选无限个了。
for(int i=1; i<=n; i++)
for(int j=w[i]; j>=V; j++)
f[j]=max(f[j],f[j-w[i]]+v[i]);
1.4 多重背包问题
给出每种物品有 \(s_i\) 个,怎么做?
一个最简单的想法是直接把 \(s_i\) 个看成不同的 \(s_i\) 种,但是这样的复杂度是 \(O(\sum s\times V)\) 的,不可接受。
考虑倍增,将物品拆成 \(2^0,2^1,2^2,\dots,2^{\log s}\) 个,复杂度变成了 \(O(\sum{\log s}\times V)\) 。
int cnt=0;
for(int i=1,v,w,s;i<=n;i++){
rd(w,v,s);
for(int j=1; j<=s; j<<=1) w[++cnt]=j*w,v[cnt]=j*v,s-=j;
if(s) w[++cnt]=s*w,v[cnt]=s*v;
}
for(int i=1; i<=cnt; i++)
for(int j=V; j>=w[i]; j--)
f[j]=max(f[j],f[j-w[i]]+v[i]);
1.5 “恰好为 V”
问能否从中选取若干件总价值恰好为 \(V\)。
\(f_{i,j}\) 表示前 \(i\) 件物品能否取到恰好 \(j\)。
转移同理。
1.6 有依赖的背包
考虑依赖关系,对子集进行讨论,枚举子树哪些被选。
1.7 背包的方案计数
考虑如果不需要最优解,直接将取 max 改为求和即可。
否则记 \(g_i\) 表示 \(f_i\) 最大时的方案数,看从哪里转移过来输出即可。
2. 区间 dp
区间 dp 经典方程式:\(f_{i,j}=\max(f_{i,k}+f_{k+1,j}+cost(i,j))\)。
断环为链,直接做。
3. 树形 dp
3.1 simple
设 \(f_{i,0/1}\) 表示选/不选 \(i\) 的最优情况。
于是可以知道 \(f_{i,0}=\max(f_{j,0},f_{j,1})+a_i,f_{i,1}=f_{j,0}+a_i\),其中 \(j\) 是 \(i\) 的儿子。
dfs 转移。
3.2 树上背包
建图,发现是一个森林。
我们设 \(f_{u,i,j}\) 表示以 \(u\) 号点为根的子树中,已经遍历了 \(u\) 号点的前 \(i\) 棵子树,选了 \(j\) 门课程的最大学分。
转移的过程结合了树形 DP 和 背包 的特点,我们枚举 \(u\) 点的每个子结点 \(v\),同时枚举以 \(v\) 为根的子树选了几门课程,将子树的结果合并到 \(u\) 上。
记点 \(x\) 的儿子个数为 \(s_x\),以 \(x\) 为根的子树大小为 \(\textit{siz_x}\),可以写出下面的状态转移方程:
\( f_{u,i,j}=\max_{v,k \leq j,k \leq \textit{siz_v}} f_{u,i-1,j-k}+f_{v,s_v,k} \)
注意上面状态转移方程中的几个限制条件,这些限制条件确保了一些无意义的状态不会被访问到。
\(f\) 的第二维可以很轻松地用滚动数组的方式省略掉,注意这时需要倒序枚举 \(j\) 的值。
可以证明,该做法的时间复杂度为 \(O(nm)\)。
3.3 换根 dp
换根 dp 通过预处理换根的贡献,使得仅用两次 dfs 可以处理一些问题。
通常在出现在无根树或者需要对每个点计算答案时考虑换根。
不妨令 \(u\) 为当前结点,\(v\) 为当前结点的子结点。首先需要用 \(s_i\) 来表示以 \(i\) 为根的子树中的结点个数。\(s_u=1+\sum s_v\)。
考虑计 \(f_u\) 为以 \(u\) 为根时,所有结点的深度之和。
考虑让根从 u 换到 v,这样 v 的子树中的点的 dep--, v 字数外的点的 dep++,可以知道 \(f_v=f_u+n-2\times s_v\)。
直接转移即可。
4.DAG 上 dp
考虑拓扑排序,从入度为 0 的点开始记忆化搜索,这样可以方便的计算答案。
5.状压 dp
5.1 朴素的状压 dp
如果遇到 \(n\) 较小的题目,考虑用二进制表示每个位置的状态。
考虑 P2704,记录当前有炮兵的位置,在计数时对其他的地方加上贡献即可。
5.2 一些常用的优化技巧:
两边 dp(与 meet in middle 同理)、阈值分治。
5.3 子集 dp
有时候需要枚举一个数的子集来转移给这个数。
但是如何快速的枚举子集的子集?
考虑这样一个东西:
int full=1<<n;
for(int s=0; s<full; s++)
for(int s1=s; s1; s1=(s1-1)&s)
......
考虑这个 s1 的意义:依次消去 s 的每一位,故为枚举子集。
这个东西用二项式定理证出来是 \(O(3^n)\) 的。