OI算法总结之动态规划总结(背包)

最近学了动态规划(Dynamic Programming, DP),先写一个背包总结。

概念

背包DP是DP的一种题型,分为01背包,完全背包,多重背包,混合背包。题面通常为给你一个限制(时间,体积,钱),再给一些物品,取一个物品需要代价 \(v\) ,但是会获得价值 \(w\) ,当然因题而异,代价的变量名不一定是 \(v\) ,而对应价值也不一定是 \(w\)

一个正确的DP必须满足三个条件:最优子结构,无后效性,子问题重叠。

最优子结构

最优子结构我个人认为是DP以及递推一个重要的部分

  1. 一个DP题要得出最优解的一个组成部分是做出一个抉择

  2. 对于一个给定的问题,在它所有的第一个抉择中,假设你已经知道哪个抉择才会得到最优解,但现在并不关心这个抉择怎么得到,仅仅是假设已经得到了这个抉择

  3. 确定可以得到能获得最优解的抉择后,再确定这次抉择会产生哪些子问题,以及怎么最优的刻画子问题空间

  4. 证明作为构成原问题最优解的组成部分,每个子问题的解就是它本身的最优解。方法是反证法,考虑加入某个子问题的解不是其自身的最优解,那么就可以从原问题的解中用该子问题的最优解替换掉当前的非最优解,从而得到原问题的一个更优的解,从而与原问题最优解的假设矛盾。

最优子结构的不同体现在两个方面:

  1. 原问题的最优解中涉及多少个子问题

  2. 确定最优解使用哪些子问题时,需要考察多少种选择。

这是来自OI-Wiki的解释,OI-wiki比较权威,一定是对的,但说的可能有些难懂,我不太敢说,怕说错了误导别人

无后效性,子问题重叠,还有以下的东西我说吧。

无后效性

已经得到最优解了的子问题,不会受到后面的抉择的影响。许多问题都是由于这条性质而致错的。

子问题重叠

如果会出现大量相同子问题,可以把每个子问题的结果存起来,避免重复求解相同子问题而浪费时间。

基本步骤

正常的DP问题分为3个步骤

  1. 将最终问题分为好多个阶段,对应各自的子问题,然后找到子问题的特征,后面称为状态

  2. 对于每一个状态,有好多个变化状态的抉择,它们会使状态变化,变为另一个状态,后面称为状态转移方程

  3. 按照顺序求解每一个子问题

01背包

01背包顾名思义,每个物品只能取一次,不取为0,取为1。

DP要注意DP是递推的进阶版,在看到一个问题时,可能认为它是一道贪心题,但实际上是DP,贪心与DP的区别在于贪心是当前利益最大化,而DP是全局利益最大化。

举个栗子:采药

这道题就是一个典型的01背包问题,题中的限制是时间,每采一株草药都会消耗时间,我们设每株草药所需时间为\(v_i\),价值为\(w_i\)

这题看起来很贪,但当你用贪心来想这题你会发现对于数据输入:

100 3
1 8
2 50
100 114514

来说明显最后一株是最优的,但贪心做法是能取则取,取不了就拉倒。

所以贪心结果为58

而正确答案为114514

综上,此题不能用贪心,改用DP

DP的做法是我们定义一个数组 \(f_{i,j}\)\(f_{i,j}\) 表示前\(i\)个物品剩余时间为\(j\)时的最大价值。

假设当前已经处理好了前 \(i-1\) 个物品的状态,那么对于第 \(i\) 个物品有两种情况,取与不取。

  1. 如果不取这个物品,那么背包剩余容量不变,即 \(f_{i,j}=f_{i-1,j}\)

  2. 如果这个物品,那么背包剩余容量要减掉物品所需代价 \(v_i\) ,当前价值要加上对应的价值 \(w_i\),即 \(f_{i,j}=f_{i-1,j-v_i}+w_i\)

如果要保证最大价值就需要两种情况取最大值

由此可得

\[f_{i,j}=\max{(f_{i-1,j},f_{i-1,j-v_i}+w_i)} \]

我们把这个式子叫做状态转移方程

最后的的答案就是\(f_{M,T}\)

但部分题目中 \(n\)\(m\) 偏大,会导致MLE,故此,我们还需要优化一下我们的 \(f\) 数组。

为了压缩\(f\)数组,我们可以采用滚动数组的方式来减少空间。

