真的吗?|

LgxTpre

园龄:2年6个月粉丝:24关注:6

题解:樱花

题目链接

前言

这是一道混合背包的问题,其他题解讲述的十分清晰,但在多重背包的问题上大多只是使用了二进制拆分优化,本篇文章将以通俗易懂的方法讲解单调队列优化多重背包,因此默认大家已经掌握了背包的知识,推荐大家拿着笔推一推式子。

前置知识

  • 多重背包

  • 优先队列

什么样的 DP 可以使用单调队列优化

状态转移方程形如:

Fi=max/minj=lii1{gj}+wilili+1

翻译成人话意思就是对于序列中的点,决策区间随着序列下标的增大从左向右进行转移,类似于一个窗口从左向右进行滑动。如果存在 i1<i2 ,且 gi1 劣于 gi2 。那么当窗口滑动到 i2 时,i1 永远不会成为最优决策点,此时这个点也将不用进行转移了。

一个人如果比你小还比你强,那么你就可以退役了。

对此我们可以维护一个下标递增的单调队列,使队头一直保持最优转移。每次从队尾判断,若已经不可能成为最优决策就弹出。特别的,当队头超出了所在的新区间,因为下标单调递增,以后它也不可能回到这个区间,这时我们还需要将队头弹出。这样每次转移之后,我们只需要取出队头,队头就是最优解。

保持单调性的方法非常简单,在新的队尾入队前,要先将劣于它的元素从尾部弹出;“窗口”滑动之后,要检查队头是否合法,如果不合法要从队头弹出。因此一般使用双端队列或者直接数组模拟。

单调队列优化多重背包

记总共有 N 种物品,每种物品中一个物品的体积为 Vi ,价值为 Wi ,数量为 Ci , Fi,j 为前 i 件物品选出总重量为 j , DP 值表示最大价值。

容易得出多重背包的状态转移方程为:

Fi,j=max1kCi{Fi1,jkVi+kWi}

这个东西看起来可能不是很清晰,我们把它分成一项一项来看:

Fi,j=max{Fi1,j,Fi1,jv+w,Fi1,j2v+2w,,Fi1,jkv+kw}

Fi,jv=max{Fi1,jv,Fi1,j2v+w,,Fi1,jkv+(k1)w,Fi1,j(k+1)v+kw}

可以发现对于任何一个 Fi,j 是由它前面的 (k+1) 项形如 Fi1,jkVi+kWi 的式子转移而来,直到背包的体积不能再用。

但是,这个队列中前面的数,每次都会增加一个 w ,所以我们需要做一些转换。

不妨设 j=kVi+d ,其中 k 表示的是 j 重量所能装下的该物品最大数量, d 则是余数。可以得到:

Fi,j=max1kCi{Fi1,kVi+dkVi+kWi}

Fi,j=max1kCi{Fi1,(kk)Vi+d+kWi}

Fi,j=max1kCi{Fi1,(kk)Vi+d+kWikWi+kWi}

Fi,j=max1kCi{Fi1,(kk)Vi+d+(kk)Wi}+kWi

对于每一种余数 d 我们可以维护一个单调队列,因为通过式子可以看出不同余数之间的状态不会互相转移,这样我们就可以在线性的时间里求出在第 i 个物品时,对于 jd(modVi)Fi,j 最大值。

时间复杂度 O(NV) ,空间复杂度 O(NV)

二维数组代码:

int n,V,v[MAX],w[MAX],c[MAX];  
	int f[MAX][MAX],q[MAX];   //数组模拟单调队列 
	cin>>n>>V;
	for(int i=1;i<=n;i++)
		cin>>v[i]>>w[i]>>c[i];
	for(int i=1;i<=n;i++)
		for(int j=0;j<v[i];j++)  //枚举余数
		{
			int head=0,tail=-1;
			for(int k=j;k<=V;k+=v[i])
			{
				//(j-q[tail])/v[i] 以及下面的 (j-q[head])/v[i] 对应的都是每一个k' 
				//超出可取范围,弹出队头  
				while(head<=tail&&(k-q[head])/v[i]>c[i]) head++;   
				//当前队尾解劣于新解,全部弹出
				while(head<=tail&&f[i-1][q[tail]]+(k-q[tail])/v[i]*w[i]<=f[i-1][k]) tail--;   
				q[++tail]=k;
				//每次更新直接取出队头最优解 
				f[i][k]=f[i-1][q[head]]+(k-q[head])/v[i]*w[i]; 
			}
		} 
	cout<<f[n][V];

