【C++心路历程36】单调队列优化多重背包

我们知道,01背包的时间复杂度为O(N*C),而完全背包因其一维数组填表的特殊性也可一做到O(N*C),那么多重背包的O(N*C*n[i])(n[i]为每个物品的数量)我们看起来就有些不爽了。其实可以利用单调队列做到O(N*C)的复杂度!

int N,v[maxn],p[maxn],C,n[maxn];
数组意义:
N个物品,每个物体有n[i]个,体积为v[i],价值为p[i],现放入容积为C的背包,求最大价值。

朴素的dp方程:

f(i,j)=max{f(i-1,j-x*v[i])+p[i]*x}   0<=x<=min(j/v[i],n[i])

我们发现,拖慢时间的问题主要出在枚举x上,那么该怎样处理x呢?
我们在填表时,f(i,j)格子主要用到了i-1行的某些格子,观察dp方程,又是在这些格子中,取一个最大值。这时j-x*v[i]便会成为我们优化的一个累赘,不妨令k=j-x*v[i];
则方程变形为:

f(i,j)=max {f(i-1,k)+(j-k)*p[i]/v[i]}   max(0,j-n[i]*v[i])<=k<=j

再次变形:

f(i,j)=max{f(i-1,k)-k/v[i]*p[i]}+j/v[i]*p[i] max(0,j-n[i]*v[i])<=k<=j

这样转化我们发现,max框框里(也就是单调队列的维护对象)已经符合要求了,但是有人会问,k/v[i]*p[i] 与j/v[i]*p[i]在除法中会有误差么?实际上是不存在的,因为k=j-x*v[i];即j-k一定是v[i]的倍数,于是乎便不会出错了(可以想想余数的关系)

接下来还有一个问题,如何实现这个方程?注意到填f(i,j)格子时,只会用到前面与j mod v[i]同余的格子,因此在实现时,不妨枚举v[i]的余数x,即0~v[i]-1,每枚举一个x,便可将其后与x的同余的格子全部填完。而枚举每个x时,便可以用单调队列优化。
时间复杂度分析:
在填第i行时,看似是三重循环,实际上第i行每个格子都只计算了1次,故而时间复杂度为O(N*C).
附上代码:

struct data{
    LL v;int k;
}q[50005];
void dp()
{
    int front,rear;
    for(int i=1;i<=N;i++)
    for(int x=0;x<v[i];x++)
    {
        front=rear=1;
        for(int j=x;j<=C;j+=v[i])
        { 
            while(rear!=front && j-n[i]*v[i]>q[front].k) front++;//退休 
            while(rear!=front && q[rear-1].v<d[(i-1)&1][j]-1ll*j/v[i]*p[i]) rear--;// 淘汰 
            q[rear].v=(d[(i-1)&1][j]-1ll*j/v[i]*p[i]);//新元素入队 
            q[rear++].k=j;
            d[i&1][j]=(q[front].v+1ll*j/v[i]*p[i]);
        }
    }
    LL ans=d[N&1][C];
    cout<<ans;
}
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值