第三讲:多重背包

5 篇文章 0 订阅

有N种物品和一个容量为V的背包。第i种物品最多有n[i]件可用,每件费用是w[i],价值是c[i]。求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。

基本算法:    
             这题目和完全背包问题很类似。基本的方程只需将完全背包问题的方程略微一改即可,因为对于第i种物品有n[i]+1种策略:取0件,取1件……取n[i]件。令f[i][v]表示前i种物品恰放入一个容量为v的背包的最大权值,则:f[i][v]=max{f[i-1][v-k*w[i]]+ k*c[i]|0<=k<=n[i]}。复杂度是O(V*∑n[i])。

转化为01背包问题  
             另一种好想好写的基本方法是转化为01背包求解:把第i种物品换成n[i]件01背包中的物品,则得到了物品数为∑n[i]的01背包问题,直接求解,复杂度仍然是O(V*∑n[i])。

            二进制01:
            我们期望将它转化为01背包问题之后能够像完全背包一样降低复杂度。考虑二进制的思想,我们考虑把第i种物品换成若干件物品,使得原问题中第i种物品可取的每种策略——取0..n[i]件——均能等价于取若干件代换以后的物品。另外,取超过n[i]件的策略必不能出现。   方法是:将第i种物品分成若干件物品,其中每件物品有一个系数,这件物品的费用和价值均是原来的费用和价值乘以这个系数。使这些系数分别为 1,2,4,...,2^(k-1),n[i]-2^k+1,且k是满足n[i]-2^k+1>0的最大整数(注意:这些系数已经可以组合出1~n[i]内的所有数字)。
二进制的正确性:
            假设有 1000 个苹果,现在要取n个苹果,如何取?朴素的做法应该是将苹果一个一个拿出来,直到n个苹果被取出来。
再假设有 1000 个苹果和10只箱子,利用箱子进行某些预工作,然后如何快速的取出n个苹果呢?所以可以在每个箱子中放 2^i (i<=0<=n)个苹果,也就是 1、2、4、8、16、32、64、128、256、489(n减完之前的数之后不足 2^i,取最后剩余的数),相当于把十进制的数用二进制来表示,取任意n个苹果时,只要推出几只箱子就可以了。
再次分析:
只看上面是不好理解的,比如:7的二进制 7 = 111, 它可以分解成 001, 010, 100. 这三个数可以组合成任意小于等于 7 的数,而且每种组合都会得到不同的数。再比如,13 = 1101, 则分解为 0001, 0010, 0100, 0110. 前三个数字可以组合成 7 以内任意一个数,每个数再加上0110(= 6) 之后可以组合成任意一个大于等于 6 小于等于 13 的数,所以依然能组成任意小于等于
 13 的数,很明显 6,7 会多重复 1 次,但对于求解背包问题是没有影响的,基于这种思想把一种多件物品转换为,多件一种物品,然后用01背包求解即可。
下面证明一下为什么有重复没有影响的正确性:
不正确可能产生的原因就是将重复的多加一次,比如 7+7=14 就可能造成错误,但是分析一下其实是不可能出现的,因为假如第一个 7 是由 0001+0010+0100 得到的,那么第二个 7 就需要用到 0110+0001,但 0001 只能出现一次,(每个数最多选一次)所以不会形成这种错误的,其它的可能错误操作也能由这种解释进行否定,从而验证了正确性。
原问题转化为了复杂度为O(V*∑logn[i])的01背包问题,是很大的改进。

