动态规划入门与线性 dp
引入
动态规划(Dynamic Programming,DP),是一种将原问题分为一些子问题,通过局部最优解推出全局最优解。
一般来说,做一道 dp 题有 \(4\) 个步骤:
- 设计 dp 状态:根据几个关键信息定下状态和最优化属性。
- 定下拓扑序。
- 设计状态转移方程。
- 确定初始状态、目标状态和边界条件。
基本要求
dp 的基本要求有三个:
- 最优子结构性质:即你需要保证当前状态的最优结果都是由子问题的最优结果转移而来。
- 无后效性:即当前的选择在你的状态设计下不会影响后续的决策。
- 子问题重叠:当有大量子问题时,需要把这些子问题的结果记录下来,防止多次求解导致效率低下。
dp 的分类
大致可分为如下几类:
- 序列 dp
- 背包 dp
- 区间 dp
- DAG 上 dp
- 树形 dp
- 状压 dp
- 概率 dp
- 还有些不会的,见 OI-wiki。
其中序列、背包、区间、概率 dp 都是线性 dp。
还有一些其他类型的 dp,比较有意思。
线性 dp
线性DP是动态规划问题中的一类问题,指状态之间有线性关系的动态规划问题。
虽然是线性 dp,但部分时间复杂度并不是纯线性的,因为线性 dp 的时间复杂度并不一定是线性的。
其他类型 dp:P1216 数字三角形
如果只是单纯的去考虑走最大数,你就会陷入坑中,考虑动态规划。
不要去看原图,只用看读入的数据,这样不抽象。
令 \(a_{i,j}\) 表示第 \(i\) 行第 \(j\) 个数,那么从 \((i,j)\) 就可以移动到 \((i+1,j)\) 或 \((i+1,j+1)\)。
- 状态:\(dp_{i,j}\) 表示从 \((i,j)\) 往下走到第 \(n\) 行最高得分。
- 转移:\(dp_{i,j}=\max(dp_{i+1,j},dp_{i+1,j+1})+a_{i,j}\)。
- 拓扑序:由于 \(i\) 由 \(i+1\) 转移而来,拓扑序就是 \(i\) 从大到小。
- 初始状态:对于 \(1\leqslant i\leqslant n\),\(dp_{n,i}=a_{n,i}\)。
- 目标状态:\(dp_{1,1}\)。
时间复杂度:\(O(n^2)\),空间复杂度:\(O(n^2)\)。
序列 dp:最长上升子序列 LIS
P1020 导弹拦截 tips:这题求的两个分别是最长不上升子序列和最长上升子序列,详细证明自己搜。
B3637 最长上升子序列 tips:模板题,数据范围很小,\(O(n^2)\) 可过。
给定一个长度为 \(n\) 的序列 \(a\),求它最长上升子序列长度。
数据范围:\(1\leqslant n \leqslant 5000, 1\leqslant a_i \leqslant 10^9(1\leqslant i \leqslant n)\)。
令 \(a_0=0\)。
- 状态:\(dp_i\) 表示以第 \(i\) 个元素结尾的最长上升子序列长度。
- 转移:\(dp_i=\max\limits_{0\leqslant j < i \&\& a_j < a_i}\{dp_j\}+1\)。
- 拓扑序:由于 \(dp_i\) 只由 \(0\sim i-1\) 转移而来,拓扑序就是 \(i\) 从小到大。
- 初始状态:\(dp_0=0\)。
- 目标状态:\(\max\limits_{1\leqslant i \leqslant n}\{dp_i\}\)。
时间复杂度:\(O(n^2)\),空间复杂度:\(O(n)\)。
在这个数据范围中,\(O(n^2)\) 可过,但如果 \(1\leqslant n \leqslant 10^5\) 怎么办呢?
这也好办,只要写个离散化+树状数组/线段树优化就行了。
tips:其实这个可以贪心+二分,但不属于 dp,不谈。
时间复杂度:\(O(n\log n)\),空间复杂度:\(O(n)\)。
序列 dp:最长公共子序列 LCS
P1439 【模板】最长公共子序列 tips:本题表面上求的是 LCS,实际为 LIS,但 50pts 暴力可以 \(O(n^2)\) LCS 求。
给出一个长度为 \(n\) 的序列 \(a\),还有一个长度为 \(m\) 的序列 \(b\),求 \(a\) 和 \(b\) 的最长公共子序列长度。
数据范围:\(1\leqslant n,m \leqslant 5000,1\leqslant a_i,b_j \leqslant 10^9(1\leqslant i \leqslant n,1\leqslant j \leqslant m)\)。
- 状态:\(dp_{i,j}\) 表示 \(a\) 的前 \(i\) 个元素和 \(b\) 的前 \(j\) 个元素的最长公共子序列长度。
- 转移:\(dp_{i,j}=\begin{cases}\max(dp_{i-1,j},dp_{i,j-1}) & a_i \ne b_j\\ dp_{i-1,j-1}+1 & a_i=b_j \end{cases}\)。
- 拓扑序:\(i\) 从小到大,\(i\) 相同则 \(j\) 从小到大。
- 初始状态:对于 \(1\leqslant i \leqslant n,dp_{i,0}=0\),对于 \(1\leqslant i \leqslant m,dp_{0,i}=0\),\(dp_{0,0}=0\)。
- 目标状态:\(dp_{n,m}\)。
时间复杂度:\(O(n \times m)\),空间复杂度:\(O(n \times m)\)。
关于 P1439:由于给定的是两个 \(1\sim n\) 的序列,所以对于两个数 \(a_i,a_j(i<j)\),如果它们可以在两序列的公共子序列中,那么必然在 \(b\) 序列中的 \(a_i\) 的位置小于 \(a_j\) 的位置,令 \(c_i\) 为 \(b\) 序列中 \(a_i\) 的位置,答案就是 \(c\) 的最长上升子序列长度,用树状数组优化即可。
时间复杂度:\(O(n \log n)\),空间复杂度:\(O(n)\)。
背包 dp:01 背包
P1048 采药 模板题。
01 背包的模型就是给出 \(n\) 个物品的占用空间 \(a_i\) 和价值 \(b_i\),对于每个物品,都可以选或者不选,问在给定的背包容量 \(m\) 下最大获得的价值和。
- 状态:\(dp_{i,j}\) 表示考虑完前 \(i\) 个物品(草药),耗费的背包容量(时间)为 \(j\) 所获得的最大价值和。
- 转移:\(dp_{i,j}=\begin{cases} dp_{i-1,j}& 1\leqslant j < a_i\\ \max(dp_{i-1,j},dp_{i-1,j-a_i}+b_i) & a_i \leqslant j \leqslant m\end{cases}\)
- 拓扑序:\(i\) 从小到大。
- 初始状态:\(dp_{0,0} = 0\)。
- 目标状态:\(dp_{n,m}\)。
时间复杂度:\(O(n \times m)\),空间复杂度:\(O(n \times m)\)。
背包 dp:多重背包
U280382 多重背包问题 别人造的例题。
多重背包的模型就是给出 \(n\) 个物品的占用空间 \(a_i\)、价值 \(b_i\) 和选择的个数 \(c_i\),对于每个物品,都可以选 \(0\sim c_i\),问在给定的背包容量 \(m\) 下最大获得的价值和。
- 状态:\(dp_{i,j}\) 表示考虑完前 \(i\) 个物品,耗费的背包容量为 \(j\) 所获得的最大价值和。
- 转移:分为两种解法,见下方。
- 拓扑序:\(i\) 从小到大。
- 初始状态:\(dp_{0,0}=0\)。
- 目标状态:\(dp_{n,m}\)。
solution 1
转移方法:暴力。
\(dp_{i,j}=\max\limits_{0\leqslant k \leqslant c_i \&\& j \geqslant k \times a_i}\{ dp_{i-1,j-k\times a_i}+k\times b_i\}\)。
时间复杂度:\(O(m \times \sum\limits_{1\leqslant i \leqslant n} c_i)\),空间复杂度:\(O(n\times m)\)。
如果 \(\sum\limits_{1\leqslant i \leqslant n} c_i\) 比较大则完全接受不了,那么就需要考虑 solution 2。
solution 2
转移方法:拆分 \(c_i\) 转 01 背包。
众所周知,如果你有了 \(1,2,4,8 \cdots 2^k\),那么你可以用每个元素不超过 \(1\) 次凑出来 \(0\sim 2^{k+1}-1\) 中所有整数,那么考虑把 \(c_i\) 尽量拆成 \(1,2,4\cdots\),先拆一遍,最后剩下一部分单独考虑,这下就可以凑出来 \(0\sim c_i\) 中的所有整数了。
01 背包转移见上方。
时间复杂度:\(O(m\times (n + \sum\limits_{1\leqslant i \leqslant n}\log c_i))\),空间复杂度:\(O(n\times m)\)。
背包 dp:完全背包
P1616 疯狂的采药 模板题。
个人感觉比多重背包简单。
完全背包的模型就是给出 \(n\) 个物品的占用空间 \(a_i\)、价值 \(b_i\),对于每个物品,都可以选无限多个,问在给定的背包容量 \(m\) 下最大获得的价值和。
- 状态:\(dp_{i,j}\) 表示考虑完前 \(i\) 个物品,耗费的背包容量为 \(j\) 所获得的最大价值和。
- 转移:\(dp_{i,j}=\begin{cases} dp_{i-1,j}& 1\leqslant j \leqslant a_i\\ dp_{i,j}=\max(dp_{i-1,j},dp_{i,j-a_i}+b_i) & a_i \leqslant j \leqslant m\end{cases}\)。
- 拓扑序:\(i\) 从小到大,\(i\) 相同时 \(j\) 从小到大。
- 初始状态:\(dp_{0,0}=0\)。
- 目标状态:\(dp_{n,m}\)。
解释一下转移,由于每个物品可以无限选择,那么直接调用 \(dp_{i,j-a_i}\) 相当于再选一个,而之前并没限定选择几个,这就完美地处理了无限个物品。
时间复杂度:\(O(n \times m)\),空间复杂度:\(O(n \times m)\)。
背包 dp:分组背包
P1757 通天之分组背包 模板题。
其实和 01 背包差不多,只要把它门分个组存储,每个组选一个处理 01 背包即可。
区间 dp
P1775 石子合并(弱化版) 敲门砖,模板题。
P1880 石子合并 标准版,模板题。
区间 dp,维护的信息为一段区间的最优化属性。
在石子合并弱化版中,为了让代价尽量小,每一段区间的代价都要尽可能小。
- 状态:\(dp_{l,r}\) 表示将 \([l,r]\) 合并成一堆石子的最小代价。
- 转移:\(dp_{l,r}=\min\limits_{l\leqslant i < r}\{dp_{l,i}+dp_{i+1,r}\}+\sum\limits_{l\leqslant i \leqslant r}m_i\)。
- 拓扑序:\(r-l\) 从小到大。
- 初始状态:对于每个 \(1\leqslant i \leqslant n\),\(dp_{i,i}=0\)。
- 目标状态:\(dp_{1,n}\)。
时间复杂度:\(O(n^3)\),空间复杂度:\(O(n^2)\)。
石子合并的标准版中,则是将一排变成了一个环,本质没区别,只要一个环形题的常见优化套路:断环成链即可。
时间复杂度:\(O(8\times n^3)\),空间复杂度:\(O(4\times n^2)\)。
dp 优化
滚动数组
通过数组的滚动来实现空间优化的目的,在 \(dp_{i,\cdots}\) 只和 \(dp_{i-1,\cdots}\) 有关时,通常可以滚动成两个数组,方便处理。
当然还有一个叫做自我滚动的东西,只要注意好拓扑序是否满足只与 \(i\) 有关,以及自我滚动后的拓扑序即可。
记忆化搜索
并不是传统意义上的优化,但它能让你的代码逻辑更加清晰。
就是在搜索时记录当前情况下的最优解,当下一次访问时直接返回结果即可。
缺点就是递归常数大一些。