背包九讲 之 多重背包及其优化

多重背包问题

在了解了01背包以及完全背包后。我们继续来了解一下多重背包。

问题重述

有 N 种物品和一个容量是 V 的背包。第 i 种物品最多有 si 件,每件体积是 vi,价值是 wi。求解将哪些物品装入背包,可使物品体积总和不超过背包容量,且价值总和最大。输出最大价值。

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

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

分析: 相较于01背包和完全背包,多重背包介于二者之间,每种物品有明确的数量,我们不能像01背包每次对物品只选择拿或不拿,也不能像完全背包对每一个物品在体积足够得情况下,任取去试。下面我们根据下面三种数据范围了解三种解法。
数据范围1: 0<N,V≤100; 0<vi,wi,si≤100;
数据范围2: 0< N ≤1000; 0<V≤2000; 0<vi,wi,si≤2000;
数据范围3: 0< N ≤1000; 0<V≤20000; 0<vi,wi,si≤20000;

方法1(暴力解法):

数据范围1: 0<N,V≤100; 0<vi,wi,si≤100;

思路分析: 每个物品都有了确切的数量,我们对比01背包直接思考就是每件物品我们除了需要知道选还是不选外,在选的情况下还需要知道选几件。这样一来它的问题就和完全背包一致了,但不同的是选择有了自己的数量上限,我们不能像完全背包一样在体积的限制下任取,还有了数量限制。所以我们只能在01背包的基础上继续判断我们选几件最佳,具体操作就是在每个物品,循环体积时,再加一层循环继续判断。

代码如下:

#include<iostream>
#include<vector>
using namespace std;
int main()
{
	int N,V;	
	vector<int>ans;
	cin>>N>>V;		    //输入 物品种类数量和背包体积 
	ans.resize(V+10,0); //开辟合适大小的一维数组空间滚动储存当前答案 
	for(int i=0;i<N;i++) //开始遍历物品数 
	{
		int volume,value,counts; //现场输入物品属性不同于01背包除了体积、价值还多了数量 
		cin>>volume>>value>>counts;
		for(int j=V;j>=volume;j--) //和01背包一样体积从大到小遍历 
		{
			for(int k=1;k*volume<=j&&k<=counts;k++)//ans[j]本身是不选当前物品我们用它和在选的情况下个选几个分别比较找最佳 
			{//物品在选的情况下可以选的数量是从一到不能超过背包体积和自己的数量 
				ans[j]=max(ans[j],ans[j-k*volume]+k*value);//状态转移注意选几个空多少体积、加多少价值 
			}
		}
	}
	cout<<ans[V]<<endl;//输出答案 
	return 0;
} 

方法2(二进制方法优化):

数据范围2: 0< N ≤1000; 0<V≤2000; 0<vi,wi,si≤2000;

思路分析: 承接方法1,我们思考在上一中方法中对于最里那一层循环我们比较的,其实是在数量合法的情况下,这种物品0个、1个、2个、3个……选还是不选。我们等同于将物品的数量摊了开来,将其转化成了01背包,用counts个1中的不同数量组合来表示了每一种可能。那么这所有的可能除了用不同数量的1来表示出来能不能用更少的数字表示出来?这样我们的循环就会少一些。

实际上从1到counts个所有的数可以用二进制方式的组合表示出来比如1到5中所有的数可以由:1、2、2三个数字组合出来(1、2、1+2、2+2、1+2+2)。具体原因就是一个数的二进制的表达如果所有位全是1那么不同位上1的组合就能表示从1到它的所有数比如7的二进制是111那么001、010、100(对应十进制的1、2、4)的组合就能表示1到7所有的数。这样就比较容易理解了。那么从它到下一个二进制全是1的数之间的数就是它的这些数再加一个少的数。例如7下一个二进制全是1的数是15(1111)。那么11是介于7和15之间的数,从1到它的所有数就可以用1、2、4、4(11-7);表示出来因为1、2、4可以表示1到7那么再加上4就可以表示到11了。如此一来我们就可以将每counts照这个原则用最少的数将从1到counts的所有数组合出来不需要只用1组合。

代码如下:

