背包9讲
背包9讲
https://github.com/tianyicui/pack
刘汝佳 9.3.2 01背包的两种递推公式都是基于对第i件物品的策略(放或不放)
第2种递推
用f(i,j)表示“把前i个物品装到容量为j的背包中的最大总重量”,其状态转移方程也不难得出:
f(i, j) = max{f(i-1,j), f(i-1, j-V[i]) + W[i]}
边界是类似的:i=0时为0,j<0时为负无穷,最终答案为f(n,C)。代码也是类似的:
for(int i = 1; i <= n; i++)
for(int j = 0; j <= C; j++){
f[i][j] = (i==1 ? 0 : f[i-1][j]);
if(j >= V[i]) f[i][j] = max(f[i][j], f[i-1][j-V[i]]+W[i]);
}
以上代码中 i==1的时候f[i][j] 初始化为0,相当于事先在双层for循环之前初始化所有f[0][j] = 0,然后再这里直接赋值 f[i][j] = f[i-1][j]
使用滚动数组时,j需要逆序遍历,原因如下,参考 http://blog.csdn.net/insistgogo/article/details/8579597
优化空间复杂度 上述的方法,我们使用二维数组 f[i][v] 保存中间状态,这里我们可以使用一维数组f[v]保存中间状态就能得到结果
分析 我们现在使用f[v]保存中间状态,我们想要达到的效果是,第i次循环后,f[v]中存储的是前i个物体放到容量v时的最大价值 在回顾下之前讲过的状态转移方程: f[i][v] = max(f[i - 1][v],f[i - 1][v - weight[i]] + cost[i]) 我们可以看到,要想得到 f[i][v],我们需要知道 f[i - 1][v] 和 f[i - 1][v - weight[i]],由于我们使用二维数组保存中间状态,所以可以直接取出这两个状态。 当我们使用一维数组存储状态时,f[v]表示,在执行i次循环后(此时已经处理i个物品),前i个物体放到容量v时的最大价值,即之前的f[i][v]。与二维相比较,它把第一维隐去了,但是二者表达的含义还是相同的,只不过针对不同的i,f[v]一直在重复使用,所以,也会出现第i次循环可能会覆盖第i - 1次循环的结果。 为了求f[v],我们需要知道,前i - 1个物品放到容量v的背包中带来的收益,即之前的f[i - 1][v] 和 前i - 1件物品放到容量为v - weight[i]的背包中带来的收益,即之前的f[i - 1][v - weight[i]] + cost[i]。
难点 由于我们只使用一维数组存储,则在求这两个子问题时就没有直接取出那么方便了,因为,第i次循环可能会覆盖第i - 1次循环的结果。 现在我们来求这两个值 1)前i - 1个物品放到容量v的背包中带来的收益,即之前的f[i - 1][v] : 由于,在执行在i次循环时,f[v]存储的是前i个物体放到容量v时的最大价值,在求前i个物体放到容量v时的最大价值(即之前的f[i][v])时,我们是正在执行第 i 次循环,f[ v ]的值还是在第 i - 1 次循环时存下的值,在此时取出的 f[ v ]就是前i - 1个物体放到容量v时的最大价值,即f[i - 1][v]。 2)前i - 1件物品放到容量为v - weight[i]的背包中带来的收益,即之前的f[i - 1][v - weight[i]] + cost[i] 由于,在执行第i次循环前,f[0 ~ V]中保存的是第i - 1次循环的结果,即是前i - 1个物体分别放到容量0 ~ V时的最大价值,即f[i - 1][0 ~ V]。 则,在执行第i次循环前,f 数组中v - weight[i]的位置存储就是我们要找的 前i - 1件物品放到容量为v - weight[i]的背包中带来的收益 (即之前的f[i - 1][v - weight[i]]),这里假设物品是从数组下标1开始存储的。
伪代码 for i=1..N //枚举物品 for v=V..0 //枚举容量,从大到小 f[v]=max{f[v],f[v-weight[i]] + cost[i]}; 由上面伪代码可知,在执行第 i 次循环时,需要把背包容量由V..0都要遍历一遍,检测第 i 件物品是否能放。
逆序枚举容量的原因 注意一点,我们是由第 i - 1 次循环的两个状态推出 第 i 个状态的,而且 v > v - weight[i],则对于第i次循环,背包容量只有当V..0循环时,才会先处理背包容量为v的状况,后处理背包容量为 v-weight[i] 的情况。 具体来说,由于,在执行v时,还没执行到v - weight[i]的,因此,f[v - weight[i]]保存的还是第i - 1次循环的结果。即在执行第i次循环 且 背包容量为v时,此时的f[v]存储的是 f[i - 1][v] ,此时f[v-weight[i]]存储的是f[i - 1][v-weight[i]]。 相反,如果在执行第 i 次循环时,背包容量按照0..V的顺序遍历一遍,来检测第 i 件物品是否能放。此时在执行第i次循环 且 背包容量为v时,此时的f[v]存储的是 f[i - 1][v] ,但是,此时f[v-weight[i]]存储的是f[i][v-weight[i]]。 因为,v > v - weight[i],第i次循环中,执行背包容量为v时,容量为v - weight[i]的背包已经计算过,即f[v - weight[i]]中存储的是f[i][v - weight[i]]。即,对于01背包,按照增序枚举背包容量是不对的。 |
另外,可以用图形形象化的说明逆序遍历容量的原因
不管正序还是逆序,v在一次for循环中设置了f[0...V]中的所有元素,可以看做从左到右(正序)或从右到左(逆序)对数组f[0...V]刷了颜色,每次for循环使用不同的颜色
如果是正序,因为v - weight[i] < v,在设置f[v]的时候已经设置过f[v - weight[i]],f[v - weight[i]]的颜色已经更新了,代表f[i][v - weight[i]],错误
如果是逆序,在设置f[v]的时候,f[v - weight[i]]的颜色还没更新,代表f[i-1][v - weight[i]],正确
http://poj.org/problem?id=3624 是01背包问题,需要滚动数组,以减少内存消耗
#include <cstdio> #include <cstring> #include <cmath> #include <algorithm> using namespace std; int main() { int N, M; //1 <= N <= 3402, 1 <= M <= 12880 //int W[12900]; //int D[12900]; int W, D; int f[12900]; while (scanf("%d%d", &N, &M) != EOF ){ // 如果这里的 != EOF 去掉,提交的时候出现 Output Limit Exceeded /* for (int i = 1; i <= N; i++){ scanf("%d%d", &W[i], &D[i]); } */ memset(f, 0, sizeof(f)); for (int i = 1; i <= N; i++) { scanf("%d%d", &W, &D); for (int j = M; j >= 0; j--){ if (j >= W) f[j] = max(f[j], f[j - W] + D); } } printf("%d\n", f[M]); } return 0; }
例题9-5 劲歌金曲 UVa 12563
参考 http://blog.csdn.net/u013480600/article/details/40376143
这里对01背包里公式里面的数组f[]的类型,max函数,和状态切换进行抽象,
在传统的01背包中,数组f[]的类型是 int,代表容量这一个单一值,max函数用来比较容量的大小
转移方程是 f[i][v] = max(f[i - 1][v],f[i - 1][v - weight[i]] + cost[i])
如果用滚动数组 f[v] = max(f[v],f[v - weight[i]] + cost[i])
f[v - weight[i]] + cost[i] 代表一个新的状态,max()函数用于在两个状态中选择一个最优的
劲歌金曲问题里面,数组f[]的类型包含两个int,分别代表歌曲的长度和歌曲的数量,自定义opt函数来比较两个状态
#define _CRT_SECURE_NO_WARNINGS #include <cstdio> #include <cstring> #include <cmath> #include <algorithm> using namespace std; // 参考 http://blog.csdn.net/u013480600/article/details/40376143 struct songState { int len; int num; songState() : len(0), num(0){} songState(int l, int n) :len(l), num(n) {} }; songState opt(songState s1, songState s2) { if ((s1.num > s2.num) || ((s1.num == s2.num) && (s1.len > s2.len))) return s1; else return s2; } int main() { int T; // T <= 100 int n, max_t; // 1 <= n <= 50, 1 <= max_t <= 1000000000 int t[55]; songState f[180*50 + 5]; int kase = 1; scanf("%d", &T); while (T--) { scanf("%d%d", &n, &max_t); max_t -= 1; int sum = 0; for (int i = 1; i <= n; i++) { scanf("%d", &t[i]); sum += t[i]; } if (sum < max_t) max_t = sum; memset(f, 0, sizeof(f)); for (int i = 1; i <= n; i++){ for (int j = max_t; j >= 0; j--){ if (j >= t[i]){ f[j] = opt(f[j], songState(f[j - t[i]].len + t[i], f[j - t[i]].num + 1)); } } } printf("Case %d: %d %d\n", kase++, f[max_t].num + 1, f[max_t].len + 678); } return 0; }