POJ1276
题目
多重背包模板题,给定背包容量\(V\),给定\(N\)种物品,每种物品的个数\(n_i\)、体积\(v_i\)和重量\(w_i\)已知,求背包能装下的物品的最大重量。对应本题就是,给定提款的金额cash,给定\(N\)种钱币,每种钱币的个数为\(n_i\)、面额\(D_i\)已知,求能兑换的钱币的最大值。本题中,体积和重量都等于面额。
Sample Input
735 3 4 125 6 5 3 350
633 4 500 30 6 100 1 5 0 1
735 0
0 3 10 100 10 50 10 10
Sample Output
735
630
0
0
算法思路
刚开始的想法是直接转化为01背包问题来解,即将第\(i\)种物品换成\(n_i\)件01背包中的物品,则得到了物品数为\(\sum{n_i}\)的01背包问题,直接求解复杂度为\(\mathcal{O}(V\sum{n_i})\),测试表明会TLE。
需要对以上的朴素想法进行优化。将第\(i\)件物品参考二进制的思想来拆分,即拆分乘若干件01背包中的物品,每件物品的系数分别为\(2^0, 2^1, 2^2,\cdots,2^{k-1},n_i-2^k+1\),\(k\)是满足\(n_i-2^k+1>0\)的最大整数。例如,若\(n_i=13\),则可以拆分为系数分别为\(1,2,4,6\)四件物品。拆分的核心出发点就是,原先的\(1\cdots n_i\)之间的任意整数都能用相应的系数组合而成。这样的话,复杂度就可以降低为\(\mathcal{O}(V\sum{\lceil \log{n_i} \rceil})\)。
另外,若\(n_i\times v_i>V\)时,针对物品\(i\)可以看作完全背包问题,可以用完全背包的复杂度为\(\mathcal{O}(NV)\)算法求解物品\(i\)。
代码
直接转化为01背包求解
Result: TLE
#include
#include
#include
int cash, N;
int denominations[10 * 1000 + 5];
int opt[100000 + 5];
int main() {
while (scanf("%d", &cash) != EOF) {
memset(opt, 0, sizeof(opt));
scanf("%d", &N);
int n, D, ptr = 1;
for (int i = 1; i <= N; i++)
{
scanf("%d %d", &n, &D);
for (int j = 1; j <= n; j++)
denominations[ptr++] = D;
}
for (int i = 1; i < ptr; i++)
for (int j = cash; j >= denominations[i]; j--)
opt[j] = std::max(opt[j], opt[j - denominations[i]] + denominations[i]);
printf("%d\n", opt[cash]);
}
return 0;
}
二进制转化+完全背包优化
程序中01背包和完全背包都优化了空间复杂度,只需要定义opt
为一维数组即可。二维数组opt
到一维数组opt
的空间优化没那么好理解,加一点注解。
01背包状态转移方程:
\[ \begin{equation} opt[i][j] = \max(opt[i-1][j], opt[i-1][j-v_i] + w_i) \end{equation} \]
对于ZeroOnePack
函数的注解:
若外层正处于第i
次循环,内层循环中,j
逆序减小,计算opt[j]
的值时,opt[j]
、opt[j-cost]
存储都是i-1
次循环的值,即分别对应于(1)式中的\(opt[i-1][j]\)、\(opt[i-1][j-v_i]\)。而如果j
正序增大的话,计算opt[j]
时,opt[j-cost]
已经赋过值,存储是i
次循环的值,与状态转移方程不符。
完全背包状态转移方程:
\[ \begin{equation} opt[i][j] = \max(opt[i-1][j], opt[i][j-v_i] + w_i) \end{equation} \]
对于CompletaPack
函数的注解:
若外层正处于第i
次循环,内层循环中,j
正序增大,计算opt[j]
的值时,opt[j]
存储的是i-1
次循环的值,即对应(2)式中的\(opt[i - 1][j]\)。opt[j-cost]
在本次循环中已经赋过值,存储的是第i
次循环的值,对应于(2)式中的\(opt[i][j-v_i]\),与状态转移方程相符。opt[j] = std::max(opt[j], opt[j - cost] + weight)
这个表达式表示,我正在考虑是否往背包中加一件物品i
,而\(opt[i][j-v_i]\)中可能已经包含了若干件物品i
,现在考虑是否再增加物品i
,所以这一点上可以反映完全背包每种物品有无穷多的性质,也正是CompletePack
正确的原因。
Result: 700kB, 32ms
#include
#include
#include
int cash, N;
int opt[100000 + 5];
void CompletaPack(int cost, int weight) {//原理见上文注解
for (int j = cost; j <= cash; j++)
opt[j] = std::max(opt[j], opt[j - cost] + weight);
}
void ZeroOnePack(int cost, int weight) {//原理见上文注解
for (int j = cash; j >= cost; j--)
opt[j] = std::max(opt[j], opt[j - cost] + weight);
}
void MultiplePack(int cost, int weight, int num) {
if (cost * num >= cash) {
CompletaPack(cost, weight);
return;
}
int k = 1;
while (k < num) {//二进制拆分物品
ZeroOnePack(k * cost, k * weight);
num -= k;
k *= 2;
}
ZeroOnePack(num * cost, num * weight);
return;
}
int main() {
while (scanf("%d", &cash) != EOF) {
memset(opt, 0, sizeof(opt));
scanf("%d", &N);
int num, denomination;
for (int i = 1; i <= N; i++)
{
scanf("%d %d", &num, &denomination);
MultiplePack(denomination, denomination, num);
}
printf("%d\n", opt[cash]);
}
return 0;
}
参考:
[1] 背包问题九讲 2.0