背包问题详解

动态规划是神器,背包问题又是动态规划中的最主要一类,必须好好学学。

 

这个公式就是背包问题的状态转移方程,具有普适性。

F[i, v]表示前 i 种(注意一种和一件的区别) 物品在背包容量为 v 的情况下所获得的最大价值。

Ci表示第 i 种物品的费用(即占用背包容量)

Wi表示第 i 种物品的价值

k 表示 第 i 种物品的可选择的件数,M 表示第 i 种物品可选择的最大件数,即对应 第 i 种物品有 0,1,2,3,...Mi共 (Mi + 1)种情况。

上述公式的含义在于:前 i 种物品在背包容量为 v 时的最大价值  F[i, v] 是由 前 (i - 1)种物品的最大价值的基础上 加上 第 i  种物品对应的 (Mi + 1)种情况所组成的价值中的最大者。

1. 01背包

1.1 基本方法

M = 1 对应01背包问题,此时每种物品只有 不选(k = 0) 和选中(k = 1) 两种情况。

“将前 i 种物品放入容量为 v 的背包中”这个子问题,若只考虑第 i 种物品的策略(放或不放),那么就可以转化为一个只和前 i − 1 种物品相关的问题。

如果不放第 i 种物品(k = 0),那么问题就转化为“前 i − 1 种物品放入容量为 v 的背包中”,相应价值为 F [i − 1, v];

如果放第 i 种物品(k = 1),那么问题就转化为“前 i − 1 种物品放入剩下的容量为 v − Ci 的背包中”,此时能获得的最大价值就是 F [i − 1, v − Ci ] 再加上通过放入第 i 种物品获得的价值 Wi 。

伪代码如下:

时间复杂度和空间复杂度均为O(NV)

我们利用二维数组 F[i, v]即可求解。

我们看到变量 v 循环方向是 正向增大方向,这里我们利用一个表格来手工地大概模拟一下算法

F[0, 0] F[0, 1] F[0, 2] F[0, 3] F[0, 4]
F[1, 0] F[1, 1] F[1, 2] F[1, 3] F[1, 4]
F[2, 0] F[2, 1] F[2, 2] F[2, 3] F[2, 4]
F[3, 0] F[3, 1] F[3, 2] F[3, 3] F[3, 4]
F[4, 0] F[4, 1] F[4, 2] F[4, 3] F[4, 4]

 

 

 

 

       

我们假设求解F[2, 2] ,则 F[2, 2] = max{ F[1, 2], F[1, 2 - C2] + W2},

我们可以看到每求一个F[i, v] 都要依赖于 其正上方数 F[i - 1, v] 和其左上方数 F[i - 1,v - Ci]

(因为v - Ci < v),而我们两层循环中,循环变量 i 和 v均是正向增大方向,可以保证计算顺序沿右下角进行,并且可以保证计算的正确性。

 

1.2 空间优化方法

时间复杂度O(NV)已经不可以再优化,空间复杂度O(NV)可以优化为O(V)。具体如下:

 

我们可以看到,两个伪代码之间的唯一区别就是 循环变量 v 的变化方向。

因为状态转移方程中确定了 每计算一个新值F[v]时都要依赖于其左边值(v - Ci < v),

如果我们将 v 正向循环,则左边值F[v - Ci]将先计算出,新的左边值有可能覆盖掉原左边值(因为是一维数组),

所以计算F[v]时本应该依赖的原F[v]有可能已经被覆盖,导致了错误的依赖。

