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

Posted wtyuan

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了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}(Vsum{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)四件物品。拆分的核心出发点就是,原先的(1cdots n_i)之间的任意整数都能用相应的系数组合而成。这样的话,复杂度就可以降低为(mathcal{O}(Vsum{lceil log{n_i} ceil}))

  另外,若(n_i imes v_i>V)时,针对物品(i)可以看作完全背包问题,可以用完全背包的复杂度为(mathcal{O}(NV))算法求解物品(i)

直接转化为01背包求解

Result: TLE

#include <stdio.h>
#include <string.h>
#include <algorithm>

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
", opt[cash]);
    }
    return 0;
}

二进制转化+完全背包优化

  程序中01背包和完全背包都优化了空间复杂度,只需要定义opt为一维数组即可。二维数组opt到一维数组opt的空间优化没那么好理解,加一点注解。

  01背包状态转移方程:
[ egin{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次循环的值,与状态转移方程不符。

  完全背包状态转移方程:
[ egin{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 <stdio.h>
#include <string.h>
#include <algorithm>

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
", opt[cash]);
    }
    return 0;
}

参考:

[1] 背包问题九讲 2.0

以上是关于POJ1276 多重背包(01背包 完全背包)的主要内容,如果未能解决你的问题,请参考以下文章

poj 1276 Cash Machine(多重背包)

POJ1276(多重背包)

POJ 1276 Cash Machine(多重背包的二进制优化)

POJ_1276_多重背包

POJ - 1276 Cash Machine(多重背包)

poj 1276(多重背包+最接近)