一、题目
有 N 种物品和一个容量为 V 的背包,每种物品都有无限件可用
放入第 i 种物品的耗费的空间是 Ci,得到的价值是 Wi
求解:
将哪些物品装入背包,可使这些物品的耗费的空间总和不超过背包容量,且价值总和最大
二、基本思路
这个问题非常类似于01背包问题,所不同的是每种物品有无限件
也就是从每种物品的角度考虑,与它相关的策略已并非取或不取两种
而是有取 0 件、取 1 件、取 2 件……直至取 ⌊V / Ci⌋ 件等很多种
如果仍然按照解01背包时的思路,令 dp[i, v] 表示前i种物品恰放入一个容量为 v 的背包的最大权值
仍然可以按照每种物品不同的策略写出状态转移方程
像这样:
dp[i, v] = max{dp[i − 1, v − kCi] + kWi | 0 ≤ kCi ≤ v}
这跟01背包问题一样有O(V * N)个状态需要求解
但求解每个状态的时间已经不是常数了,求解状态 dp[i, v] 的时间是 O( v / Ci)
总的复杂度可以认为是 O(N * V * Σv/Ci ),是比较大的
将01背包问题的基本思路加以改进,得到了这样一个清晰的方法
这说明01背包问题的方程的确是很重要,可以推及其它类型的背包问题
但我们还是要试图改进这个复杂度
三、一个简单有效的优化
若两件物品 i、 j 满足 Ci ≤ Cj 且 Wi ≥ Wj ,则将可以将物品 j 直接去掉,不用考虑
这个优化的正确性是显然的:
任何情况下都可将价值小耗费高的j换成物美价廉的 i,得到的方案至少不会更差
对于随机生成的数据,这个方法往往会大大减少物品的件数,从而加快速度
然而这个并不能改善最坏情况的复杂度,因为有可能特别设计的数据可以一件物品也去不掉
这个优化可以简单的O(N ^ 2)地实现,一般都可以承受
另外,针对背包问题而言,比较不错的一种方法是:
首先将费用大于V 的物品去掉,然后使用类似计数排序的做法,计算出费用相同的物品中价值最高的是哪个
可以O(V + N)地完成这个优化
四、转化为01背包问题求解
考虑到第 i 种物品最多选 V / Ci 件
于是可以把第 i 种物品转化为 V / Ci 件费用及价值均不变的物品,然后求解这个01背包问题
这样的做法完全没有改进时间复杂度
但这种方法也指明了将完全背包问题转化为01背包问题的思路:
将一种物品拆成多件只能选 0 件或 1 件的01背包中的物品
更高效的转化方法是:
把第 i 种物品拆成费用为 Ci * 2^k、价值为 Wi * 2^k 的若干件物品,其中 k 取遍满足 Ci * 2^k ≤ V 的非负整数
这是二进制的思想
因为,不管最优策略选几件第 i 种物品,其件数写成二进制后,总可以表示成若干个 2^k 件物品的和
这样一来就把每种物品拆成 O(log(V / Ci) ) 件物品,是一个很大的改进
五、O(V * N)的算法
这个算法使用一维数组,先看伪代码:
dp[0..V ] = 0
for i = 1 to N
for v = Ci to V
dp[v] = max(dp[v], dp[v − Ci] + Wi)
你会发现,这个与01背包问题的代码只有v的循环次序不同而已
为什么这个算法就可行呢?
首先想想为什么01背包中要按照 v 递减的次序来循环
让v递减是为了保证第 i 次循环中的状态 dp[i, v]是由状态 dp[i − 1, v − Ci]递推而来
换句话说,这正是为了保证每件物品只选一次
保证在考虑“选入第 i 件物品”这件策略时,依据的是一个绝无已经选入第 i 件物品的子结果 dp[i −1, v − Ci]
而现在完全背包的特点恰是每种物品可选无限件
所以在考虑“加选一件第 i 种物品”这种策略时,却正需要一个可能已选入第 i 种物品的子结果 dp[i, v − Ci]
所以就可以并且必须采用 v 递增的顺序循环
这就是这个简单的程序为何成立的道理
值得一提的是,上面的伪代码中两层for循环的次序可以颠倒
这个结论有可能会带来算法时间常数上的优化
这个算法也可以由另外的思路得出
例如,将基本思路中求解 dp[i, v − Ci]的状态转移方程显式地写出来
代入原方程中,会发现该方程可以等价地变形成这种形式:
dp[i, v] = max(dp[i − 1, v], dp[i, v − Ci] + Wi)
将这个方程用一维数组实现,便得到了上面的伪代码
最后抽象出处理一件完全背包类物品的过程伪代码:
def CompletePack(dp, C, W)
for v = C to V
dp[v] = max{dp[v], dp[v − C] + W}
六、小结
完全背包问题也是一个相当基础的背包问题,它有两个状态转移方程
希望能够对这两个状态转移方程都仔细地体会
不仅记住,也要弄明白它们是怎么得出来的,最好能够自己想一种得到这些方程的方法
事实上,对每一道动态规划题目都思考其方程的意义以及如何得来
是加深对动态规划的理解、提高动态规划功力的好方法
转自《背包九讲V_2.0》%%%作者,感谢作者
#include <iostream>
#include <cstdio>
#include <cstring>
using namespace std;
const int INF = 0x3f3f3f3f;
const int maxn = 2000 + 10;
int C[maxn];
int W[maxn];
int dp[50010];
int main()
{
#ifdef __AiR_H
freopen("in.txt", "r", stdin);
#endif
int N;
scanf("%d", &N);
while (N--) {
int M, V;
scanf("%d%d", &M, &V);
for (int i = 1; i <= M; ++i) {
scanf("%d%d", &W[i], &C[i]);
}
dp[0] = 0;
for (int i = 1; i <= V; ++i) {
dp[i] = -INF;
}
for (int i = 1; i <= M; ++i) {
for (int j = W[i]; j <= V; ++j) {
dp[j] = max(dp[j], dp[j-W[i]] + C[i]);
}
}
if (dp[V] >= 0) {
printf("%d\n", dp[V]);
} else {
printf("NO\n");
}
}
return 0;
}