【算法-DP】DP
众所周知,DP就是毒品的意思
定义及作用
DP,动态优化,属于最恶心人的算法,但也是行之有效的。
一般会比贪心,DFS什么的快上几个 \(n\)。
讲完了。
DP 的应用条件
最优子结构,无后效性和子问题重叠。
最优子结构
注意:具有最优子结构的问题也可能适用贪心求解。
注意:要确保我们获取了最优解中用到的所有子问题。
- 求问题最优解的第一个组成部分是预处理;
- 对于一个给定问题,在其可能的第一步选择中,假定已经知道哪种选择才会得到最优解。现在并不需要证明,只是假定已经知道了这种选择;
- 给定可获得的最优解的选择后,确定这次选择会产生哪些子问题,以及如何最好地描述子问题空间;
- 证明作为构成原问题最优解的组成部分,每个子问题的解就是它本身的最优解。可以反证证明:考虑加入某个子问题的解不是其自身的最优解,那么就可以从原问题的解中用该子问题的最优解替换掉当前的非最优解,从而得到原问题的一个更优的解,从而与原问题最优解的假设矛盾。
要保持子问题空间尽量简单,只在必要时扩展。
最优子结构的不同体现在两个方面:
- 原问题的最优解中涉及多少个子问题;
- 确定最优解使用哪些子问题,需要求得多少种选择。
子问题图中每个定点对应一个子问题,而需要的选择对应关联至子问题顶点的边。
无后效性
已经求解的子问题,不会再受到后续决策的影响。
子问题重叠
如果有大量的重叠子问题,我们可以用空间将这些子问题的解存储下来,避免重复求解相同的子问题,从而提升效率。(就是记忆化)
线性 DP
线性 DP 属于比较友善的 DP 之一,
一般要求求出在某个时刻的最优解。
顾名思义,现象 DP 是指状态与题设线性相关的 DP。
维数也与题设相同。
这种 DP 一般是需要“某有序序列中若干前子问题的答案”。
例如斐波那契数列即是需要 \(f_{i-1}\) 和 \(f_{i-2}\)。
背包 DP
个人认为背包 DP 其实挺简单的。
一般是求将 \(n\) 个物品,放入容量为 \(v\) 的背包中,价值最大。
01 背包
for (int i = 1; i <= n; i ++) for (int j = v ; j >= w[i]; j --)
f[i][j] = max(f[i-1][j], f[i-1][j-w[i]] + v[i]);
\(f_{i, j}\) 表示在容量为 \(j\) 的背包中放入 \(i\) 件物品
外层循环:循环 \(n\) 次,挨个试第 \(i\) 个物品加不加
内层循环:循环 \(v\) 次,挨个试容量为 \(j\) 的情况下能不能使总价值更高
因为 \(j\) 小于 \(w_i\) 的情况下没有办法再放东西了,所以就不用考虑(也不能考虑,因为加了就不准了,除非特判)
其中的转移方程很重要:
分类讨论:
-
不加入第i个物品
背包的剩余容量不变,背包中物品的总价值不变,故此种情况的最大价值为 \(f_{i-1, j}\); -
加入第i个物品
背包的剩余容量会减小 \(w_i\),背包中物品的总价值会增大 \(v_i\),故此种情况的最大价值为 \(f_{i-1, i-w[i]}+v_i\)。
扩展:空间复杂度优化
上面方法的时间和空间复杂度均为 \(\Theta(N\times V)\),其中时间复杂度基本已经不能再优化了,但空间复杂度却可以优化到 \(\Theta(V)\)。
先考虑上面的基本思路。
如果只用一个一维数组 \(f\),能否保证循环结束后 \(f_j\) 表示的就是我们想要的答案 \(f_{i, j}\)?
是可以的。
思考:
\(f_{i, j}\) 是由 \(f_{i-1, j}\) 和 \(f_{i-1, j-c_i}\) 求得的,那么只需要保证在推 \(f_{i, j}\) 时能够得到 \(f_{i-1, v}\) 和 \(f_{i-1, j-c_i}\) 的值。
事实上,只需每次在进行容量模拟的循环中都以逆序推导,就可以保证求 \(f_j\) 时 \(f_{j-c_i}\) 保存的是状态 \(f_{i-1}\) 下的值。
for (int i = 1; i <= n; i ++) for (int j = v; j >= w[i]; j --)
f[j] = max(f[j], f[j-c[i]]+w[i]);