大家好,又见面了,我是你们的朋友全栈君。
#背包九讲详解
##0-1背包问题
有n个重量和价值分别为 w i , v i w_i, v_i wi,vi 的物品。从这些物品中挑选出总重量不超过 W W W 的物品, 求所有挑选方案中价值总和的最大值。
样例:
n = 4 ( w , v ) = { ( 2 , 3 ) , ( 1 , 2 ) , ( 3 , 4 ) , ( 2 , 2 ) } W = 5 n = 4\\ (w, v) = \{(2,3), (1,2), (3,4), (2,2)\}\\ W = 5 n=4(w,v)={
(2,3),(1,2),(3,4),(2,2)}W=5
n个物品,每种物品只有两种选择,放进背包,或者不放进背包。n个物品对应的,最大的所有可能的总数为 2 n 2^n 2n 种不同的放法。
最朴素的,我们可以枚举所有可能的放法,找到最大值。
R e c ( n , W ) Rec(n, W) Rec(n,W) 表示剩余容量为 W W W 的背包,还有 1 ~ n 1~n 1~n 这 n n n 个物品可以选择, 所能取得的最大的背包价值。
还剩 i i i 个物品可以选, 背包容量剩余 j j j 的时候,可以分为两种情况:
- 第 i i i 个物品不选,只考虑前 i − 1 i-1 i−1 个物品。则 R e c ( i , j ) = R e c ( i − 1 , j ) Rec(i, j) = Rec(i-1, j) Rec(i,j)=Rec(i−1,j) ,此时背包容量j不变
- 第 i i i 个物品被选择, 接下来要考虑的就是, 如何在前 i − 1 i-1 i−1 个物品中选择物品放入容量为 j − w [ i ] j-w[i] j−w[i] 的背包, 则 R e c ( i , j ) = R e c ( i − 1 , j − w [ i ] ) + v [ i ] Rec(i, j) = Rec(i-1, j-w[i]) + v[i] Rec(i,j)=Rec(i−1,j−w[i])+v[i]
总和两种可能,取较大值。可以给出递推式:
R e c ( i , j ) = m a x ( R e c ( i − 1 , j ) , R e c ( i − 1 , j − w [ i ] ) + v [ i ] ) Rec(i, j) = max(Rec(i-1, j), Rec(i-1, j-w[i])+v[i]) Rec(i,j)=max(Rec(i−1,j),Rec(i−1,j−w[i])+v[i])
还需要考虑一些细节:
- 终止条件, i = 0 i = 0 i=0 , 无物品可选 R e c ( 0 , j ) = 0 Rec(0, j) = 0 Rec(0,j)=0。
0个物品可以选择,放入容量为j的背包, 得到的最大价值只能为0 - j < w[i],背包剩余容量不足以放下第i个物品, R e c ( i , j ) = R e c ( i − 1 , j ) Rec(i, j) = Rec(i-1, j) Rec(i,j)=Rec(i−1,j)
综上,得到递推式:
R e c ( i , j ) = { 0 i = 0 R e c ( i − 1 , j ) j < w [ i ] m a x ( R e c ( i − 1 , j ) , R e c ( i − 1 , j − w [ i ] ) + v [ i ] ) o t h e r Rec(i, j) = \begin{cases} 0 & i = 0\\ Rec(i-1, j) & j < w[i]\\ max(Rec(i-1, j), Rec(i-1, j-w[i])+v[i]) & other \end{cases} Rec(i,j)=⎩⎪⎨⎪⎧0Rec(i−1,j)max(Rec(i−1,j),Rec(i−1,j−w[i])+v[i])i=0j<w[i]other
###递归方法:
#include <iostream>
#define MAXN 10000
using namespace std;
int w[MAXN] = {
0, 2, 1, 3, 2};
int v[MAXN] = {
0, 3, 2, 4, 2};
int W = 5, n = 4;
int Rec(int i, int j) {
int res;
if (i == 0) {
// 终止条件, 无物品可选 Rec(0, j) = 0
// 0个物品可以选择,放入容量为j的背包, 得到的最大价值只能为0
res = 0;
}
else if (j < w[i]) {
// 背包剩余容量不足以放下第i个物品
res = Rec(i-1, j);
}
else {
// 抉择,第i个物品选或者不选,都试一下,取较大值
res = max(Rec(i-1, j), Rec(i-1, j-w[i]) + v[i]);
}
return res;
}
int main() {
cout << Rec(n, W) << endl;
return 0;
}
大概画个递归过程:
Rec(4, 5)
/ \
Rec(3, 3) Rec(3, 5)
/ \ / \
Rec(2, 0) Rec(2, 3) Rec(2, 2) Rec(2, 5)
/ / \ / \ / \
Rec(1, 0) Rec(1, 2) Rec(1, 3) Rec(1, 1) Rec(1, 2) Rec(1, 4) Rec(1, 5)
/ / \ / \ / / \ / \ / \
(0,0) (0,0) (0,2) (0,1) (0,3) (0,1) (0,0) (0,2) (0,2) (0,4) (0,3) (0,5)
从上图可以看出,我们枚举了所有可能的情况,即对于每个物品i,物品i选还是不选,我们都考虑了一遍。递归的层数是n+1层,最后一层是判定终止。
再来重申一下 R e c ( i , j ) Rec(i, j) Rec(i,j) 的含义,表示有编号为1~ i i i 的这前 i i i 个物品可以选择,放入容量为 j j j 的背包,所能达到的最大价值。终止条件是 i = 0 i = 0 i=0,时,无物品可选, R e c ( 0 , j ) = 0 Rec(0, j) = 0 Rec(0,j)=0 ,题目要求的是 R e c ( n , W ) Rec(n, W) Rec(n,W) 。
在求解 R e c ( n , W ) Rec(n, W) Rec(n,W) 的过程中,有些情况可能会被重复计算。比如上图 R e c ( 1 , 2 ) Rec(1,2) Rec(1,2) 在第四行计算了2次。这种重复计算是随着 n n n 的增大,指数级增长的,所以 n , W n, W n,W 较大时,问题就是不可解的。时间和空间复杂度太高 ,为 O ( 2 n ) O(2^n) O(2n) 。
###记忆化搜索
我们可以利用递归求解过程中的重复计算。如果 R e c ( i , j ) Rec(i, j) Rec(i,j) 已经计算过,则记录下来,下次需要的时候直接拿来用即可。
#include <iostream>
#include <cstring>
#define MAXN 1000
using namespace std;
int w[MAXN] = {
0, 2, 1, 3, 2};
int v[MAXN] = {
0, 3, 2, 4, 2};
int dp[MAXN][MAXN]; //记录搜索过的结果
int W = 5, n = 4;
int Rec(int i, int j) {
//Rec(i, j)计算过,直接拿来用
if (dp[i][j] != -1) return dp[i][j];
int res;
if (i == 0) {
res = 0;
}
else if (j < w[i]) {
res = Rec(i-1, j);
}
else {
res = max(Rec(i-1, j), Rec(i-1, j-w[i]) + v[i]);
}
return dp[i][j] = res; //记录
}
int main() {
memset(dp, -1, sizeof(dp));
cout << Rec(n, W) << endl;
return 0;
}
对于任意的 i , j , R e c ( i , j ) i, j, Rec(i,j) i,j,Rec(i,j) 只会计算一次,所以复杂度为 O ( n W ) O(nW) O(nW)
###动态规划
上面的递归过程,是把大问题分解成小问题,最后由小问题的解合并成大问题的解。
动态规划的方法,就是先把小问题的解计算好,存在表里,等计算大问题的时候,需要用到小问题的解,就过来查一下表。
由递推式:
R e c ( i , j ) = { 0 ( i = 0 ) R e c ( i − 1 , j ) ( j < w [ i ] ) m a x ( R e c ( i − 1 , j ) , R e c ( i − 1 , j − w [ i ] ) + v [ i ] ) ( o t h e r ) Rec(i, j) = \begin{cases} 0 & (i = 0)\\ Rec(i-1, j) & (j < w[i])\\ max(Rec(i-1, j), Rec(i-1, j-w[i])+v[i]) & (other) \end{cases} Rec(i,j)=⎩⎪⎨⎪⎧0Rec(i−1,j)max(Rec(i−1,j),Rec(i−1,j−w[i])+v[i])(i=0)(j<w[i])(other)
转化为动态规划的写法:
d p [ i ] [ j ] = { 0 ( i = 0 ) d p [ i − 1 ] [ j ] ( j < w [ i ] ) m a x ( d p [ i − 1 ] [ j ] , d p [ i − 1 ] [ j − w [ i ] ] + v [ i ] ) ( o t h e r ) dp[i][j] = \begin{cases} 0 & (i = 0)\\ dp[i-1][j] & (j<w[i])\\ max(dp[i-1][j], dp[i-1][j-w[i]] + v[i]) & (other) \end{cases} dp[i][j]=⎩⎪⎨⎪⎧0dp[i−1][j]max(dp[i−1][j],dp[i−1][j−w[i]]+v[i])(i=0)(j<w[i])(other)
代码:
#include <iostream>
#include <cstring>
#define MAXN 1000
using namespace std;
int dp[MAXN][MAXN];
int w[MAXN] = {
0, 2, 1, 3, 2};
int v[MAXN] = {
0, 3, 2, 4, 2};
int W = 5, n = 4;
int solve(int n, int W) {
memset(dp, 0, sizeof(dp));
for (int i = 1; i <= n; i++) {
//i从1开始,因为i=0的值已经确定为0
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]);
}
}
}
return dp[n][W];
}
int main() {
cout << solve(n, W) << endl;
return 0;
}
dp 优化空间复杂度
输出一下上面的二维数组 d p [ ] [ ] dp[][] dp[][] :
0 0 0 0 0 0
0 0 3 3 3 3
0 2 3 5 5 5
0 2 3 5 6 7
0 2 3 5 6 7
我们是填 d p dp dp 表的顺序是从上到下,从左到右,从递归式可以看出, d p [ i ] [ j ] dp[i][j] dp[i][j] 是由 d p [ i − 1 ] [ j ] dp[i-1][j] dp[i−1][j] 和 d p [ i − 1 ] [ j − w [ i ] ] dp[i-1][j-w[i]] dp[i−1][j−w[i]] 的值推来的。填写第 i i i行的值,只依赖于上一行,第 i − 1 i-1 i−1行的值。下次填写第 i + 1 i+1 i+1 行的时候,只会用到第 i i i 行的值,第 i − 1 i-1 i−1 行的值以后都不会再用到了。
所以我们可以从这里入手优化空间复杂度。没有必要存下整个二维数组,我们只需要存2行,然后不断更新这2行就可以了。
实际上, d p [ i ] [ j ] dp[i][j] dp[i][j] 的值只依赖于第 i − 1 i-1 i−1 行的 d p [ i − 1 ] [ 0… j ] dp[i-1][0…j] dp[i−1][0...j] 这前 j + 1 j+1 j+1 个元素, 与 d p [ i − 1 ] [ j + 1… W ] dp[i-1][j+1 … W] dp[i−1][j+1...W] 的值无关。
所以,我们可以只存1行,就能完成整个 d p dp dp过程。用 d p [ 0… W ] dp[0…W] dp[0...W] 存储当前行,更新 d p [ 0… W ] dp[0…W] dp[0...W] 的时候,我们按照 j = W . . . 0 j = W…0 j=W...0 的递减顺序计算 d p [ j ] dp[j] dp[j],这样可以保证计算 d p [ j ] dp[j] dp[j] 时用到的 d p [ j ] 和 d p [ j − w [ i ] ] dp[j]和 dp[j-w[i]] dp[j]和dp[j−w[i]] 的值和原本的二维数组中的第 i − 1 i-1 i−1 行的值是相等的。更新完 d p [ j ] dp[j] dp[j] 的值后,对 d p [ 0… j − 1 ] dp[0…j-1] dp[0...j−1] 的值不会产生影响。
伪代码:
for i = 1 to n
for j = W to w[i]
dp[j] = max(dp[j], dp[j-w[i]] + v[i])
代码:
#include <iostream>
#include <cstring>
#define MAXN 10000
using namespace std;
int dp[MAXN];
int w[MAXN] = {
0, 2, 1, 3, 2};
int v[MAXN] = {
0, 3, 2, 4, 2};
int W = 5, n = 4;
int solve(int n, int W) {
memset(dp, 0, sizeof(dp));
for (int i = 1; i <= n; i++) {
// i从1开始,递增
for (int j = W; j >= 0; j--) {
// j按递减顺序填表
if (j < w[i]) {
dp[j] = dp[j];
}
else {
dp[j] = max(dp[j], dp[j-w[i]] + v[i]);
}
}
}
return dp[W];
}
int main() {
cout << solve(n, W) << endl;
return 0;
}
####初始化的细节问题
我们看到的求解最优解的背包问题中,事实和桑有两种不太相同的问法。
- 要求”背包恰好装满“ 时的最优解
- 不要求背包一定要被装满时的最优解
我们上面所讨论的就是第2种, 不要求背包一定要被装满时的最优解。
一种区别这两种问法的实现方法是在初始化的时候有所不不同。
如果是第一种问法,要求恰好装满背包,那么在初始化时除了 d p [ 0 ] dp[0] dp[0] 为0, 其他 d p [ 1… W ] 均 设 为 − ∞ dp[1…W]均设为 -\infty dp[1...W]均设为−∞ ,这样就可以保证最终得到 d p [ W ] dp[W] dp[W] 是一种恰好装满背包的最优解
如果并没有要求必须把背包装满,而是只希望价格尽量大,初始化时应该将 d p [ 0… W ] dp[0…W] dp[0...W] 全部设为0。
这是为什么呢?可以这样理解:初始化的 d p dp dp 数组事实上就是在没有任何物品可以放入背包时的合法状态。如果要求背包恰好装满,那么此时只有容量为0的背包可以在什么也不装的状态下被 “恰好装满” ,此时背包价值为0。其他容量的背包均没有合法的解,属于未定义的状态,所以都应该被赋值为 − ∞ -\infty −∞ 。当前的合法解,一定是从之前的合法状态推得的
如果背包并非必须被装满,那么任何容量的背包都有一个合法解 “什么也不装”,这个解的价值为0,所以初始化时状态的值也就全部为0了。
##完全背包问题
有 n n n 种重量和价值分别为 w i , v i w_i, v_i wi,vi 的物品。从这些物品中挑选总重量不超过 W W W 的物品,求出挑选物品价值总和的最大值。在这里,每种物品可以挑选任意多件。
在0-1背包问题中,每种物品只有不选和选两种可能。在完全背包问题中,每个物品可以选0, 1, … ⌊ W / w i ⌋ \lfloor W/w_i \rfloor ⌊W/wi⌋ 个。
令 d p [ i ] [ j ] : = 从 前 i 种 物 品 中 挑 选 总 重 量 不 超 过 j 的 物 品 的 最 大 总 价 值 。 令 dp[i][j] := 从前i种物品中挑选总重量不超过j的物品的最大总价值。 令dp[i][j]:=从前i种物品中挑选总重量不超过j的物品的最大总价值。 那么递推关系为:
d p [ i ] [ j ] = { 0 i = 0 m a x ( d p [ i − 1 ] [ j − k × w [ i ] ] + k × v [ i ] ) 0 ≤ k ≤ ⌊ j / w i ⌋ 1 ≤ i ≤ n dp[i][j] = \begin{cases} 0 & &i = 0\\ max(dp[i-1][j-k×w[i]] + k×v[i]) & 0 \le k \le \lfloor j/w_i \rfloor& 1 \le i \le n\\ \end{cases} dp[i][j]={
0max(dp[i−1][j−k×w[i]]+k×v[i])0≤k≤⌊j/wi⌋i=01≤i≤n
代码:
#include <iostream>
#include <cstring>
#define MAXN 1000
using namespace std;
int dp[MAXN][MAXN];
int w[MAXN] = {
0, 3, 4, 2};
int v[MAXN] = {
0, 4, 5, 3};
int W = 7, n = 3;
int solve(int n, int W) {
memset(dp, 0, sizeof(dp));
for (int i = 1; i <= n; i++) {
for (int j = 0; j <= W; j++) {
for (int k = 0; k <= j/w[i]; k++) {
dp[i][j] = max(dp[i][j], dp[i-1][j-k*w[i]] + k*v[i]);
}
}
}
return dp[n][W];
}
int main() {
cout << solve(n, W) << endl; // 10
return 0;
}
最坏情况下复杂度为 O ( n W 2 ) O(nW^2) O(nW2) , 不够好。这里面还是有很多多余的重复计算。第三层循环里,对于每种物品,都计算 ⌊ j / w i ⌋ \lfloor j/w_i \rfloor ⌊j/wi⌋ 次,这是不必要的。动态规划就是要利用已经计算过的小规模的问题的解,来求解更大规模的问题的解。让我们来探寻一下,还有什么我们没有利用的重复计算。
再重申一下, d p [ i ] [ j ] : = 从 前 i 种 物 品 中 挑 选 总 重 量 不 超 过 j 的 物 品 的 最 大 总 价 值 。 dp[i][j] := 从前i种物品中挑选总重量不超过j的物品的最大总价值。 dp[i][j]:=从前i种物品中挑选总重量不超过j的物品的最大总价值。
再来看一下上面的递推公式:
d p [ i ] [ j ] = { 0 i = 0 m a x ( d p [ i − 1 ] [ j − k × w [ i ] ] + k × v [ i ] ) 0 ≤ k ≤ ⌊ j / w i ⌋ 1 ≤ i ≤ n dp[i][j] = \begin{cases} 0 & &i = 0\\ max(dp[i-1][j-k×w[i]] + k×v[i]) & 0 \le k \le \lfloor j/w_i \rfloor& 1 \le i \le n\\ \end{cases} dp[i][j]={
0max(dp[i−1][j−k×w[i]]+k×v[i])0≤k≤⌊j/wi⌋i=01≤i≤n
这 里 的 k 可 分 为 两 种 情 况 , k = 0 和 k ≠ 0 , 也 就 是 第 i 种 物 品 不 选 , 或 者 至 少 选 1 个 。 这里的k可分为两种情况,k = 0和k \ne 0 ,也就是第i种物品不选,或者至少选1个。 这里的k可分为两种情况,k=0和k=0,也就是第i种物品不选,或者至少选1个。
- k = 0 时 , 即 不 选 择 第 i 种 物 品 , d p [ i ] [ j ] = d p [ k = 0 时,即不选择第i种物品, dp[i][j] = dp[ k=0时,即不选择第i种物品,dp[i][j]=dp[ i − 1 i-1 i−1 ] [ j ] , ][j], ][j],
- k ≠ 0 时 , 即 至 少 选 一 个 第 i 种 物 品 , d p [ i ] [ j ] = d p [ k \ne 0 时, 即至少选一个第i种物品, dp[i][j] = dp[ k=0时,即至少选一个第i种物品,dp[i][j]=dp[ i i i ] [ j − w [ i ] ] + v [ i ] ][j-w[i]] + v[i] ][j−w[i]]+v[i]
注意上面红色标识的 i − 1 i-1 i−1 和 i i i。
k = 0 时 , 比 较 容 易 理 解 , k ≠ 0 时 , 先 强 行 往 背 包 里 塞 一 个 第 i 种 物 品 , 然 后 把 问 题 转 化 成 更 小 规 模 的 问 题 。 k=0时,比较容易理解,k \ne 0时,先强行往背包里塞一个第i种物品,然后把问题转化成更小规模的问题。 k=0时,比较容易理解,k=0时,先强行往背包里塞一个第i种物品,然后把问题转化成更小规模的问题。
i 和 j 都 是 按 递 增 顺 序 循 环 的 , 所 以 求 解 d p [ i ] [ j ] 时 , d p [ i ] [ j − w [ i ] ] 的 值 是 已 经 求 过 了 的 , 可 以 直 接 拿 来 用 。 i和j都是按递增顺序循环的,所以求解dp[i][j]时, dp[i][j-w[i]]的值是已经求过了的,可以直接拿来用。 i和j都是按递增顺序循环的,所以求解dp[i][j]时,dp[i][j−w[i]]的值是已经求过了的,可以直接拿来用。
综上,可以得出递推公式:
d p [ i ] [ j ] = m a x ( d p [ i − 1 ] [ j ] , d p [ i ] [ j − w [ i ] ] + v [ i ] ) ( 1 ) dp[i][j] = max(dp[i-1][j], dp[i][j-w[i]] + v[i]) (1) dp[i][j]=max(dp[i−1][j],dp[i][j−w[i]]+v[i])(1)
更正式地推导一下:
d p [ i ] [ j ] = m a x ( d p [ i − 1 ] [ j − k × w [ i ] ] + k × v [ i ] ) , 0 ≤ k ≤ ⌊ j / w i ⌋ = m a x ( d p [ i − 1 ] [ j ] , m a x ( d p [ i − 1 ] [ j − k × w [ i ] ] + k × v [ i ] ) ) , 1 ≤ k ≤ ⌊ j / w i ⌋ dp[i][j] = max(dp[i-1][j-k×w[i]] + k×v[i]) ,0 \le k \le \lfloor j/w_i \rfloor \\ = max(dp[i-1][j], max(dp[i-1][j-k×w[i]] + k×v[i])), 1 \le k \le \lfloor j/w_i \rfloor dp[i][j]=max(dp[i−1][j−k×w[i]]+k×v[i]),0≤k≤⌊j/wi⌋=max(dp[i−1][j],max(dp[i−1][j−k×w[i]]+k×v[i])),1≤k≤⌊j/wi⌋
= m a x ( d p [ i − 1 ] [ j ] , =max(dp[i-1][j], =max(dp[i−1][j], m a x ( d p [ i − 1 ] [ ( j − w [ i ] ) − ( k − 1 ) × w [ i ] ] + ( k − 1 ) × v [ i ] ) max(dp[i-1][(j-w[i]) – (k-1)×w[i]] + (k-1)×v[i]) max(dp[i−1][(j−w[i])−(k−1)×w[i]]+(k−1)×v[i]) + v [ i ] ) , +v[i]), +v[i]),$0 \le (k-1) \le \lfloor j/w_i \rfloor -1\$= m a x ( d p [ i − 1 ] [ j ] , =max(dp[i-1][j], =max(dp[i−1][j], d p [ i ] [ j − w [ i ] ] dp[i][j-w[i]] dp[i][j−w[i]] + v [ i ] ) + v[i]) +v[i])
刚好和上面的(1)式一样。
代码:
#include <iostream>
#include <cstring>
#define MAXN 1000
using namespace std;
int dp[MAXN][MAXN];
int w[MAXN] = {
0, 3, 4, 2};
int v[MAXN] = {
0, 4, 5, 3};
int W = 7, n = 3;
int solve(int n, int W) {
memset(dp, 0, sizeof(dp));
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][j-w[i]] + v[i]);
}
}
}
return dp[n][W];
}
int main() {
cout << solve(n, W) << endl; // 10
return 0;
}
类似0-1背包问题, 完全背包问题也可以在空间上进行优化。
我 们 发 现 d p [ i ] [ j ] 的 值 只 依 赖 于 第 i 行 和 第 i − 1 行 的 值 , 可 以 只 用 一 个 一 维 数 组 来 存 所 有 需 要 的 值 我们发现dp[i][j]的值只依赖于第i行和第i-1行的值, 可以只用一个一维数组来存所有需要的值 我们发现dp[i][j]的值只依赖于第i行和第i−1行的值,可以只用一个一维数组来存所有需要的值
伪代码:
for i = 1 to n
for j = 0 to W
dp[j] = max(dp[j], dp[j-w[i]] + v[i])
我们可以发现,完全背包的伪代码和0-1背包的伪代码非常像,只是第二层循环j的顺序不同,0-1背包是逆序循环,完全背包是正序循环。0-1背包问题为何逆序,之前已经讲得非常清楚了。完全背包问题第二层的j正序循环,因为 d p [ i ] [ j ] dp[i][j] dp[i][j] 需要用到 d p [ i ] [ j − w [ i ] ] dp[i][j-w[i]] dp[i][j−w[i]] , 只用一维数组保存的话,按j正序循环,就能保证计算 d p [ j ] dp[j] dp[j] 时,旧的 d p [ j ] dp[j] dp[j] 的值就等于 d p [ i − 1 ] [ j ] dp[i-1][j] dp[i−1][j] 的值, d p [ j − w [ i ] ] dp[j-w[i]] dp[j−w[i]] 已经计算过,且对应的就是二维数组中 d p [ i ] [ j − w [ i ] ] dp[i][j-w[i]] dp[i][j−w[i]] 的值。
代码:
#include <iostream>
#include <cstring>
#define MAXN 1000
using namespace std;
int dp[MAXN];
int w[MAXN] = {
0, 3, 4, 2};
int v[MAXN] = {
0, 4, 5, 3};
int W = 7, n = 3;
int solve(int n, int W) {
memset(dp, 0, sizeof(dp));
for (int i = 1; i <= n; i++) {
for (int j = 0; j <= W; j++) {
if (j < w[i]) {
dp[j] = dp[j];
}
else {
dp[j] = max(dp[j], dp[j-w[i]] + v[i]);
}
}
}
return dp[W];
}
int main() {
cout << solve(n, W) << endl; // 10
return 0;
}
##多重背包问题
有 n n n 种物品和一个容量为 W W W 的背包。第 i i i 种物品有 m i m_i mi 个,每件重量为 w i w_i wi , 价值为 v i v_i vi ,求从这 n n n 种物品中挑选重量总和不超过 W W W 的物品的最大价值。
###转化成0-1背包问题
多重背包问题,最简单的解法,就是转化成0-1背包问题。第 i i i 个物品有 m i m_i mi 个, 等价于有 m i m_i mi 个相同的物品。但直接拆分成 m i m_i mi 件物品并不是最好的方法。我们可以利用二进制来拆分。例如 m 1 = 13 = 2 0 + 2 1 + 2 2 + 6 m_1 = 13 = 2^0 + 2^1 + 2^2 + 6 m1=13=20+21+22+6 ,我们将第一种物品共13件,拆分成 2 0 , 2 1 , 2 2 , 6 2^0, 2^1, 2^2, 6 20,21,22,6 这四件, 13以内的任何数字都可以通过这四种数字组合而成。
下面给出一个二进制拆分的多重背包模板:
const int N = 100, W = 100000;
int cost[N], weight[N], number[N];
int dp[W + 1];
int knapsack(int n, int w)
{
for (int i = 0; i < n; ++i)
{
int num = min(number[i], w / weight[i]);
for (int k = 1; num > 0; k*=2)
{
if (k > num) k = num;
num -= k;
for (int j = w; j >= weight[i] * k; --j)
dp[j] = max(dp[j], dp[j - weight[i] * k] + cost[i] * k);
}
}
return dp[w];
}
时间复杂度为 O ( n l o g M × W ) O(nlogM × W) O(nlogM×W) , 实际应用已经足够好了。
###单调队列优化
多重背包问题的递归式为:
d p [ i ] [ j ] = { 0 i = 0 m a x ( d p [ i − 1 ] [ j − k × w [ i ] ] + k × v [ i ] ) 0 ≤ k ≤ m [ i ] 1 ≤ k ≤ n dp[i][j] = \begin{cases} 0 & &i = 0\\ max(dp[i-1][j-k×w[i]] + k×v[i]) & 0 \le k \le m[i] & 1 \le k \le n \end{cases} dp[i][j]={
0max(dp[i−1][j−k×w[i]]+k×v[i])0≤k≤m[i]i=01≤k≤n
根据递推式,很容易能写出三重循环版本的多重背包代码。但这明显不是我们想要的。是否有方法能像完全背包问题一样,能有 O ( n W ) O(nW) O(nW) 的解法呢?
是的,有。
通过巧妙的构造,我们就可以用单调队列来优化多重背包问题, 使得复杂度降为 O ( n W ) O(nW) O(nW)
什么是单调队列: http://blog.csdn.net/justmeh/article/details/5844650
用单调队列优化: http://blog.csdn.net/flyinghearts/article/details/5898183
多重背包 单调队列模板:
#include <iostream>
#include <deque>
#include <algorithm>
using namespace std;
struct Pack
{
int sum, cost;
Pack(int s, int c) : sum (s), cost(c) {
}
};
const int Maxv = 1001;
deque <Pack> Q;
int N, V, F[Maxv];
int main()
{
cin >> N >> V;
for (int i = 1, p, w, c; i <= N; i ++)
{
cin >> p >> w >> c; p = min(p, V / w);
for (int j = 0; j < w; j ++)
{
Q.clear();
for (int k = 0; k <= (V - j) / w; k ++)
{
int y = F[k * w + j] - k * c;
while (Q.size() && Q.back().cost <= y) Q.pop_back();
Q.push_back(Pack(k, y));
if (Q.front().sum < k - c) Q.pop_front();
F[k * w + j] = Q.front().cost + k * c;
}
}
}
cout << F[V] << endl;
return 0;
目前第三讲刚开了个头。 补全随缘吧。。工作后没太多时间了(捂脸)
未完待续,我会尽快补全9讲的。
优秀的背包问题模板:http://blog.csdn.net/libin56842/article/details/9396649;
发布者:全栈程序员-用户IM,转载请注明出处:https://javaforall.cn/153455.html原文链接:https://javaforall.cn
【正版授权,激活自己账号】: Jetbrains全家桶Ide使用,1年售后保障,每天仅需1毛
【官方授权 正版激活】: 官方授权 正版激活 支持Jetbrains家族下所有IDE 使用个人JB账号...