目录
一. 问题描述
现有 N 种不同的物品和一个容量为 V 的背包,已知第 i 件物品最多有
件,每件物品的体积是
,价值是
。要求在有限的背包容量下,使得装入的物品总价值最大。
特点:在01背包基础上增加了具体的物品数量限制,但是与完全背包相比每个物品不是无限选择的。这里区别一下三种背包:
(1)01背包:背包有最大容量C,给出n种物品,每种物品仅仅一个,有自己的重量和价值wi和vi,求背包可装下的最大价值
(2)完全背包:背包有最大容量C,给出n种物品,每种物品无限个,有自己的重量和价值wi和vi,求背包可装下的最大价值
(3)多重背包:背包有最大容量C,给出n种物品,每种物品给出数量m,有自己的重量和价值wi和vi,求背包可装下的最大价值
二. 基本思路与代码
回顾完全背包问题的暴力解法,在背包承重为 j 的前提下,第 i 种物品最多能放 个(这里是整除)。由此可见,对于多重背包问题,只需取
。故对完全背包问题的暴力解法做一点简单修改即可求解多重背包问题。
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1000 + 7;
int v[N], w[N], s[N];
int dp[N][N];
int main(){
int n, m;
cin >> n >> m;
for(int i = 1; i <= n; i ++) cin >> v[i] >> w[i] >> s[i];
for(int i = 1; i <= n; i ++){
for(int j = 0; j <= m; j ++){
int t = min(s[i],j/w[i]);
for(int k = 0; k <= t; k ++){
dp[i][j] = max(dp[i][j], dp[i - 1][j - k * v[i]] + k * w[i]);
}
}
}
cout << dp[n][m] << endl;
return 0;
}
或者说,我们考虑把 s[i] 件物品拆分为 s[i] 种不同的一件物品,转化为01背包去解题,二者本质上是一样的。其代码示例如下:
#include <bits/stdc++.h>
using namespace std;
int a[10005],b[10005];
int dp[10005];
int main()
{
int t = 0;
int n,m;
cin>>n>>m;
while(n--)
{
int w,v,s;
cin>>v>>w>>s;
while(s--)
{
a[++t]=v;
b[t]=w;
}//死拆,把多重背包拆成01背包
}
for(int i=1;i<=t;i++){
for(int j=m;j>=a[i];j--){
dp[j]=max(dp[j-a[i]]+b[i],dp[j]);//直接套01背包的板子
}
}
cout<< dp[m] <<endl;
return 0;
}
三. 二进制优化
1. 优化思路
对于多重背包我们完全可以把他转化为01背包,每一种都分成一个个的来当做01背包处理。但是一个个分的话效率太低,这里考虑二进制处理。这里引入两个定理:
- 任何一个数字 n 都可以拆为
的形式
可以组合为
之间的任意一个数字。比如我们可以用1,2,4,8,…,512 (2^9) 来组合成0~1023内的任意一个数字,至多枚举10次
则根据以上可知, 就可以组合为 1 - n 内的任意一个数字。我们之前暴力求解时将物品拆分为一个个遍历的目的就是组合为 1 - n 内的任意一种可能来取最值,那么目前有了二进制拆分这种更少的拆分次数就可以实现所有数字组合,不仅加快了效率而且这些数字也可以组合为 1 - n 间的任意一个数字表示任意一个数量,两全其美。
举个更形象的例子:要求在一堆苹果选出n个苹果。我们传统的思维是一个一个地去选,选够n个苹果就停止。这样选择的次数就是n次;如果将这一堆苹果分别按照 1,2,4,...,256,512 分到10个箱子里,那么由于任何一个数字 x∈[0,1023] (第11个箱子才能取到1024) 都可以从这10个箱子里的苹果数量表示出来,这样选择的次数就是 ≤10次。
int k = 1;
while(k<m){
ZeroOnePack(w*k,v*k);//01背包(组合物品重量为w*k,价值为v*k)
m-=k;
k*=2;
}
ZeroOnePack(w*m,v*m);
2. 代码实现
(1)多重背包转化为01背包
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 25000;
int f[N], v[N], w[N];
int n, m;
int main(){
cin >> n >> m;
//将每种物品根据物件个数进行打包
int cnt = 0;
for(int i = 1; i <= n; i ++){
int a, b, s;
cin >> a >> b >> s;
int k = 1;
while(k <= s){
cnt ++;
v[cnt] = k * a;
w[cnt] = k * b;
s -= k;
k *= 2;
}
if(s > 0){
cnt ++;
v[cnt] = s * a;
w[cnt] = s * b;
}
}
//多重背包转化为01背包问题
for(int i = 1; i <= cnt; i ++){
for(int j = m; j >= v[i]; j --){
f[j] = max(f[j], f[j - v[i]] + w[i]);
}
}
cout << f[m] << endl;
return 0;
}
(2)拆分为完全背包和01背包
- 若w * k >= C(该物品总重量>=背包总重):可以看作为完全背包处理
- 若w * k < C : 当做01背包处理
#include <iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<string>
using namespace std;
const int maxn = 10000 + 5;
int best[maxn];
int n,C;
void CompletePack(int w,int v)//完全背包(顺序)
{
for(int i = w;i<=C;i++){
best[i] = best[i]>best[i - w]+v?best[i]:best[i-w]+v;
}
}
void ZeroOnePack(int w,int v)//01背包(逆序)
{
for(int i = C;i>=w;i--){
best[i] = max(best[i],best[i-w]+v);
}
}
void MultiplePack(int w,int v,int m)
{
if(w*m>=C){//当做完全背包处理
CompletePack(w,v);
return;
}
int k = 1;//分解为01背包(二进制拆分)
while(k<m){
ZeroOnePack(w*k,v*k);
m-=k;
k*=2;
}
ZeroOnePack(w*m,v*m);//最后剩余
return;
}
int main()
{
while(scanf("%d%d",&n,&C)!=EOF&&(n||C)){
memset(best,0,sizeof(best));
int w,v,m;
for(int i = 0;i<n;i++){
scanf("%d%d%d",&w,&v,&m);
MultiplePack(w,v,m);
}
printf("%d\n",best[C]);
}
return 0;
}
四. 为什么不能用完全背包方式优化
五. 例题分析
(1) HDU - 2844 Coins
给你n个钱币的数量和价值, 给你一个m ,让求这些钱币能组合成多少种不同的<=m的价值。注意:这里要求支付价钱时,组合是正好的不用找零!
分析:乍一眼以为要从规划数量入手,那样就用不上条件了。其实留意一下题意里的注意,我们还是dp每种价格下最大的组合价值,如果这个是一种组合方式的话 dp[i] == i 了(因为dp[i]为<=i的最大价值),所以我们dp以后在查找一下有多少dp[i] == i即可!注意这里钱币的重量 = 钱币的价值。
#include <iostream>
#include<cstdio>
#include<cstring>
#include<string>
#include<algorithm>
using namespace std;
const int maxn = 100 + 10;
int w[maxn],v[maxn];
int dp[maxn*1000];
int n,m;
void CompletePack(int W,int V)
{
for(int i = W;i<=m;i++){
dp[i] = max(dp[i],dp[i - W] + V);
}
}
void ZeroOnePack(int W,int V)
{
for(int i = m;i>=W;i--){
dp[i] = max(dp[i],dp[i - W] + V);
}
}
void MultiplePack(int wi,int vi)
{
if(wi*vi>=m){
CompletePack(vi,vi);
return;
}
int k = 1;
while(k<wi){
ZeroOnePack(k*vi,k*vi);
wi-=k;
k*=2;
}
ZeroOnePack(wi*vi,vi*wi);
return;
}
int main()
{
while(scanf("%d%d",&n,&m)!=EOF&&(n||m)){
memset(dp,0,sizeof(dp));
for(int i = 0;i<n;i++){
scanf("%d",&v[i]);
}
for(int j = 0;j<n;j++){
scanf("%d",&w[j]);
}
for(int i = 0;i<n;i++){
MultiplePack(w[i],v[i]);
}
int cnt = 0;
//cout<<m<<endl;
for(int i = 1;i<=m;i++){
//cout<<dp[i]<<endl;
if(dp[i]==i)cnt++;
}
printf("%d\n",cnt);
}
return 0;
}