我们知道,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;
}