空间上的进一步优化

虽然时间优秀了,但是空间还不够优秀。

因为第 i 层的状态只会用到第 i1 层,我们可以把这个式子用优化完全背包的思路压成一维,倒着向回推一边。考虑倒序枚举背包体积,对于每种余数 d[0,Vi1] ,倒序循环 k=(Vd)/Vi0 ,这样就可以得到一个新的状态转移方程。

Fd+kVi=maxkCikk1{Fd+kVi+(kk)Wi}

这样我们就可以得到一个空间复杂度为 O(V) 的更优秀算法。

一维数组代码:

int n,V,v[MAX],w[MAX],c[MAX];  
	int f[MAX],q[MAX],g[MAX];   //数组模拟单调队列 
	cin>>n>>V;
	for(int i=1;i<=n;i++)
		cin>>v[i]>>w[i]>>c[i];
	for(int i=1;i<=n;i++)
	{
		memcpy(g,f,sizeof g);
		for(int j=0;j<v[i];j++)  //枚举余数
		{
			int head=0,tail=-1;
			for(int k=j;k<=V;k+=v[i])
			{
				while(head<=tail&&(k-q[head])/v[i]>c[i]) head++;
				while(head<=tail&&g[q[tail]]+(k-q[tail])/v[i]*w[i]<=g[k]) tail--;
				q[++tail]=k;
				f[k]=g[q[head]]+(k-q[head])/v[i]*w[i];
			}
		} 
	}
	cout<<f[V];

完整代码

#include<bits/stdc++.h>
#define MAX 10010
using namespace std;

inline int read()
{
	int s=0,w=1;
	char c=getchar();
	while(!isdigit(c)) (c=='-')?w=-1:w=1,c=getchar();
	while(isdigit(c)) s=(s<<1)+(s<<3)+(c^48),c=getchar();
	return s*w;
}

int t1,t2,t3,t4,V;
int n,c[MAX],w[MAX],v[MAX];
int f[MAX],g[MAX];

inline void Zerone_Bag(int i)
{
	for(int k=V;k>=w[i];k--)
		f[k]=max(f[k],f[k-w[i]]+v[i]);
	return;
}
inline void Full_Bag(int i)
{
	for(int k=w[i];k<=V;k++)
		f[k]=max(f[k],f[k-w[i]]+v[i]);
	return;
}
inline void Multiple_Bag(int i)
{
	int q[MAX];
	memcpy(g,f,sizeof g);
	for(int j=0;j<w[i];j++)
	{
		int head=0,tail=-1;
		for(int k=j;k<=V;k+=w[i])
		{
			while(head<=tail&&(k-q[head])/w[i]>c[i]) head++;
			while(head<=tail&&g[q[tail]]+(k-q[tail])/w[i]*v[i]<=g[k]) tail--;
			q[++tail]=k;
			f[k]=g[q[head]]+(k-q[head])/w[i]*v[i];
		}
	}
	return;
}

int main()
{
	t1=read(),t2=read(),t3=read(),t4=read();
	V=(t3*60+t4)-(t1*60+t2);
	n=read();
	for(int i=1;i<=n;i++)
		w[i]=read(),v[i]=read(),c[i]=read();
	for(int i=1;i<=n;i++)
	{
		if(c[i]==0) Full_Bag(i);
		if(c[i]==1) Zerone_Bag(i);
		if(c[i]>=2) Multiple_Bag(i);
	}
	cout<<f[V];
	return (0-0);
}

参考资料

多重背包优化

李煜东 《算法竞赛进阶指南》

本文作者:LgxTpre

本文链接:https://www.cnblogs.com/LittleTwoawa/p/16621098.html

版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。

posted @   LgxTpre  阅读(38)  评论(0编辑  收藏  举报
点击右上角即可分享
微信分享提示
评论
收藏
关注
推荐
深色
回顶
收起