背包九讲(一)01背包

1. 题目

1.1 题目描述

N N N件物品和一个容量为 W W W的背包。每件物品只能使用一次
i i i件物品的体积是 w i w_i wi,价值是 v i v_i vi
求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。

1.2 经典例题

洛谷P1048 [NOIP2005 普及组] 采药

2. 思路

2.1 基本思路

这是最基础的背包问题,特点是:每种物品仅有一件,可以选择取或不取
考虑如何将问题转化成规模更小的子问题。对于第 i i i件物品,在最终方案中要么不取、要么取,于是我们可以对这两种情况进行分类讨论,转化为子问题:

  • 如果不取第 i i i件物品,那么相当于只有前 i − 1 i-1 i1件物品、背包大小相同的子问题
  • 如果取了第 i i i件物品,那么相当于只有前 i − 1 i-1 i1件物品( i − 1 i-1 i1件物品已经经过了选择)、背包大小减去 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 1i的物品,背包容量为 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[i1][j], dp[i1][jw[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,则有状态转移如下:

背包九讲(一)01背包_第1张图片

背包九讲(一)01背包_第2张图片

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[i1][],即上一行的数据。也就是说,比 i − 1 i-1 i1更小的再也不会被用到。如果把 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从大到小),那么 “活跃” 的格子就正好只有上一行的左半部分以及这一行的右半部分。(即除白色格子以外的格子)
背包九讲(一)01背包_第3张图片
那么实际上我们只需要保存这些 “活跃” 格子的状态就可以了,我们可以得到一维的状态转移方程:
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[jw[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,有以下三个物品:
背包九讲(一)01背包_第4张图片
求将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。

我们先来看内层循环 j j j从小到大遍历(顺序遍历)的情况
背包九讲(一)01背包_第5张图片
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从大到小遍历(逆序遍历)的情况
背包九讲(一)01背包_第6张图片
上面逆序遍历的是不是就实现了每件物品只使用一次的要求,其实顺序遍历 每件物品可以被反复使用,这个就是我们后面要讲的完全背包。

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] Vdp[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(sum2dp[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[jc[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[jc[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[jv[i]][kg[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 maxmin,或者像存在性问题进行 = 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[jw[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[jw[i]],因为这时候出现了新的最优选法,原来的 c n t [ j ] cnt[j] cnt[j]就没用了,我们拿当前转移过来的方案数即 c n t [ j − w [ i ] ] cnt[j-w[i]] cnt[jw[i]]赋值。
  • d p [ j − w [ i ] ] + v [ i ] = d p [ j ] dp[j-w[i]]+v[i]=dp[j] dp[jw[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[jw[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[N1][W]是从哪个状态转移过来的:

  • 如果 d p [ N ] [ W ] = d p [ N − 1 ] [ W ] dp[N][W]=dp[N-1][W] dp[N][W]=dp[N1][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[N1][Ww[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];
    }
}

你可能感兴趣的:(算法知识,算法,数据结构,动态规划)