原题链接
数据范围是0~1000,0~20000,如果使用单调队列优化的话,时间复杂度是O(nv)
单调队列优化
由来,启发:
在枚举多重背包的选法的时候,会出现如图的情况,红色框内的被重复枚举,只是偏移量w不同
而dp优化的思想就是减小重复操作
如果从一个点出发,最后到不能再减v的时候,剩下的数是小于v的(模v的余数)
也就是说一个j是从他模v的余数出发的,如图r是模v的余数
如图每次到一个背包容积时枚举此时所有的选法,就是在框中选一个最大值,其实可以发现是一个单调队列的过程,每次滑动窗口选择一个窗口内的最值
注意:运用单调队列优化时,要注意偏移量w
显而易见,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
每次计算窗口内的值 dp[j]=dp[q[hh]]+(j-q[hh])/v*w
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)//枚举所有选法(包括不选)
{
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)
tt--;
q[++tt]=j;
dp[j]=g[q[hh]]+(j-q[hh])/vi*wi;
}
}
}
cout<<dp[V];
}