不难发现,对于 \(f_i\) 来说,有影响的只有 \(f_{i-1}\) 因此可以直接压掉第一维直接用 \(f_j\) 来处理到当前物品时背包容量为 \(j\) 的最大价值。

换句话说,可以直接把第一维的处理交给最外层循环,因为在整个转移方程中第一维只有 \(i-1\)\(i\) 两种情况,而他们的第二维已经分辨了他们的区别,所以有没有第一维都一样的。

最后我们得到的状态转移方程如下:

\[f_j=\max{(f_j,f_{j-v_i}+w_i)} \]

代码上就是无脑去掉第一维就可以了,别忘了最后输出要输出 \(f_T\)

这个方法被称为动态规划,可以理解为递推的进阶版。

建议大家记住这个状态转移方程,因为大部分背包问题的转移方程都是从这个方程变化得到的

如果你按照以上思路写出代码然后提交上去却发现并未AC,我可以给出一个原因,但这个原因并不一定是你的错误原因。

如果你的第二维循环是正向的,那么请注意,在 \(j \ge v_i\) 时,\(f_{i,j}\) 会被 \(f_{i,j-v_i}\) 所影响,直白点说就是变成了一个物品可以取好多次,这明显是错误的,但这正是后面完全背包的写法。

所以应该把第二维循环改为反向,从背包容量枚举到 \(v_i\) ,这样 \(f_{i,j}\) 会被更早的更新, \(f_{i,j-v_i}\) 就影响不到了。

完全背包

与01背包不同的,01背包里的物品最多取一次,而完全背包可以取无限次。

完全背包的例题是与01背包相似的:例题

状态自然是和01背包相同的。

来看看二维dp数组写法,最最正常朴素的写法,我们可以通过每个物品选了多少个来转移。

得到如下的状态转移方程

\[f_{i,j}=\max\limits_{k=0}^{+\infty}(f_{i-1,j},f_{i-1,j-k\times v_i}+w_i\times k) \]

额,看起来好复杂对吧

那可以分析一下,发现对于 \(f_{i,j}\) ,只有 \(f_{i,j-v_i}\) 对他有影响。

而且 \(f_{i,j-v_i}\)\(f_{i,j-2 \times v_i}\) 更新过了,所以 \(f_{i,j-w_i}\) 一定是对其状态来说的最优解。

那么方程优化为

\[f_{i,j}=\max{(f_{i-1,j},f_{i,j-v_i}+w_i)} \]

这就是二维的完全背包,一维没什么好说的,跟01背包一样,无脑去掉第一维就是了。

哦,记住第二维循环一定要是正向的,不知道为啥的去看01背包错误原因。

多重背包

想必大家都能猜到,多重背包里的物品可以被取特定次。我们将这个次数定为 \(k_i\)

有了01背包和完全背包的经验,我们可以将物品取 \(k_i\) 次等量的转化为有 \(k_i\) 个相同的物品。

这不就直接转化为了普通01背包了嘛,强调一下,别搞混了,我们这里价值是 \(w_i\) ,代价是 \(v_i\) ,状态转移方程如下

\[f_{i,j}=\max\limits_{k=0}^{k_i}(f_{i-1,j},f_{i-1,j-k\times v_i}+w_i\times k) \]

翻一下上面完全背包的第一个方程,我们发现区别只有 max 上面的那个东西表示什么很容易想到,即物品可取的次数,完全背包肯定是 \(\infty\) 嘛,多重背包的次数就是 &k_i& 对吧。

max上下的两个东西就是第三层循环,因为每个物品都可以取 \(k_i\) 次嘛,按照01背包来说每个物品就相当于多了相同的 \(k_i-1\) 个 所以要多一层循环。

混合背包

混合背包就是把以上3种背包合起来了。

有的物品可以取1次,有的物品可以取无限次,有的物品可以取 \(k_i\) 次。

看起来很恐怖,但实际上主要判断一下这个物品在哪个种类然后按照不同的方式求最大利益就好了。

总结

考的较多的背包就在这里啦,当然还有其他类型背包,比如二维费用背包,分组背包,还会有一些小优化,例如二进制优化,但是还是不离开这几个背包,自己想想就能想明白。

\[\LARGE{The}~\LARGE{End} \]

posted @ 2025-04-24 13:09  Fools_Sparkle  阅读(96)  评论(2)    收藏  举报