背包DP
问题描述
基本背包问题
背包问题(Knapsack Problem)描述的是有容量约束的情况下,求考虑了所有物品(大小价值各异)之后的最优解或解的个数。[背包问题九讲]
首先来总结,如果我们要求的是背包中能装的最大价值。我们用以下变量来表示:
vi :物品i 的大小
wi :物品 i 的价值
V: 背包总容量
f[j] 表示容量为 j 的背包的答案,最后返回答案 f[V]。
两种基本的背包问题:
0-1背包:每个物品只能拿一次
完全背包:每个物品能拿无限次
背包方法
转移方程(空间压缩后)
容量j的遍历顺序
0-1背包
f[j]=maxi(f[j−vi]+wi)
j 倒序 V to vi
完全背包
f[j]=maxi(f[j−vi]+wi)
j 正序 vi to V
# 0-1背包:因为利用到本行信息,倒序保证安全更新
for n in nums:
for j in range(V, n-1, -1):
# 完全背包:因为利用到上一行信息,正序保证及时更新
for n in nums:
for j in range(n, V+1):在循环中是业务逻辑,有时候我们不是求最大最小值,而是求可行性或者方案数。
其中0-1背包和完全背包的转移方程是一样的,唯一区别是容量变量 j 的遍历方向。
遍历方向解释
能够这样做的原因是因为转移方程依赖哪一行的结果,之后我们看了二维(空间压缩前的转移方程)就明白了,图示如下:

0-1背包:因为利用到本行信息,倒序保证安全更新
完全背包:因为利用到上一行信息,正序保证及时更新
乍一看背包问题有可能觉得是用DFS求解,每次考虑是否装入某个物品。虽然用了记忆化优化后也能有效求解,但我觉得用背包DP求解会更容易理解。
初始化 f 有两种情况:
求背包中能装的最大价值,初始f[0,...,V]=0
求装满背包的最大价值,初始f[0]=0,f[1,...,V]=−∞
或者可行性解决初始化: f[0]=True,f[1,...,V]=False
可以这么考虑:考虑了0个物品后,什么都不装 = 装满容量为0的背包
延伸背包问题
多维费用背包:如果背包有多种容量限制(比如长和宽),每个物品有多个维度体积,那么可以“费用“这个维度可以扩展开来。如果每个物品只能拿一次,维度=2,那么就是二维费用0-1背包问题,
分组背包:如果所有物品被划分成 K 组,每组最多只能拿一个物品
背包方法
转移方程(空间压缩后)
容量j的遍历顺序
二维0-1背包
f[j][k]=maxi(f[j−vi][k−ui]+wi)
j 倒序 V to vik 倒序 V to ui
分组背包
f[j]=max(f[j−vi]+wi)∣itemi∈group k
k 正序 1 to K
j 倒序 V to 0
例题
0-1背包
空间压缩前的转移方程:
f[i][j]={不拿物品i拿物品i::f[i−1][j]f[i−1][j−vi]+wi
目标和:转化问题以后为0-1背包方案数问题。每个物品只能拿一次,有几种装满背包的方案?
分割等和子集:转化后为0-1背包可行性问题。每个物品只能拿一次,是否能正好装满背包。
最后一块石头的重量 II:转化后为0-1背包最小值问题。
有一堆石头,每块石头的重量都是正整数。
每一回合,从中选出任意两块石头,然后将它们一起粉碎。假设石头的重量分别为 x 和 y,且 x <= y。那么粉碎的可能结果如下:
如果 x == y,那么两块石头都会被完全粉碎;
如果 x != y,那么重量为 x 的石头将会完全粉碎,而重量为 y 的石头新重量为 y-x。
最后,最多只会剩下一块石头。返回此石头最小的可能重量。如果没有石头剩下,就返回 0。
完全背包
空间压缩前的转移方程:唯一的区别在于拿了物品 i 以后不急着增加 i,因为你还有机会拿i。
f[i][j]={不拿物品i拿物品i::f[i−1][j]f[i][j−vi]+wi
二维费用0-1背包
f[i][j][k]={不拿物品i拿物品i::f[i−1][j][k]f[i−1][j−vi][k−ui]+wi
01 字符构成最多的字符串:多维费用的 0-1 背包最大值,有两个背包大小,0 的数量和 1 的数量。
盈利计划:多维费用的 0-1 背包最大值
分组背包
f[k][j]={不拿第k组拿第k组i::f[k−1][j]f[k−1][j−vi]+wi∣itemi∈group k
掷骰子的N种方法:每一组是一个骰子,每个骰子只能拿一个体积为1到6的物品
Last updated