参考了著名的背包九讲,可以在这里下载,在这里做一个个人笔记,当然过程中有些不懂的地方也参考了许多其他博客,如果能帮助到你那就更好了^ ^
有N
件物品和一个容量为V
的背包,放入第i
件物品的费用是 C i 1 C_i^1 Ci1,得到的价值是 W i W_i Wi。求解将哪些物品装入背包可使价值综合最大。
最基础的背包问题,每种物品只有一件,可以选择放或者不放。
用子问题来获取状态方程:即 F [ i , j ] F[i, j] F[i,j] 表示前 i i i 件物品恰放入一个容量为 v v v 的背包可以获得的最大价值。
d p [ i , v ] = m a x { d p [ i − 1 , v ] , d p [ i − 1 , v − C i ] + W i } dp[i,v] = max\left\{ dp[i-1, v], dp[i-1, v-C_i]+ W_i \right\} dp[i,v]=max{dp[i−1,v],dp[i−1,v−Ci]+Wi}
可能刚开始看到这个方程会比较懵,接下来通过一个实例来理解一下,这个栗子参考了这篇博客(帮助很大)。
假设我们有一个可以装下4磅东西的背包,可以装的物品如下:
当然可以选择暴力解法,穷举所有可能的装法(当然会有很多装法超出容量),然后选择价值最大的装法。每个商品都有两种状态(装或不装),所以算法的时间复杂度为 O ( 2 N ) O(2^N) O(2N),显然不合理。
然后就是动态规划解法,将装满一个大背包转化为装满一个小背包的问题。
从上面我们的状态转移方程就能看出,求解的过程就是填写一个网格(二维数组),当将网格填写完整后,我们的问题也就解决了。我们当前问题的网格如下:
我们接下来依次填写各行。
① 首先是吉他行,在这一行中,我们在不同背包容量情况下考虑是否装入吉他,判断的依据就是是否能够获得更高的价值。
这种情况十分简单,因为目前只有吉他一种物品,所以最优情况当然是选择装入吉他!
② 其次是音响行,我们需要考虑是否装入音响,这时我们的状态转移方程就派上用场了。
d p [ 音 响 , v ] = m a x { d p [ 音 响 之 前 , v ] , d p [ 音 响 之 前 , v − C 音 响 ] + W 音 响 } dp[音响,v] = max\left\{ dp[音响之前, v], dp[音响之前, v-C_{音响}]+ W_{音响} \right\} dp[音响,v]=max{dp[音响之前,v],dp[音响之前,v−C音响]+W音响}
由于音响为4磅,所以前三格都只能选择 d p [ 音 响 之 前 , v ] dp[音响之前, v] dp[音响之前,v],如下图所示:
对于第四格 d p [ 音 响 , 4 ] = m a x { 1500 , 3000 + 0 } = 3000 dp[音响,4] = max\left\{ 1500, 3000+ 0 \right\} = 3000 dp[音响,4]=max{1500,3000+0}=3000 ,即选择音响(剩余容量为0,什么也装不下)
③ 笔记本电脑行,同样的方法,使用状态方程求解。
由于笔记本为3磅,所以前两个单元格直接继承上一行的结果:
对于第三格 d p [ 笔 记 本 电 脑 , 3 ] = m a x { 1500 , 2000 + 0 } = 2000 dp[笔记本电脑,3] = max\left\{ 1500, 2000+ 0 \right\} = 2000 dp[笔记本电脑,3]=max{1500,2000+0}=2000 ,即选择音响(剩余容量为0,什么也装不下)
对于第四格:
d p [ 笔 记 本 电 脑 , 4 ] = m a x { d p [ 笔 记 本 电 脑 之 前 , 4 ] , w 笔 记 本 电 脑 + d p [ 笔 记 本 电 脑 之 前 , 1 ] } dp[笔记本电脑,4] = max\left\{ dp[笔记本电脑之前, 4], w_{笔记本电脑}+ dp[笔记本电脑之前, 1] \right\} dp[笔记本电脑,4]=max{dp[笔记本电脑之前,4],w笔记本电脑+dp[笔记本电脑之前,1]}
= m a x { 3000 , 2000 + 1500 } = 3500 = max\left\{ 3000, 2000+ 1500 \right\} = 3500 =max{3000,2000+1500}=3500
然后我们就填满了表格,也就得到了最优解。
通过这个栗子就能清楚的看到整个问题解决的过程,每当我们要选择装入一个新的物品时(当前行),我们就参考前面的结果(前一行),由此来做出决策,这样对状态转移方程也总算时理解了。
这样我们就能写出这个过程的伪代码了:
dp[0,0...V] ← 0
for i ← 1 to N // 依次遍历N件物品
for v ← 1 to Ci // 装不下当前物品的背包直接继承前值
dp[i, v] ← dp[i-1,v]
for v ← Ci to V // 能装下当前物品的背包使用状态方程
dp[i, v] ← max{dp[i-1,v], dp[i-1,v-Ci]+Wi}
以上方法的时间和空间复杂度为 O ( V N ) O(VN) O(VN),相比较于暴力穷举 O ( 2 N ) O(2^N) O(2N) 当然是快很多,但是还能在空间复杂度上做进一步优化。
从上面的栗子可以看出,我们考虑新的物品是否装入背包时,只需要参考前一行的结果,之前保存的结果后面都用不上了,所以我们并不需要一直保存着这样一个二维的矩阵。
并且我们看到这样一个事实,我们只参考了小于等于当前坐标的结果,即 d p [ i − 1 , v ] , d p [ i − 1 , v − C i ] dp[i-1, v], dp[i-1, v-C_i] dp[i−1,v],dp[i−1,v−Ci](第 v v v 个格子和第 v − C i v-C_i v−Ci 个格子)
基于上面事实,我们其实只需要使用一个一维数组就能实现整个过程了,状态方程为:
d p [ v ] = m a x { d p [ v ] , d p [ v − C i ] + W i } dp[v] = max\left\{ dp[v], dp[v-C_i] + W_i \right\} dp[v]=max{dp[v],dp[v−Ci]+Wi}
但是要求 v v v 按照递减的顺序从 V , V − 1 , . . , 0 V,V-1,..,0 V,V−1,..,0 计算 d p [ v ] dp[v] dp[v] ,这样就能保证整个运算过程的正确性。
此时的空间复杂度就下降到了 O ( V ) O(V) O(V) ,但是时间复杂度并没有降低。
下面时这个过程的伪代码:
dp[0...V] ← 0
for i ← 1 to N // 依次遍历N件物品
for v ← V to Ci // 能装下当前物品的背包使用状态方程
dp[v] ← max{dp[v], dp[v-Ci]+Wi}
在上述01背包的基础上,如果要求恰好装满背包,那应该如何求解?
其实和前面的区别仅仅时初始化的方式不同。之前我们初始化就是将 d p [ 0... V ] dp[0...V] dp[0...V] 都初始化为0, 含义就是无论当前背包容量为多少,此时获取的最大价值为0;
而现在要求恰好装满背包,我们只需要将 d p [ 0 ] dp[0] dp[0] 初始化为0,其余 d p [ 1... V ] dp[1...V] dp[1...V] 初始化为 − ∞ -\infty −∞ ,含义就是只有 d p [ 0 ] dp[0] dp[0] 一个合法的解(此时恰好装满),其他情况都不是合法的解(赋值为 − ∞ -\infty −∞ )。
这样就能过完美解决这样一个情况了
对于不需要恰好转满背包的情况,我们最终的结果是 d p [ V ] dp[V] dp[V] ,而获取这一结果我们只需要前面 d p [ V − C N ] dp[V - C_N] dp[V−CN] 的结果,接着向前追随就只需要 d p [ V − C N − C N − 1 ] dp[V - C_N - C_{N-1}] dp[V−CN−CN−1] 的结果,由此我们就能在一定程度上简化计算。
优化后的伪代码如下:
dp[0...V] ← 0
for i ← 1 to N // 依次遍历N件物品
bound = max(Ci, sum(Ci+1, Ci+2, ..., CN))
for v ← V to bound // 能装下当前物品的背包使用状态方程
dp[v] ← max{dp[v], dp[v-Ci]+Wi}
代码部分:
int knapsack01(vector<int> weight, vector<int> value, int volume){
vector<int> dp(volume + 1);
for(int i = 0; i < weight.size(); i++){
for(int v = volume; v >= weight[i]; v--){
dp[v] = max(dp[v], dp[v-weight[i]] + value[i]);
}
}
return dp[volume];
}
输入样例:
vector<int> weight = {4, 3, 1};
vector<int> value = {3000, 2000,1500};
int volume = 4
输出结果:
3500