多重背包的二进制优化与单调队列优化

多重背包定义

给定n种物品,其中第 i 种物品的体积为 w i w_i wi,价值为 v i v_i vi,并且有 c i c_i ci
有一容积为m的背包,要求选择若干个物品放入背包,使得物品的价值总和最大。

二进制优化

对多重背包问题一个朴素的思路是将第i种物品视作不同的 c i c_i ci个物品,再做0/1背包,复杂度为 O ( m ∑ i = 1 n c i ) O(m\sum_{i=1}^n c_i) O(mi=1nci)
注意到任意正整数都可以表示为若干个2的幂次方之和
对第 i 种物品,我们并不用将 c i c_i ci一一拆分,而可以将 c i c_i ci按二的幂次方拆分
此时再做0/1背包,复杂度就变为 O ( m ∑ i = 1 n log ⁡ c i ) O(m\sum_{i=1}^n{\log{c_i}}) O(mi=1nlogci)

const int maxn=1000010;
int n,m;
int val[maxn],wt[maxn];
int dp[maxn];
int cnt,ans;

int main()
{
    n=read(); m=read();
    for(int i=1;i<=n;i++)
    {
        int v=read(),w=read(),sum=read();
        for(int j=1;j<=sum;j<<=1)
        {
        	val[++cnt]=j*v; wt[cnt]=j*w;
        	sum-=j;
        }
        if(sum) val[++cnt]=sum*v,wt[cnt]=sum*w;
    }
    
    for(int i=1;i<=cnt;i++)
    for(int j=m;j>=wt[i];j--)
    dp[j]=max(dp[j],dp[j-wt[i]]+val[i]);
        
    int ans=0;
	for(int i=1;i<=m;++i)
	ans=max(ans,dp[i]);

    printf("%d",ans);
    return 0;
}

单调队列优化

对于朴素的多重背包,其递推式为 d p [ j ] = m a x ( d p [ j − k ∗ w i ] + k ∗ v i ) ,   1 ≤ k ≤ c i dp[j]=max(dp[j-k*w_i]+k*v_i),\ 1\leq k \leq c_i dp[j]=max(dp[jkwi]+kvi), 1kci
对于 d p [ j ] dp[j] dp[j],他会从 j − w i , j − 2 w i , . . . , j − c i w i j-w_i, j-2w_i,...,j-c_iw_i jwi,j2wi,...,jciwi转移
而对于 d p [ j − w i ] dp[j-w_i] dp[jwi],他会从 j − 2 w i , j − 3 w i , . . . , j − ( c i + 1 ) w i j-2w_i, j-3w_i,...,j-(c_i+1)w_i j2wi,j3wi,...,j(ci+1)wi转移
可以发现二者的转移仅有两处不同,如下图所示
在这里插入图片描述
这启发我们 j j j按膜 w i w_i wi分组,这样不同组的j转移的位置一定互不相同(也即不会相互转移)
对于膜 w i w_i wi为u的一组,可得递推方程 d p [ u + k w i ] = m a x ( d p [ u + t w i ] + ( k − t ) v i ) ,   k − c i ≤ t ≤ k − 1 dp[u+kw_i]=max(dp[u+tw_i]+(k-t)v_i),\ k-c_i\leq t \leq k-1 dp[u+kwi]=max(dp[u+twi]+(kt)vi), kcitk1
按照0/1背包的思路倒序枚举k,用单调队列维护 m a x ( d p [ u + t w i ] − t v i ) max(dp[u+tw_i]-tv_i) max(dp[u+twi]tvi)即可
循环计算 u ∈ [ 0 , w i ) u\in[0,w_i) u[0,wi)的每一组即可计算得考虑前i种物品得dp[j]

const int maxn=500010;
int n,m;
int v[maxn],w[maxn],c[maxn];
int dp[maxn];
int q[maxn];

int calc(int i,int u,int k){
	return dp[u+k*w[i]]-k*v[i];
}

int main()
{
	n=read();m=read();
    for(int i=1;i<=n;i++)
    {
    	v[i]=read();
    	w[i]=read();
    	c[i]=read();
	}
    
    for(int i=1;i<=n;++i)
    {
    	for(int u=0;u<w[i];++u)
    	{
    		int ll=1,rr=1;
    		
    		int mx=(m-u)/w[i]; // 上述推导u+kw中k得最大值
    		for(int k=mx-1;k>=max(mx-c[i],0);--k) // 将初始的转移位置加入单调队列
    		{
    			while(ll<rr && calc(i,u,q[rr-1])<=calc(i,u,k)) --rr;
    			q[rr++]=k;
			}
    		
    		for(int k=mx;k>=0;--k) // 倒序枚举每个状态
    		{
    			while(ll<rr&&q[ll]>k-1) ++ll;
    			
				int j=u+k*w[i];
				if(ll<rr)dp[j]=max(dp[j],calc(i,u,q[ll])+k*v[i]);
    			
				if(k-c[i]-1>=0) // 加入新的转移位置
				{
					while(ll<rr && calc(i,u,q[rr-1])<=calc(i,u,k-c[i]-1)) --rr;
					q[rr++]=k-c[i]-1;
				}
			}
		}
    }
	
	int ans=0;
	for(int i=1;i<=m;++i)
	ans=max(ans,dp[i]);
	
    printf("%d",ans);
    return 0;
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值