我们将01背包的计算程序抽象出来,如下所示:

 1 #include <iostream>
 2 #include <vector>
 3 #include <assert.h>
 4 #include <algorithm>
 5 using namespace std;
 6 
 7 /* 01背包:每种物品仅有一个,可以选择装或者不装
 8 定义状态 f[i][j]表示 把前i个物品装进容量为j的背包可以获得的最大价值
 9 状态转移方程:
10 f[i][j] = max{f[i - 1][j], f[i - 1][j - Wi] + Vi}
11 将第i个物品装进容量为j的背包时,有两种情况:
12 第i个不装进去,这时获得的价值为:f[i - 1][j]
13 第i个装进去,这时获得的价值为:f[i - 1][j - Wi] + Vi
14 */
15 int ZeroOnePack1(const std::vector<int>& weight, const std::vector<int>& value, const int totalWeight) {
16     assert(weight.size() == value.size() && totalWeight > 0);
17     std::vector<std::vector<int>> f(weight.size() + 1, std::vector<int>(totalWeight + 1, 0));
18     for (int i = 1; i <= weight.size(); ++i) {   
19         for (int j = weight.at(i - 1); j <= totalWeight; ++j) {
20                 f.at(i).at(j) = std::max(f.at(i - 1).at(j), f.at(i - 1).at(j - weight.at(i - 1)) + value.at(i - 1));
21         }
22     }
23     return f.at(weight.size()).at(totalWeight);
24 }
25 
26 int ZeroOnePack2(const std::vector<int>& weight, const std::vector<int>& value, const int totalWeight) {
27     assert(weight.size() == value.size() && totalWeight > 0);
28     std::vector<int> f(totalWeight + 1, 0);
29     for (int i = 1; i <= weight.size(); ++i) {
30         for (int j = totalWeight; j >= weight.at(i - 1); --j) {
31             f.at(j) = std::max(f.at(j), f.at(j - weight.at(i - 1)) + value.at(i - 1));
32         }
33     }
34     return f.at(totalWeight);
35 }
36 
37 int main() {
38     // your code goes here
39     const std::vector<int> value = {1, 2, 3, 4, 5};
40     const std::vector<int> weight = {5, 4, 3, 2, 1};
41     int maxValue1 = ZeroOnePack1(weight, value, 10);
42     int maxValue2 = ZeroOnePack2(weight, value, 10);
43     std::cout << maxValue1 << std::endl << maxValue2;
44     return 0;
45 }

 

2.完全背包

Mi = [V/Ci](取下整) 对应完全背包,即每种物品均可以无限取(当然,背包容量有限制)

2.1 转化为01背包

01背包中 每种物品只能有 0(不选) 和 1(选中),而完全背包中,每种物品Ti最多有[V/Ci]件,我们可以把这[V/Ci]件物品 等效于 [V/Ci]种 费用为Ci,价值为Wi 的物品Si,最后这[V/Ci]种物品Si的选择情况列表中1(表示选中)的个数的总和就是物品Ti被选中的件数K值。

伪代码如下:

2.2  一维数组解法

伪代码如下:

 

我们可以看到:完全背包和01背包两个伪代码之间的唯一区别就是 循环变量 v 的变化方向

我们假设要求出F[i, v - Ci]:

我们已经知道背包问题的状态转移方程,如下所示:

 

因此,我们可以得到:

这里,我们可以在上述公式两边加上一个数:

由此,我们把上式代入到原来的状态转移方程,我们可以得到完全背包的另一个等效的状态转移方程:

为什么这个算法可行呢? 首先想想为什么 01背包中内循环要逆序?

逆序是为了保证 每个物品只选1次,保证在 “选择第i件物品”时,依赖的是一个没有选择第i件物品的子结果f[i-1][j-Wi]

现在完全背包的特点是每种物品可选无限个,没有了每种物品只能选一次的限制,所以就可以并且必须采用内循环递增的顺序

 1 #include <iostream>
 2 #include <vector>
 3 #include <assert.h>
 4 #include <algorithm>
 5 using namespace std;
 6 
 7 int CompletePack1(const std::vector<int>& weight, const std::vector<int>& value, const int totalWeight) {
 8     assert(weight.size() == value.size() && totalWeight > 0);
 9     std::vector<int> f(totalWeight + 1, 0);
10     for (int i = 1; i <= weight.size(); ++i) {
11         for (int j = totalWeight; j >= weight.at(i - 1); --j) {
12             for (int k = 0; k * weight.at(i - 1) <= j; ++k) {
13                 f.at(j) = std::max(f.at(j), f.at(j - k * weight.at(i - 1)) + k * value.at(i - 1));
14             }
15         }
16     }
17     return f.at(totalWeight);
18 }
19 
20 int CompletePack2(const std::vector<int>& weight, const std::vector<int>& value, const int totalWeight) {
21     assert(weight.size() == value.size() && totalWeight > 0);
22     std::vector<int> f(totalWeight + 1, 0);
23     for (int i = 1; i <= weight.size(); ++i) {
24         for (int j = weight.at(i - 1); j <= totalWeight; ++j) {
25             f.at(j) = std::max(f.at(j), f.at(j - weight.at(i - 1)) + value.at(i - 1));
26         }
27     }
28     return f.at(totalWeight);
29 }
30 
31 int main() {
32     // your code goes here
33     const std::vector<int> weight = {3, 4, 5};
34     const std::vector<int> value = {4, 5, 6};
35     int maxValue1 = CompletePack1(weight, value, 10);
36     int maxValue2 = CompletePack2(weight, value, 10);
37     std::cout << maxValue1 << std::endl << maxValue2;
38     return 0;
39 }

 

