背包问题(简单DP)
摘要
该讲主要介绍三类背包问题,都是比较经典的DP问题,比之前所讲的股票问题难度有所提升。
背包Ⅰ(01背包)
题面
有N件物品和一个容量为V的背包。第i件物品的费用是c[i],价值是w[i]。求解将哪些物品装入背包可使价值总和最大。
时间限制:5000ms,内存限制:65536kb
输入
多组输入数据
每组数据第一行两个数n,v,表示物品的数量和背包的容量。(1≤n≤500,1≤v≤30000)
接下来n行,每行两个整数,表示物品的费用和价值(1≤ci,wi≤500)
输出
每组数据一行一个数。
输入样例
3 6
2 1
3 2
2 3
输出样例
5
AC代码
#include<iostream>
#include<cstdio>
#include<cstdlib>
using namespace std;
struct node{
int value;
int price;
};
struct node a[505];
int b[30005]={};
int max(int n,int m){
if(n>=m) return n;
return m;
}
int main(){
int n,val,vmax=0,i,j,v;
while(~scanf("%d%d",&n,&val)){
vmax=0;
for(i=1;i<=n;i++){
scanf("%d%d",&a[i].price,&a[i].value);
}
for(i=0;i<=val;i++){
b[i]=0;
}
for(i=1;i<=n;i++){
for(v=val;v>=a[i].price;v--){
b[v]=max(b[v],b[v-a[i].price]+a[i].value);
}
}
printf("%d\n",b[val]);
}
}
分析
我们还是直接从DP的角度开始分析这个问题,这是一个01背包问题,我们先假设一些变量,b[i,j]表示当前背包被占用的容量是j的情况下,前i个物品的最佳组合的总价值。a[i].price和a[i].value即表示当前商品所需要的容量和当前商品的价值。然后对于当前这个商品有以下两种可能:
- 包剩余的容量不够装当前商品,总价值保持不变,不装入该商品,即b[i,j]=b[i-1,j];
- 有足够容量装该商品,但是装了之后不一定是最佳的价值(因为占用了容量无法保证后面商品是否更好),需要一个选择,即b[i,j]=max(b[i-1,j],b[i-1,j-a[i].price]+a[i].value);
第二个式子怎么理解呢,如果当前产品装进去了,那么装入之前的状态就是b[i-1,j-a[i].price],这样说应该比较好理解了。然后这样我们可以得到一个转移方程如下,当然可以通过初始赋值将其转化为一个方程,代码实现我也放在下方。
- j>=a[i].price: b[i,j]=max(b[i-1,j],b[i-1,j-a[i].price]+a[i].value)
- j<a[i].price: b[i,j]=b[i-1,j]
for(int i=1;i<=n;i++){
for(int j=1;j<=v;j++){
if(j>=a[i].price){
b[i][j]=max(b[i-1][j],b[i-1][j-a[i].price]+a[i].value);
}
else{
b[i][j]=b[i-1][j];
}
}
}
上文这样确实是能够解决问题,但其实是可以再进行优化的,从二维数组优化到一维数组来解决。因为我们可以知道每一次往二维数组b[i,j]中写入数据的时候都是从上一次得到的数据来写入的,其实i就变得没有必要了,因为总是从b[i-1,…]中获取数据的,因此只需要一维数组即可,但是对于j来说,则需要一点思考,到底是由顶向下还是由底向上循环。假设考虑由底向上循环,我们考虑模拟取第i件物品的情况,v应该是从a[i].price到val的,那么在最开始的时候就相当于模拟取了一件了,那么在v取到2*a[i].price的时候,就相当于模拟取了两个第i件了,这明显是与题意不符的(这是后面会说到的完全背包),所以我们采用由顶向下的循环就不会出现这种问题了。大家如果还不明白可以自己画画图推一推就好了。
for(i=1;i<=n;i++){
for(v=val;v>=1;v--){
if(v>=a[i].price) b[v]=max(b[v],b[v-a[i].price]+a[i].value);
}
}
转移方程
- b[j]=max(b[j],b[j-a[i].price]+a[i].value)
HINT
注意初始化。
背包Ⅱ(完全背包)
题面
有N种物品和一个容量为V的背包,每种物品都有无限件可用。第i种物品的费用是c[i],价值是w[i]。求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。
时间限制:1000ms,内存限制:65536kb
输入
多组输入数据
每组数据第一行两个数n,v,表示物品的数量和背包的容量。(1≤n≤500,1≤v≤30000)
接下来n行,每行两个整数,表示物品的费用和价值(1≤ci,wi≤500)
输出
每组数据一行一个数。
输入样例
3 6
2 1
3 2
2 3
输出样例
9
AC代码
#include<iostream>
#include<cstdio>
#include<cstdlib>
using namespace std;
struct node{
int value;
int price;
};
struct node a[505];
int b[30005]={};
int max(int n,int m){
if(n>=m) return n;
return m;
}
int main(){
int n,val,vmax=0,i,j,v;
while(~scanf("%d%d",&n,&val)){
vmax=0;
for(i=1;i<=n;i++){
scanf("%d%d",&a[i].price,&a[i].value);
}
for(i=0;i<=val;i++){
b[i]=0;
}
for(i=1;i<=n;i++){
for(v=a[i].price;v<=val;v++){
b[v]=max(b[v],b[v-a[i].price]+a[i].value);
}
}
printf("%d\n",b[val]);
}
}
分析
刚拿到这个题目相信大家很容易从贪心的思想去解决,其实是错误的,因为这个容量是有限的,不可分割的,即使我们算出平均最大价值,但是装入背包时其容量还是不可分割的,所以贪心肯定是没法实现的,其他具体不能用贪心的原因,在此也不再赘述。
然后我们还是直接从DP的角度开始分析这个问题,这是一个完全背包问题,我们还是先假设一些变量,b[i,j]表示当前背包被占用的容量是j的情况下,前i个物品的最佳组合的总价值。a[i].price和a[i].value即表示当前商品所需要的容量和当前商品的价值。然后对于当前这个商品有k种选择,因为可以选择k=0,1,2…个当前的物品种数,只要不超过背包总容量即可,所以我们直接给出转移方程:
- b[i,j] = max(b[i-1,j-k*a[i].price]+k*a[i].value) 0<=k*a[i].price<=j
这里就不贴代码了,因为很明显时间复杂度太大,需要三个循环才能实现,并且还不是那么容易。所以我们直接开始说优化的过程。还是同样先优化为一维数组。因为每次取物品的时候都是可以无限量的取的,所以我们的k就可以省略了,直接采用两重循环实现,并且要保证每次的量的无限,我们的j的循环则需要从小到大进行循环,代码如下文所示。为什么呢,在这里举个例子说明以下吧:假设我们取第i件物品需要取j件才能满足最大价值的时候,我们的第二重循环运转的时候,v是从a[i].price一直循环到val也就是我们可以在取了一次后,b[v]更新,这样在v取到2*a[i].price的时候就能模拟第二次取,并且此时的b[v-a[i].price]是之前第一次取后更新了过后的值,这样一直循环下去便实现了模拟取无穷次的过程。
for(i=1;i<=n;i++){
for(v=1;v<=val;v++){
if(v>=a[i].price) b[v]=max(b[v],b[v-a[i].price]+a[i].value);
}
}
转移方程
- b[j]=max(b[j],b[j-a[i].price]+a[i].value)
HINT
注意初始化。
背包Ⅲ(多重背包)
题面
有N种物品和一个容量为V的背包。第i种物品最多有m[i]件可用,每件费用是c[i],价值是w[i]。求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。
时间限制:1000ms,内存限制:65536kb
输入
多组输入数据
每组数据第一行两个数n,v,表示物品的数量和背包的容量。(1≤n≤500,1≤v≤30000)
接下来n行,每行三个整数,表示物品的费用,价值,数量(1≤ci,wi≤500,1≤mi≤200)
输出
每组数据一行一个数。
输入样例
2 10
2 1 3
3 2 2
输出样例
6
AC代码
#include<iostream>
#include<cstdio>
#include<cstdlib>
using namespace std;
struct node{
int value;
int price;
int num;
};
struct node a[505];
int b[30005]={},val;
int max(int n,int m){
if(n>=m) return n;
return m;
}
void ZeroOnePack(int *b,int price,int value){
int v;
for(v=val;v>=price;v--){
b[v]=max(b[v],b[v-price]+value);
}
}
void CompletePack(int *b,int price,int value){
int v;
for(v=price;v<=val;v++){
b[v]=max(b[v],b[v-price]+value);
}
}
void MultiplePack(int *b,int price,int value,int num){
if (price*num>=val){
CompletePack(b,price,value);
return;
}
int k=1;
while(k<num){
ZeroOnePack(b,k*price,k*value);
num=num-k;
k=2*k;
}
ZeroOnePack(b,price*num,value*num);
}
int main(){
int n,i,j,v;
while(~scanf("%d%d",&n,&val)){
for(i=1;i<=n;i++){
scanf("%d%d%d",&a[i].price,&a[i].value,&a[i].num);
}
for(i=0;i<=val;i++){
b[i]=0;
}
for(i=1;i<=n;i++){
MultiplePack(b,a[i].price,a[i].value,a[i].num);
}
printf("%d\n",b[val]);
}
}
分析
我们还是从最简单的方法开始分析,因为多重背包,我们直接将某个物品拆成m[i]个同样的物品,只不过每件物品只能取一次,这样就变成了我们上文说到的01背包了,方法非常的简单。转移方程给在下方:
- b[i,j]=max(b[i-1,j-k*a[i].price]+k*a[i].value) 0<=k<=m[i]
这样的时间复杂度为O(V*Σn),我们可以对其进行优化,如何优化呢,我们直接引入二进制来优化,将第i件物品分成若干件物品,,每个物品的数目,分别为1,2,4,8,…,2k-1,n-2k+1,k是满足n-2k+1>0的最大整数。如果不够明白,举个例子,假设某物品有49件,那么可以分为49=1+2+4+8+16+18这5件物品,并且1~49间的任何数都可以由这五件物品构成,所以这样应该比较容易理解了。所以时间复杂度也降为O(V*Σlogn)了。
那应该怎么实现呢,首先对于第i件物品来说,如果a[i].price*a[i].num>=val即如果没法全部装下的话,就不用将其全部拆分了,直接当作完全背包处理就好,模拟出装几件该物品更好即可(如果不太理解可以去看上文的完全背包的分析)。如果是小于的话,那我们就采用拆分的办法即可,代码给出在下方。
void MultiplePack(int *b,int price,int value,int num){
if (price*num>=val){
CompletePack(b,price,value);
return;
}
int k=1;
while(k<num){
ZeroOnePack(b,k*price,k*value);
num=num-k;
k=2*k;
}
ZeroOnePack(b,price*num,value*num);
}
HINT
其实还可以更深度的优化到O(V*n)需要用到优先队列,在本文不再赘述,供大家思考。
参考
《背包九讲》