Mind and Hand Help

Dynamic Programming

记忆化搜索

记忆化搜索是一种通过记录已经遍历过的状态的信息,从而避免对同一状态重复遍历的搜索实现方式。

因为记忆化搜索确保了每个状态只访问一次,它也是一种常见的动态规划实现方式。

背包DP

引入

个物品和一个容量为 的背包,每个物品有重量 和价值 两种属性,要求选若干物品放入背包使背包中物品的总价值最大且背包中物品的总重量不超过背包的容量。

由于每个物体只有两种可能的状态(取与不取),对应二进制中的 0 和 1,这类问题便被称为「0-1 背包问题」。

0-1背包

解释

例题中已知条件有第 个物品的重量 ,价值 ,以及背包的总容量

设:DP 状态 为在只能放前 个物品的情况下,容量为 的背包所能达到的最大总价值

考虑转移。 假设当前已经处理好了前 个物品的所有状态,那么对于第 个物品,

  • 当其不放入背包时,背包的剩余容量不变,背包中物品的总价值也不变,故这种情况的最大价值为

  • 当其放入背包时,背包的剩余容量会减小 ,背包中物品的总价值会增大 ,故这种情况的最大价值为

由此可以得出状态转移方程:

由于对 有影响的只有 ,可以去掉第一维,直接用 来表示处理到当前物品时背包容量为 的最大价值,得出以下方程:

务必牢记并理解这个转移方程,因为大部分背包问题的转移方程都是在此基础上推导出来的

完全背包

解释

完全背包模型与 0-1 背包类似,与 0-1 背包的区别仅在于一个物品可以选取无限次,而非仅能选取一次。

我们可以借鉴 0-1 背包的思路,进行状态定义:设 为只能选前 个物品时,容量为 的背包可以达到的最大价值。

需要注意的是,虽然定义与 0-1 背包类似,但是其状态转移方程与 0-1 背包并不相同。

过程

可以考虑一个朴素的做法:对于第 件物品,枚举其选了多少个来转移。这样做的时间复杂度是 的。

状态转移方程如下

考虑做一个简单的优化。可以发现,对于 ,只要通过 转移就可以了。因此状态转移方程为:

与 0-1 背包相同,我们可以将第一维去掉来优化空间复杂度。如果理解了 0-1 背包的优化方式,就不难明白压缩后的循环是正向的(也就是上文中提到的错误优化)。

多重背包

多重背包也是 0-1 背包的一个变式。与 0-1 背包的区别在于每种物品有 个,而非一个。

一个很朴素的想法就是:把「每种物品选 次」等价转换为「有 个相同的物品,每个物品选一次」。这样就转换成了一个 0-1 背包模型,套用上文所述的方法就可已解决。状态转移方程如下:

时间复杂度

混合背包

混合背包就是将前面三种的背包问题混合起来,有的只能取一次,有的能取无限次,有的只能取 次。

这种题目看起来很吓人,可是只要领悟了前面几种背包的中心思想,并将其合并在一起就可以了。下面给出伪代码:

for (循环物品种类) { if (是 0 - 1 背包) 套用 0 - 1 背包代码; else if (是完全背包) 套用完全背包代码; else if (是多重背包) 套用多重背包代码; }

二维费用背包

个任务需要完成,完成第 个任务需要花费 分钟,产生 元的开支。

现在有 分钟时间, 元钱来处理这些任务,求最多能完成多少任务。

这道题是很明显的 0-1 背包问题,可是不同的是选一个物品会消耗两种价值(经费、时间),只需在状态中增加一维存放第二种价值即可。

这时候就要注意,再开一维存放物品编号就不合适了,因为容易 MLE。

分组背包

件物品和一个大小为 的背包,第 个物品的价值为 ,体积为 。同时,每个物品属于一个组,同组内最多只能选择一个物品。求背包能装载物品的最大总价值。

