6.多重背包问题3 (单调队列优化)

原题链接

数据范围是0~1000,0~20000,如果使用单调队列优化的话,时间复杂度是O(nv)

单调队列优化

由来,启发

在枚举多重背包的选法的时候,会出现如图的情况,红色框内的被重复枚举,只是偏移量w不同

而dp优化的思想就是减小重复操作

如果从一个点出发,最后到不能再减v的时候,剩下的数是小于v的(模v的余数)

也就是说一个j是从他模v的余数出发的,如图r是模v的余数

 

 如图每次到一个背包容积时枚举此时所有的选法,就是在框中选一个最大值,其实可以发现是一个单调队列的过程,每次滑动窗口选择一个窗口内的最值

注意:运用单调队列优化时,要注意偏移量w

因为我们最多只能选s个,单调窗口大小为s,由于我们需要求的是在一个窗口内选择装入背包的最大值,所以可以使用单调队列优化

显而易见,m 一定等于 k*v + j,其中  0 <= j < v
所以,我们可以把 dp 数组分成 j 个类,每一类中的值,都是在同类之间转换得到的
也就是说,dp[k*v+j] 只依赖于 { dp[j], dp[v+j], dp[2*v+j], dp[3*v+j], ... , dp[k*v+j] }

因为我们需要的是{ dp[j], dp[v+j], dp[2*v+j], dp[3*v+j], ... , dp[k*v+j] } 中的最大值,
可以通过维护一个单调队列来得到结果。这样的话,问题就变成了 j 个单调队列的问题

从正拓扑序的角度考虑

r表示所有小于v的数  0<=r<v;
dp[r]=dp[r]
dp[r+v]=max(dp[r+v],dp[r]+w)
dp[r+2v]=max(dp[r+2v],dp[r]+2w,dp[r+v]+w)
...
dp[r+(s+1)v]=max(dp[r+(s+1)v],...dp[r+v]+sw)
...

注意到每次向右滑动窗口,窗口内所有元素的偏移量都会加上一个w

可以改写成

dp[r]    =     dp[r]
dp[r+v]  = max(dp[r], dp[r+v] - w) + w
dp[r+2v] = max(dp[r], dp[r+v] - w, dp[r+2v] - 2w) + 2w
dp[r+3v] = max(dp[r], dp[r+v] - w, dp[r+2v] - 2w, dp[r+3v] - 3w) + 3w
...
每次入队的值            dp[r+k*v]-k*w (与当前j无关的偏移量)
每次计算窗口内的值      dp[j]=dp[q[hh]]+(j-q[hh])/v*w (当前j有关的偏移量)
j表示当前窗口的最后一个元素
每次计算时 例如 dp[r+3v] 
在j==r+3v时,dp[r+3v]
在j==r+4v时,dp[r+4v]+w
在j==r+5v时,dp[r+5v]+2w ...

操作 

  • 放入队列的时候比较当前的尾端+当前偏移量    g[q[tt]]-(q[tt]-r)/vi*wi<=g[j]-(j-r)/vi*wi
  • 在队列中存放的是dp数组值                               dp[hh~tt]
  • 出队计算的时候选择的是前面最大的值+当前偏移量       dp[j]=g[q[hh]]+(j-q[hh])/vi*wi;

单调队列问题,最重要的两点

  • 维护队列元素的个数,如果不能继续入队,弹出队头元素
  • 维护队列的单调性,即:尾值 >= dp[j + k*v] - k*w,这里维护最大值,就是单调递减

本题中,队列中元素的个数应该为 s+1 个,即 0 -- s 个物品 i

#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
const int N=2e4+10;
int dp[N],g[N],q[N];
int w[N],v[N],s[N];

int main()
{
    int n,V;
    cin>>n>>V;
    for(int i=1;i<=n;i++)
        cin>>v[i]>>w[i]>>s[i];
    for(int i=1;i<=n;i++)   //选择i个物品
    {   
        memcpy(g,dp,sizeof dp); //在枚举过程中要求保持前面的值
        //dp数组可能被改变,所以拷贝一下,开二维数组可以不用这个步骤
        int vi=v[i],si=s[i],wi=w[i];
        for(int r=0;r<v[i];r++) //枚举余数
        {
            int hh=0,tt=-1; //不同的余数是不同的单调队列,需要独立枚举
            for(int j=r;j<=V;j+=vi)//枚举所有选法(包括不选),j从余数开始,每次滑动v
            {
                //因为实际窗口长度是s+1,所以这里j-s*v
                if(hh<=tt&&q[hh]<j-si*vi) hh++;
                while(hh<=tt&&g[q[tt]]-(q[tt]-r)/vi*wi<=g[j]-(j-r)/vi*wi)
            //这里的偏移量是从r开始0~x(x==(V-r)/v*w,0,1,2,3,...)
                    tt--;
                q[++tt]=j;
            //因为背包不一定足够大装满s个,所以没有限制
                dp[j]=g[q[hh]]+(j-q[hh])/vi*wi;//不用-r,因为j-q[hh]是v的余数
            //计算的偏移量与j有关,所以是 j-...
            }
        }   
    }
    cout<<dp[V];
}

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值