3.多重背包

 

多重背包与完全背包极为相似,只是每种物品都有一个相应的最大选择件数(该最大选择件数不一定为[V/Ci], 区别于完全背包中每种物品都可以无限取到装满背包为止)

基本方法就是转化为01背包问题:把第 i 种物品换成 Mi 件 01背包中的物品,则得到了物品数为 ΣMi 的 01 背包问题。直接求解之,复杂度是O(V ΣMi )。

 1 #include <iostream>
 2 #include <vector>
 3 #include <assert.h>
 4 #include <algorithm>
 5 using namespace std;
 6 
 7 int MultiPack(const std::vector<int>& weight, const std::vector<int>& value, const std::vector<int>& num, const int totalWeight) {
 8     assert(weight.size() == value.size() && value.size() == num.size() && totalWeight > 0);
 9     std::vector<int> f(totalWeight + 1, 0);
10     for (int i= 1; i <= weight.size(); ++i) {
11         for (int j = totalWeight; j >= weight.at(i - 1); --j) {
12             for (int k = 0; k <= std::min(num.at(i - 1), j / weight.at(i - 1)); ++k) {
13                 f.at(j) = std::max(f.at(j), f.at(j - k * weight.at(i - 1)) + k * value.at(i - 1));
14             }
15         }
16     }
17     return f.at(totalWeight);
18 }
19 
20 int main() {
21     // your code goes here
22     const std::vector<int> weight = {2, 4};
23     const std::vector<int> value = {100, 100};
24     const std::vector<int> num = {4, 2};
25     int maxValue = MultiPack(weight, value, num, 8);
26     std::cout << maxValue;
27     return 0;
28 }

注意K值的取值范围: 

k <= std::min(num.at(i - 1), j / weight.at(i - 1))

4.混合背包问题

4.1  01背包和完全背包混合

01 背包和完全背包中的伪代码变量 v 的循环方向不同,故该混合背包伪代码可以如下:(注意:代码中 "第  i 件物品" 应该改为 "第 i 种物品").复杂度是 O(V N )

 

4.2  3种背包全混合

 

5. 二维费用背包问题

(二维费用只是费用状态多加了一维,其原理和普通的一维费用背包的原理完全相同)

二维费用的背包问题是指:对于每种物品,具有两种不同的费用,选择这种物品必须同时付出这两种费用。对于每种费用都有一个可付出的最大值(背包容量)。问怎样选择物品可以得到最大的价值。

设第 i 种物品所需的两种费用分别为 Ci 和 Di 。两种费用可付出的最大值(也即两种背包容量)分别为 V 和 U 。物品的价值为 Wi 。

设 F [i, v, u] 表示前 i 种物品付出两种费用分别为 v 和 u 时可获得的最大价值。状态转移方程就是:

5.1  二维费用01背包

基本方法:可以使用三维数组,变量 i , v,u 均为正向增大方向循环 

空间优化方法:可以使用二维数组,变量 i 为 正向增大方向循环, 变量 v 和 u 为逆向减小方向循环(原理和一维费用01背包的原理完全相同)

 

5.2 二维费用完全背包

可以使用二维数组,变量 i 为 正向增大方向循环, 变量 v 和 u 为正向增大方向循环(原理和一维费用完全背包的原理完全相同)

 

待完结

欢迎大家多多批评指正,互相学习,共同进步

主要参考:<背包问题九讲>

欢迎大家转载,转载请注明出处。谢谢:-)

posted @ 2013-04-01 00:49  skyline09  阅读(1270)  评论(2编辑  收藏  举报