#include<iostream>
#include<vector>
using namespace std;
struct Node{    //定义结构体用它重新构建01背包 
	int volume;
	int value;
};
int main()
{
	int N,V;  
	vector<int>ans;
	vector<Node>Goods;	//定义结构体数组 
	cin>>N>>V;  		//输入物品数和背包大小 
	ans.resize(V+10,0);  //开辟合适大小的一维数组空间滚动储存当前答案 
	for(int i=0;i<N;i++)
	{
		int volume,value,counts; 
		cin>>volume>>value>>counts;//现场输入物品的体积、价值、数量 
		for(int j=1;j<=counts;j*=2)//开始拆解数量重新构建商品赋予价值和体积 
		{
			counts-=j;
			Goods.push_back({j*volume,j*value});
		}
		if(counts>0) //如果有剩余单独构建一个商品 
		{
			Goods.push_back({counts*volume,counts*value});
		}	
	}
	for(int i=0;i<Goods.size();i++)//接下来就是熟悉的01背包了遍历每件商品 
	{
		for(int j=V;j>=Goods[i].volume;j--)//体积从大到小 
		{
			ans[j]=max(ans[j],ans[j-Goods[i].volume]+Goods[i].value);//状态转移 
		}
	}
	cout<<ans[V]<<endl;//输出答案 
	return 0;
}

方法3(单调队列方法优化):

数据范围3: 0< N ≤1000; 0<V≤20000; 0<vi,wi,si≤20000;

我们首先需要了解一下单调队列的原理然后我们继续承接方法1结合01背包,我们发现当一个物品的属性输入后体积、价值和数量,我们实际要遍历的体积其实变成了 0min(counts*volume,V) 而这些体积在遍历时我们对每一个体积所能装下当前物品的所有可能都进行了判断,实际我们可以发现我们这些体积可以根据,对 volume 不同的余数可以分成 volume组(余数从0到volume-1) 对于其中每一组他们之间的体积是有关联的。

例如 2、2 * volume+2、3 * volume+2、4 * volume+2…… 这一组这些体积的最优解,他们其实都是通过和前面的比较转化过过来的方法一我们就是进行了所有的比较, 其实我们可以利用单调队列的原理让一个队列的队首始终储存之前合法的最优解(这里的合法主要指的是数量不要超过counts),这样当我们遍历到这个体积时就只需要将不选和在选的情况下合法的最优解(即队首元素)作比较即可 这样我们可以少一层循环只遍历一遍体积即可。

代码如下:

#include<iostream>
#include<cstring>
#define Max 20010
using namespace std;
int ans[Max];//和01背包一样定义ans数组滚动存储答案
int temp[Max];
/*与01背包不同因为我们要跳着更新答案所以不能直接更新ans,为了避免数据丢失
所以我们再定义一个temp数组每次复制一下ans的内容,再更新ans数组*/

int deq[Max]; //定义双端队列队首永远储存合法范围内的最大价值的体积 
int N,V;
int main()
{
	cin>>N>>V;   //输入物品数量和背包体积 
	for( int i = 0; i < N; i++ )//开始遍历物品 
	{
		memcpy(temp,ans,sizeof(ans));//开始前先复制一遍ans中的当前答案 
		int volume, value,counts;
		cin>> volume >> value >> counts;//现场输入物品的体积、价值、数量 
		for(int j=0; j<volume; j++) //遍历当前体积的不同余数 
		{
			int front=0,back=-1;   //初始化队列置空 
			for(int k = j; k <= V; k+=volume) //在体积不超限的情况下遍历余数相同的体积使用单调队列进行优化 
			{
				if(front<=back && ( k-deq[front] ) / volume > counts)
				{//判断在队列不为空的情况下如果最佳体积需要的物品数量超过了给定数量既不合法队首出队 
					front++;
				}
				if( front <= back )
				{//如果队列不为空且最佳体积合法则让最佳体积和当前状态比较选最大状态转移 
					ans[k] = max( ans[k],temp[deq[front]] + ( k-deq[front] ) / volume * value );
				}
				while(front <= back && temp[deq[back]] + ( k-deq[back] ) / volume * value  < temp[k] )
				{//在队列不为空且当前体积的价值一直大于队尾元素时一直让队尾出队 
					back--;
				}
				deq[++back]=k;//最后让当前体积入队 上面的判断符合条件再执行这一步必执行 
			}
		}
	}
	cout<<ans[V];//输出答案 
	return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值