这种题怎么想呢?其实是从「在所有物品中选择一件」变成了「从当前组中选择一件」,于是就对每一组进行一次 0-1 背包就可以了。

再说一说如何进行存储。我们可以将 表示第 组的第 件物品的编号是多少,再用 表示第 组物品有多少个。

有依赖的背包

小明有 元钱,想要买 个物品,第 件物品的价格为 ,重要度为 。有些物品是从属于某个主件物品的附件,要买这个物品,必须购买它的主件。

目标是让所有购买的物品的 之和最大。

考虑分类讨论。对于一个主件和它的若干附件,有以下几种可能:只买主件,买主件 + 某些附件。因为这几种可能性只能选一种,所以可以将这看成分组背包。

如果是多叉树的集合,则要先算子节点的集合,最后算父节点的集合。

泛化物品背包

这种背包,没有固定的费用和价值,它的价值是随着分配给它的费用而定。在背包容量为 的背包问题中,当分配给它的费用为 时,能得到的价值就是 。这时,将固定的价值换成函数的引用即可。

小优化

根据贪心原理,当费用相同时,只需保留价值最高的;当价值一定时,只需保留费用最低的;当有两件物品 的价值大于 的价值并且 的费用小于 的费用时,只需保留

背包问题变种

输出方案

输出方案其实就是记录下来背包中的某一个状态是怎么推出来的。我们可以用 表示第 件物品占用空间为 的时候是否选择了此物品。然后在转移时记录是选用了哪一种策略(选或不选)。输出时的伪代码:

int v = V; // 记录当前的存储空间 // 因为最后一件物品存储的是最终状态,所以从最后一件物品进行循环 for (从最后一件循环至第一件) { if (g[i][v]) { 选了第 i 项物品; v -= 第 i 项物品的重量; } else { 未选第 i 项物品; } }

求方案树

对于给定的一个背包容量、物品费用、其他关系等的问题,求装到一定容量的方案总数。

这种问题就是把求最大值换成求和即可。

例如 0-1 背包问题的转移方程就变成了:

初始条件:

因为当容量为 时也有一个方案,即什么都不装。

求最优方案总数

要求最优方案总数,我们要对 0-1 背包里的 数组的定义稍作修改,DP 状态 为在只能放前 个物品的情况下,容量为 的背包「正好装满」所能达到的最大总价值。

这样修改之后,每一种 DP 状态都可以用一个 来表示方案数。

表示只考虑前 个物品时背包体积「正好」是 时的最大价值。

表示只考虑前 个物品时背包体积「正好」是 时的方案数。

转移方程:

如果 说明我们此时不选择把物品放入背包更优,方案数由 转移过来,

如果 说明我们此时选择把物品放入背包更优,方案数由 转移过来,

如果 说明放入或不放入都能取得最优解,方案数由 转移过来。

初始条件:

memset(f, 0x3f3f, sizeof(f)); // 避免没有装满而进行了转移 f[0] = 0; g[0] = 1; // 什么都不装是一种方案

因为背包体积最大值有可能装不满,所以最优解不一定是

最后我们通过找到最优解的价值,把 数组里取到最优解的所有方案数相加即可。

背包的第 k 优解

普通的 0-1 背包是要求最优解,在普通的背包 DP 方法上稍作改动,增加一维用于记录当前状态下的前 k 优解,即可得到求 0-1 背包第 优解的算法。

具体来讲: 记录了前 个物品中,选择的物品总体积为 时,能够得到的第 大的价值和。这个状态可以理解为将普通 0-1 背包只用记录一个数据的 扩展为记录一个有序的优解序列。

转移时,普通背包最优解的求法是 , 现在我们则是要合并 这两个大小为 的递减序列, 并保留合并后前 大的价值记在 里,这一步利用双指针法,复杂度是 的,

整体时间复杂度为 。空间上,此方法与普通背包一样可以压缩掉第一维,复杂度是 的。

27 January 2026