1. 题目
1.1 题目描述
有
N
N
N件物品和一个容量为
W
W
W的背包。每件物品只能使用一次。
第
i
i
i件物品的体积是
w
i
w_i
wi,价值是
v
i
v_i
vi。
求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。
1.2 经典例题
2. 思路
2.1 基本思路
这是最基础的背包问题,特点是:每种物品仅有一件,可以选择取或不取。
考虑如何将问题转化成规模更小的子问题。对于第
i
i
i件物品,在最终方案中要么不取、要么取,于是我们可以对这两种情况进行分类讨论,转化为子问题:
- 如果不取第 i i i件物品,那么相当于只有前 i − 1 i-1 i−1件物品、背包大小相同的子问题
- 如果取了第 i i i件物品,那么相当于只有前 i − 1 i-1 i−1件物品( i − 1 i-1 i−1件物品已经经过了选择)、背包大小减去 w i w_i wi的子问题,在它的答案上再加上 v i v_i vi的价值(取了第 i i i件物品贡献的价值)。
在这两种情况中取价值更高的,作为答案。
2.2 状态转移方程
设
d
p
[
i
]
[
j
]
dp[i][j]
dp[i][j]表示使用编号为
1
∼
i
1 ∼ i
1∼i的物品,背包容量为
j
j
j时的最大价值,有转移方程:
d
p
[
i
]
[
j
]
=
m
a
x
(
d
p
[
i
−
1
]
[
j
]
,
d
p
[
i
−
1
]
[
j
−
w
[
i
]
]
+
v
[
i
]
)
dp[i][j] = max(dp[i-1][j], \ dp[i-1][j-w[i]] + v[i])
dp[i][j]=max(dp[i−1][j], dp[i−1][j−w[i]]+v[i])
2.3 例子
为了加深对状态转移方程的理解,我们来看下图的一个例子,每个格子代表一个状态, ( 0 , 0 ) (0,0) (0,0)代表初始状态,蓝色的格子代表已经求得的状态,灰色的格子代表非法状态,红色的格子代表当前正在进行转移的状态,图中的第 i i i行代表了前 i i i个物品对应容量的最优值,第 4 4 4个物品的体积为 2 2 2,价值为 8 8 8,则有状态转移如下:
2.4 代码
for (int i = 1; i <= n; i++) { // 遍历物品
for (int j = 0; j <= W; j++) { // 遍历背包容量
if (j < w[i]) dp[i][j] = dp[i-1][j];
else dp[i][j] = max(dp[i-1][j], dp[i-1][j-w[i]] + v[i])
}
}
3. 优化空间复杂度
我们发现以上方法的状态数是 O ( N W ) O(NW) O(NW)的,整个求解过程的时间和空间复杂度均为 O ( N W ) O(NW) O(NW),其中时间复杂度已经不能再优化了,但是空间复杂度还是可以优化的。
3.1 滚动数组
我们观察刚才的代码:每个 d p [ i ] [ j ] dp[i][j] dp[i][j]在转移时只用到了 d p [ i − 1 ] [ ∗ ] dp[i-1][*] dp[i−1][∗],即上一行的数据。也就是说,比 i − 1 i-1 i−1更小的再也不会被用到。如果把 d p dp dp看成一张二维的表格,那么只有两行的格子是 “活跃” 的。基于这一思想,我们可以只保存这两行。
3.2 代码
int pre = 0, cur = 1; // pre:前一行 cur:当前行
for (int i = 1; i <= n; i++) {
for (int j = 0; j <= W; j++) {
if (j < w[i]) dp[cur][j] = dp[pre][j];
else dp[cur][j] = max(dp[pre][j], dp[pre][j-w[i]] + v[i])
}
swap(pre, cur); // 每一轮结束 当前行变成前一行, 交换, 每次只用两行的空间
}
3.3 一维数组
我们继续刚才的思路:把
d
p
dp
dp看成一张二维的表格,那么每个格子在转移时,只会用到上一行中在它左侧的格子。如果我们调整一下转移的顺序,每一行从右往左进行更新(
j
j
j从大到小),那么 “活跃” 的格子就正好只有上一行的左半部分以及这一行的右半部分。(即除白色格子以外的格子)
那么实际上我们只需要保存这些 “活跃” 格子的状态就可以了,我们可以得到一维的状态转移方程:
d
p
[
j
]
=
m
a
x
(
d
p
[
j
]
,
d
p
[
j
−
w
[
i
]
]
+
v
[
i
]
)
dp[j] = max(dp[j], \ dp[j-w[i]] + v[i])
dp[j]=max(dp[j], dp[j−w[i]]+v[i])
3.4 代码
for (int i = 1; i <= n; i++) { // 遍历物品
for (int j = W; j >= w[i]; j--) { // 遍历背包容量
dp[j] = max(dp[j], dp[j-w[i]] + v[i]);
}
}
3.5 例子
我们通过下面这个例子来深入理解下,为什么降维之后,遍历背包容量(内层循环)要逆序遍历。
假定目前背包容量为
5
5
5,有以下三个物品:
求将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。
我们先来看内层循环
j
j
j从小到大遍历(顺序遍历)的情况:
01
01
01背包基本要求是每件物品只能使用一次,我们从使用第一件物品的时候,就可以发现如果内层循环
j
j
j从小到大遍历,那么这件物品会被多次使用。
比如
d
p
[
2
]
dp[2]
dp[2]的状态一定来自
d
p
[
1
]
dp[1]
dp[1],而
d
p
[
1
]
dp[1]
dp[1]的状态来自于
d
p
[
0
]
dp[0]
dp[0],这时候我们发现体积为
1
1
1的第一件物品被用了两次:
d
p
[
2
]
=
d
p
[
1
]
+
5
=
(
d
p
[
0
]
+
5
)
+
5
dp[2]=dp[1]+5=(dp[0]+5)+5
dp[2]=dp[1]+5=(dp[0]+5)+5
接下来我们来看下内层循环
j
j
j从大到小遍历(逆序遍历)的情况:
上面逆序遍历的是不是就实现了每件物品只使用一次的要求,其实顺序遍历 每件物品可以被反复使用,这个就是我们后面要讲的完全背包。
4. 经典题型
4.1 最大值问题
题目链接:洛谷P1048 [NOIP2005 普及组] 采药
题意:有
N
N
N件物品和一个容量为
W
W
W的背包。**每件物品只能使用一次。**第
i
i
i件物品的体积是
w
i
w_i
wi,价值是
v
i
v_i
vi。求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。
题解:模板题,见上文
4.2 最小值问题
题目链接:洛谷P1049 [NOIP2001 普及组] 装箱问题
题意:有一个箱子容量为
V
V
V,同时有
n
n
n个物品,每个物品有一个体积。
现在从
n
n
n个物品中,任取若干个装入箱内(也可以不取),使箱子的剩余空间最小。输出这个最小值。
题解:本题可以认为每个物品的价值就是体积,
d
p
[
j
]
dp[j]
dp[j]: 在
j
j
j的箱子容量下能够装入的最大总体积。
题目要求最少剩余空间,那么我们最后答案就是
V
−
d
p
[
V
]
V-dp[V]
V−dp[V]。
对于这种最小值问题,我们转化下思路,用总值减去求得的最大值,即是最小值。
扩展题目:有
n
n
n件物品,第
i
i
i件物品价值为
a
i
a_i
ai,现在要把这些物品分成两堆,期望两堆的价值之差最小,求最小价值差。(原理本质来说是一样的,我们先用
s
u
m
sum
sum求出所有物品价值之和,然后求出
d
p
[
s
u
m
/
2
]
dp[sum/2]
dp[sum/2],即尽可能接近总价值一半的情况,最后答案就是
a
b
s
(
s
u
m
−
2
∗
d
p
[
s
u
m
/
2
]
)
abs(sum-2*dp[sum/2])
abs(sum−2∗dp[sum/2]))。
4.3 存在性问题
题目链接:洛谷P1877 [HAOI2012] 音量调节
题意:给定
n
n
n首歌和刚开始的音量
b
e
g
i
n
L
e
v
e
l
beginLevel
beginLevel,每首歌开始前能够改变的音量是
c
i
c_i
ci(当前音量调高或者调低
c
i
c_i
ci),音量不能小于
0
0
0且不能大于
m
a
x
L
e
v
e
l
maxLevel
maxLevel。求
n
n
n首歌演唱完之后,最大音量是多少。
题解:存在性问题本质来说就是在
01
01
01背包基础上,用
d
p
[
j
]
=
1
dp[j]=1
dp[j]=1表示能够达到
j
j
j这个状态,
d
p
[
j
]
=
0
dp[j]=0
dp[j]=0表示不能够达到
j
j
j这个状态。
在本题中
d
p
[
j
]
=
0
/
1
dp[j]=0/1
dp[j]=0/1表示能否达到
j
j
j这个音量,一开始我们把
d
p
dp
dp数组初始化成
0
0
0,表示所有音量都无法达到,然后把题目中给定的初始音量标记成
1
1
1,即
d
p
[
b
e
g
i
n
L
e
v
e
l
]
=
1
dp[beginLevel]=1
dp[beginLevel]=1。
在每首歌开始前,我们可以在当前音量的基础上增加或减少
c
i
c_i
ci,那么我们是不是就可以去检测一下
d
p
[
j
−
c
[
i
]
]
dp[j-c[i]]
dp[j−c[i]]或
d
p
[
j
+
c
[
i
]
]
dp[j+c[i]]
dp[j+c[i]]是否在之前达到过,如果达到过我们就能在之前的状态基础上,增加或者减少
c
i
c_i
ci,达到
j
j
j这个音量。
细节问题:本题无法用一维数组优化空间复杂度的形式(但是滚动数组还是可以的),问题在于我们内层循环逆序遍历的过程中,
d
p
[
j
+
c
[
i
]
]
dp[j+c[i]]
dp[j+c[i]]会影响到,使得该次改变执行了多次(即背包中该件物品反复使用),和上文讲到的内层循环顺序遍历,
d
p
[
j
−
c
[
i
]
]
dp[j-c[i]]
dp[j−c[i]]会影响到,使得该件物品反复使用,原理一样。
练习题目:洛谷P8742 [蓝桥杯 2021 省A] 砝码称重
4.4 二维费用问题
题目链接:洛谷P1794 装备运输
题意:有
N
N
N件物品和一个可容纳
V
V
V体积、承载
G
G
G重量的背包。**每件物品只能使用一次。**第
i
i
i件物品的体积是
v
i
v_i
vi,重量是
g
i
g_i
gi,价值是
t
i
t_i
ti。求解将哪些物品装入背包,可使得这些物品的总体积不超过背包的可容纳体积、总重量不超过背包的可承载重量,且总价值最大。
题解:经典
01
01
01背包的费用只有体积,二维费用问题是在原来问题的基础上多加了一维费用,那对应的我们只需要给状态也多加一维就好了。为了保证每件物品只使用一次,对于体积和重量的这两维,我们都采用逆序的循环。对应的状态转移方程:
d
p
[
j
]
[
k
]
=
m
a
x
(
d
p
[
j
]
[
k
]
,
d
p
[
j
−
v
[
i
]
]
[
k
−
g
[
i
]
]
+
t
[
i
]
)
dp[j][k] = max(dp[j][k],\ dp[j - v[i]][k - g[i]] + t[i])
dp[j][k]=max(dp[j][k], dp[j−v[i]][k−g[i]]+t[i])
代码:
for (int i = 1; i <= n; i++)
for (int j = V; j >= v[i]; j--)
for (int k = G; k >= g[i]; k--)
dp[j][k] = max(dp[j][k], dp[j - v[i]][k - g[i]] + t[i]);
练习题目:洛谷P1507 NASA的食物计划
4.5 方案数问题
题目链接:AcWing 背包问题求方案数
题意:有
N
N
N件物品和一个容量为
W
W
W的背包。每件物品只能使用一次。第
i
i
i件物品的体积是
w
i
w_i
wi,价值是
v
i
v_i
vi。求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。
输出最优选法的方案数。注意答案可能很大,请输出答案模
1
0
9
+
7
10^9+7
109+7的结果。
题解:对于求解方案数的问题,主要在于转移方程的时候不再是
m
a
x
、
m
i
n
max、min
max、min,或者像存在性问题进行
=
1
=1
=1标记,而是说我们需要把之前状态的方案数加到当前状态上,即
+
=
+=
+=,具体我们看下这个例题。
本题求在最优选法情况下的方案数。那么我们在原来求最优选法
01
01
01背包的基础上,可以再定义一个
c
n
t
cnt
cnt数组,
c
n
t
[
j
]
cnt[j]
cnt[j]表示在
j
j
j的背包大小下最优选法的方案数。那么
c
n
t
cnt
cnt数组会受到
d
p
dp
dp数组(或者说最优选法)的影响。有以下两种情况:
- 当 d p [ j − w [ i ] ] + v [ i ] > d p [ j ] dp[j-w[i]]+v[i]>dp[j] dp[j−w[i]]+v[i]>dp[j],即在背包大小为 j j j,在使用第 i i i个物品的时候出现了一个新的最优值,那么显然我们需要更新下 d p [ j ] dp[j] dp[j]。同时我们要让 c n t [ j ] = c n t [ j − w [ i ] ] cnt[j] = cnt[j-w[i]] cnt[j]=cnt[j−w[i]],因为这时候出现了新的最优选法,原来的 c n t [ j ] cnt[j] cnt[j]就没用了,我们拿当前转移过来的方案数即 c n t [ j − w [ i ] ] cnt[j-w[i]] cnt[j−w[i]]赋值。
- 当 d p [ j − w [ i ] ] + v [ i ] = d p [ j ] dp[j-w[i]]+v[i]=dp[j] dp[j−w[i]]+v[i]=dp[j],即在背包大小为 j j j,在使用第 i i i个物品的时候出现了一个一样的最优值情况,那么我们这时候只需要给 c n t [ j ] cnt[j] cnt[j]加上这种情况的方案,即 c n t [ j ] + = c n t [ j − w [ i ] ] cnt[j] += cnt[j-w[i]] cnt[j]+=cnt[j−w[i]]
普通的求方案数问题只需要转移
+
=
+=
+=就可以了,比如练习题目中的 小A点菜,把之前所有能够转移到当前状态的前置状态的方案数都加上。但是加上了最优之类的条件,我们就需要考虑当前状态的转移是更优的情况还是**一样优的情况,**根据不同情况对方案计数进行更改。
同样的思想在图论的最短路(松弛操作)、拓扑排序之类算法问题中也经常出现。
细节问题:本题需要考虑把
c
n
t
[
j
]
cnt[j]
cnt[j]先全部初始化成
1
1
1,因为背包什么都不装也是一种方案。
代码:
for (int i = 1; i <= N; i++)
for (int j = V; j >= v[i]; j--) {
if (dp[j] < dp[j - v[i]] + w[i]) {
dp[j] = dp[j - v[i]] + w[i];
cnt[j] = cnt[j - v[i]] % mod;
}
else if (dp[j] == dp[j - v[i]] + w[i]) {
cnt[j] = (cnt[j] + cnt[j - v[i]]) % mod;
}
}
练习题目:洛谷P1164 小A点菜
4.6 输出具体方案问题
题目链接:AcWing背包问题求具体方案
题意:有
N
N
N件物品和一个容量为
W
W
W的背包。每件物品只能使用一次。第
i
i
i件物品的体积是
w
i
w_i
wi,价值是
v
i
v_i
vi。求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。输出字典序最小的方案。
题解:输出具体方案本质来说就是输出转移路径。假设最优解是
d
p
[
N
]
[
W
]
dp[N][W]
dp[N][W],那么我们判断第
n
n
n个物品是否选择,实际上就是看
d
p
[
N
−
1
]
[
W
]
dp[N-1][W]
dp[N−1][W]是从哪个状态转移过来的:
- 如果 d p [ N ] [ W ] = d p [ N − 1 ] [ W ] dp[N][W]=dp[N-1][W] dp[N][W]=dp[N−1][W],即不选第 N N N个物品。
- 如果 d p [ N ] [ W ] = d p [ N − 1 ] [ W − w [ N ] ] + v [ N ] dp[N][W]=dp[N-1][W-w[N]]+v[N] dp[N][W]=dp[N−1][W−w[N]]+v[N],那么是选了这个物品,得到最优解。
细节问题:题目中要输出字典序最小的方案,我们物品得逆序遍历,因为从顺序遍历时,如果序号
2
2
2和序号
3
3
3的最大价值都是
10
10
10,那么最后记录的最大值对应的序号就是
3
3
3,后面的会给前面的覆盖掉,我们实际想要的是序号
2
2
2。
代码:
for (int i = N; i >= 1; i--) {
for (int j = 1; j <= W; j++) {
if (j >= w[i]) dp[i][j] = max(dp[i+1][j], dp[i+1][j-w[i]] + v[i]);
else dp[i][j] = dp[i+1][j];
}
}
for (int i = 1; i <= N; i++) {
if (W >= w[i] && dp[i][W] == dp[i+1][W-w[i]] + v[i]) {
ans.push_back(i);
W -= w[i];
}
}
扩展题目(大容量背包):有
N
N
N件物品和一个容量为
W
W
W的背包。**每件物品只能使用一次。**第
i
i
i件物品的体积是
w
i
w_i
wi。求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,求最大总体积是多少?具体方案是怎么样的?
(
1
<
=
n
<
=
1000
,
1
<
=
W
<
=
10000000
1<=n<=1000, 1<=W<=10000000
1<=n<=1000,1<=W<=10000000)
题解:对于这种大容量背包,很明显开二维
d
p
dp
dp会炸空间。我们需要给它优化一下,给它降下维。有一种方式是可以用二进制bitset优化(这个不细说了,有兴趣的可以自己研究下)。我这里介绍下类似搜索中记录路径的方式,我们可以用
p
a
t
h
[
j
]
=
i
path[j]=i
path[j]=i去记录下在
j
j
j这个背包大小情况用了
i
i
i这个物品。最后倒序去遍历检测一下第
i
i
i物品是否需要被选(和上面小容量那题一样),并且是否记录在这个
p
a
t
h
path
path里。
for (int i = 1; i <= n; i++) {
for (int j = W; j >= w[i]; j--) {
if (dp[j-w[i]] + w[i] > dp[j]) {
dp[j] = dp[j-w[i]] + w[i];
path[j] = i;
}
}
}
for (int i = n; i >= 1; i--) {
if (W >= w[i]&& dp[W] == dp[W-w[i]] + w[i] && path[W] == i) {
ans.push_back(i);
W -= w[i];
}
}