单调队列优化的O(N⋅V)算法 (单调队列解释

大佬:https://blog.csdn.net/flyinghearts/article/details/5898183

多重背包的松弛:dp[i][j] = max(dp[i-1][j],dp[i-1][j-k*c[i]]+k*v[i])
我们需要的就是如何在O(1)的时间下求出dp[i][j];

先看一个例子:取m[i] = 2, v[i] = v, w[i] = w, V > 9 * v,
并假设 f(j) = F[i - 1][j],观察公式右边要求最大值的几项:
j = 6*v:   f(6*v)、f(5*v)+w、f(4*v)+2*w 这三个中的最大值
j = 5*v:   f(5*v)、f(4*v)+w、f(3*v)+2*w 这三个中的最大值
j = 4*v:   f(4*v)、f(3*v)+w、f(2*v)+2*w 这三个中的最大值
显然,公式㈠右边求最大值的几项随j值改变而改变,但如果将j = 6*v时,每项减去6*w,j=5*v时,每项减去5*w,j=4*v时,每项减去4*w,就得到:
j = 6*v:   f(6*v)-6*w、f(5*v)-5*w、f(4*v)-4*w 这三个中的最大值
j = 5*v:   f(5*v)-5*w、f(4*v)-4*w、f(3*v)-3*w 这三个中的最大值
j = 4*v:   f(4*v)-4*w、f(3*v)-3*w、f(2*v)-2*w 这三个中的最大值
很明显,要求最大值的那些项,有很多重复。


根据这个思路,可以对原来的公式进行如下调整:

原:f[i][j]=max{f[i−1][j−k⋅v[i]]+k⋅w[i]}                  (0≤k≤min(n[i],j/v[i]))

设a=j/v[i],b=j%v[i],即j=a⋅v[i]+b

f[i][a⋅v+b]=max{f[i−1][(a−k)⋅v+b]+k⋅w}               (0≤k≤min(n,a))

设s=a−k,那么a−min(n,a)≤s≤min(n,a)

f[i][a⋅v+b]=max{f[i−1][s⋅v+b]−s⋅w}+a⋅w              (a−min(n,a)≤s≤min(n,a))

令k[x]=f[i][x⋅v+b],g[x]=f[i−1][x⋅v+b]−x⋅w,h[x]=x⋅w,那么上式即为:

k[a]=max{g[s]}+h[a]       (a−min(n,a)≤s≤min(n,a))

用单调队列维护g[s]即可实现O(1)转移,要注意满足s的范围


f[i][j]就是求j的前面m[i] + 1个数对应的f[i - 1] [b + k * d] - k * w[i]的最大值,加上a * w[i],如果将F[i][j]前面所有的f[i - 1][b + k * d] – k * w放入到一个队列,那么,F[i][j]就是求这个队列最大长度为m[i] + 1时,队列中元素的最大值,加上a * w[i]。因而原问题可以转化为:O(1)时间内求一个队列的最大值。

分割线


样例模板

题目:庆功会

题目描述:

为了庆贺班级在校运动会上取得全校第一名成绩,班主任决定开一场庆功会,为此拨款购买奖品犒劳运动员。期望拨款金额能购买最大价值的奖品,可以补充他们的精力和体力。

【输入】

第一行二个数n(n≤500),m(m≤6000),其中n代表希望购买的奖品的种数,m表示拨款金额。

接下来n行,每行3个数,v、w、s,分别表示第I种奖品的价格、价值(价格与价值是不同的概念)和能购买的最大数量(买0件到s件均可),其中v≤100,w≤1000,s≤10。

【输出】

一行:一个数,表示此次购买能获得的最大的价值(注意!不是价格)。

【输入样例】

5 1000
80 20 4
40 50 9
30 50 7
40 30 6
20 20 1

【输出样例】

1040

分割线

朴素算法:

#include<bits/stdc++.h>
using namespace std;
const int maxn=1e5;
int v[maxn],w[maxn],s[maxn];
int dp[maxn];
int main(){
	int n,m;
	scanf("%d %d",&n,&m);
	for(int i=1;i<=n;i++)
	scanf("%d %d %d",&v[i],&w[i],&s[i]);
	for(int i=1;i<=n;i++)
	for(int j=m;j>=v[i];j--)
	for(int k=0;k<=s[i];k++){
		if(v[i]*k>j) break;
		dp[j] = max(dp[j],dp[j-k*v[i]]+k*w[i]);
	}
	printf("%d",dp[m]);
	return 0;
} 

二进制优化
 

#include<bits/stdc++.h>
using namespace std;
const int maxn=1e5;
int v[maxn],w[maxn];
int dp[maxn];
int n,m,n1;
int main(){
	scanf("%d %d",&n,&m);
	for(int i=1;i<=n;i++){
		int x,y,s,t=1;
		scanf("%d %d %d",&x,&y,&s);
		while(s>=t){
			v[++n1]=x*t;
			w[n1] = y*t;
			s-=t;
			t*=2;
		}
		v[++n1]=x*s;  //把s以2的指数分堆:1,2,4,…,2^(k-1),s-2^k+1,
		w[n1]=y*s; 
	}
	for(int i=1;i<=n1;i++)
	for(int j=m;j>=v[i];j--)
	dp[j]=max(dp[j],dp[j-v[i]]+w[i]);
	printf("%d",dp[m]);
	return 0;
}

单调队列优化
 

#include<bits/stdc++.h>
using namespace std;
const int maxn=10100;
int f[maxn];
int q[maxn][2],head,tail; //q存两个单调队列两个数组 
int main(){
	int n,m;cin>>n>>m;
	for(int i=1;i<=n;i++){
		int w,c,p;
		cin>>w>>c>>p;
		for(int v=0;v<w;v++){ //根据的是每轮的费用,当前轮是根据上轮队列的最大值推出 
			head=1,tail=0;  //清空上轮队列 
			for(int j=0;j<=(m-v)/w;j++){ //范围 
				int tmp=f[j*w+v]-j*c; //tmp 是上轮dp的值,j代表s 
				while(head<=tail && tmp>=q[tail][0]) --tail; 
				q[++tail][0]=tmp,q[tail][1]=j;
				while(q[head][1]<j-min(p,j)) ++head; //以下j代表a,删除队首以去除不满足a-min(n,a)≤s的状态
				f[j*w+v]=q[head][0]+j*c; //队首最大值,加上本次的j*c; 
			}
		}
	}
	cout<<f[m];
	return 0;
}

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值