多重背包问题
题目描述
有 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数组里的数就没用了,不具有输入时的含义了,再处理的时候被破坏了,记录着最后一次拆分的结果。
单调队列优化
待更新。