背包问题——4. 多重背包

多重背包问题

题目描述

有 N 种物品和一个容量是 V 的背包。

第 i 种物品最多有 si 件,每件体积是 vi,价值是 wi。

求解将哪些物品装入背包,可使物品体积总和不超过背包容量,且价值总和最大。输出最大价值。

输入格式
第一行两个整数,N,V,用空格隔开,分别表示物品种数和背包容积。

接下来有 N 行,每行三个整数 vi,wi,si,用空格隔开,分别表示第 i 种物品的体积、价值和数量。

输出格式
输出一个整数,表示最大价值。

数据范围

Case1: 0<N,V≤100 ,0<vi,wi,si≤100
Case2: 0<N≤1000,0<V≤2000,0<vi,wi,si≤2000

输入样例

4 5
1 2 3  体积 价值 数量
2 4 1
3 4 3
4 5 2

输出样例:

10

分析:

完全背包每个物品可用无限次,因此只有一个限制条件:k*v<=V,选用的次数k受限于背包的体积;
多重背包中每个物品有s个,因此有两个限制条件,k<=s && k*v<=V,k又受限于物品本身的个数。

动态转移方程满足:dp[i][j]=max{dp[i-1][ j-k×v[i] ] + k×w[i]},其中 0 ≤ k×v[i] ≤ j && k<=s

k<=s时但因为 k*v<=V而退出时,说明物品本身足够多,相当于完全背包问题,
但是当因为k>s而退出时,说明物品个数过少,就是多重背包问题了。
现在我们考虑多重背包的问题。即每个物品个数s都比较小,可以将该物品全部装入背包,即s*v<=V,k仅受限于物品个数s的情况:

如果物品个数s足够大,在还没到s就有k*v<=V,所以可以存在中途退出的情况。

二维数组实现

#include <iostream>
#define read(x) scanf("%d",&x)
#define rep(i,a,b) for (int i=a;i<=b;i++)

using namespace std;

const int maxn=110,maxv=110;
int v[maxn],w[maxn],s[maxn];
int dp[maxn][maxv];

int main() {
    
    
    int N,V;
    read(N),read(V);
    rep(i,1,N) read(v[i]),read(w[i]),read(s[i]);

    rep(i,1,N)
        rep(j,1,V)
            for (int k=0;k<=s[i] && k*v[i]<=j;k++)
                dp[i][j]=max(dp[i][j],dp[i-1][j-k*v[i]]+k*w[i]);

    printf("%d",dp[N][V]);

    return 0;
}

但是这样的做法时间复杂度较高,时间复杂度O(NVS),只能通过Case1的数据,Case2的数据会TLE,若我们采用和完全背包一样的方式优化,当某个物品个数s足够多时,它就是完全背包,可以优化,但是当某个物品个数s比较少时呢?

假设第i个物品体积为v,价值为w,有s个,将其装入体积为j的最大价值:
f[i,j]=MAX { f[i−1,j−k∗v]+k∗w } ,k=0,1,…,s 展开,如下:

上面展开式子的前提是:(s+1)*v<=j,即s*v<=j-v,解释:体积为j的背包可以装下s+1个该物品,即体积为j-v的背包可以装下s个该物品。

这样的情况下就不能像完全背包那样优化出来:dp[i][j]=max(dp[i-1][j],dp[i][j-v]+w)
因为完全背包中是不管怎么样的物品都可以优化出这样的结果,而完全背包中存在可以这样优化的物品,也存在不可以这么优化的物品,不全部符合。

二进制优化

核心:把一个体积为v、价值为w、个数为s的物品拆成k个,这k个物品是完全等同的,之间没有差别,不存在顺序问题。

思考二进制数:11111111,8个1,即代表十进制数255,通过这8个位置取0还是取1,可以描述0~255之间的全部数。
这8个1分别代表1、2、4、8、16、32、64、128,即通过选择这8个数中的任意几个,就可以组合出0~255之间的全部数,全选就是255,全不选就是0。

因此对于十进制数N,可以将其拆成 1,2,4,……,2(k-1) ,N-(2k-1) , 注:这是拆成了k+1个数
他们之间任意组合就可以组成0~N之间的全部数。

前k个数可以凑出 0 到 2k-1 -1 之间的任何数据,再加上N-(2k-1) 可以凑出0到N之间的任何数据。

假设s=200,那么就可以拆成200=1+2+4+8+16+32+64+73,这些值就是权重,比例系数。
那么就可以将这个体积为v、价值为w、个数为s的物品拆成8个物品:

v[1]=v, w[1]=w
v[2]=2v,w[2]=2w
v[3]=4v,w[3]=4w
v[4]=8v,w[4]=8w
v[5]=16v,w[5]=16w
v[6]=32v,w[6]=32w
v[7]=64v,w[7]=64w
v[8]=73v,w[8]=73w

这新拆分出来的8个物品都是可选可不选,对每个物品都这么拆分,因此就转化成了01背包问题。

这样对于个数为s的物品,可以划分为 log(s) 上取整个单一物品,时间复杂度从O(NVS)到O(NVlogS)。

算法实现

注意到Case2的数据范围: 0<N≤1000,0<V≤2000,0<vi,wi,si≤2000
本来最多有1000个物品,每个物品体积最大为2000,对于体积为2000的物品,最多拆分成log(2000)+1个,所以真实的物品个数是二者的乘积。

vi的最大值为2000,log2000即log(2×1000)=log2+log1000=1+3×log10,
log10大于3,小于4,所以log2000大于10,小于13。

#include <iostream>
#define read(x) scanf("%d",&x)

using namespace std;

const int maxn=1010,maxv=2010;
int v[maxn*14],w[maxn*14];
int dp[maxv];              

int main()
{
    
    
    int N,V;
    read(N),read(V);
    //边输入边预处理,将每个物品进行拆分
    int a,b,s;
    int idx=0;  "idx指向数组的真实的最后一个位置"
    for (int i=1;i<=N;i++) {
    
    
        read(a),read(b),read(s);
        //进行拆分
        int k=1;
        while (k<=s) {
    
    
            v[++idx]=k*a,w[idx]=k*b;//拆分装入背包
            s-=k,k*=2;
        }
        if (s) v[++idx]=s*a,w[idx]=s*b;
        //不是2^k的话,最后还会剩下一个数
    }
    N=idx;//拆分后共有idx个物品
    for (int i=1;i<=N;i++)
        for (int j=V;j>=v[i];j--)
            dp[j]=max(dp[j],dp[j-v[i]]+w[i]);
    printf("%d",dp[V]);

    return 0;
}

也可以不先拆分,再处理,可以边拆分边处理,不用数组额外维护了:背包问题——混合背包
这样的代价是价值w数组里的数就没用了,不具有输入时的含义了,再处理的时候被破坏了,记录着最后一次拆分的结果。

单调队列优化

待更新。

猜你喜欢

转载自blog.csdn.net/HangHug_L/article/details/114259532