动态规划-DP-——背包问题

背包问题(简单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].pricea[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].priceval的,那么在最开始的时候就相当于模拟取了一件了,那么在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].pricea[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)需要用到优先队列,在本文不再赘述,供大家思考。



参考


《背包九讲》

题目来源


北航OJ



  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值