POJ1276 多重背包(01背包 完全背包)

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

你可能感兴趣的:(POJ1276 多重背包(01背包 完全背包))