单调队列优化背包
单调队列优化背包
单调队列:
-
概念:单调队列是一个双端队列,内部元素具有单调性(单调增或单调减),且保持先插入的一定在后插入的前面(也就是维护第二个值——在原序列中的编号,是递增的);
有两个操作:
- 插入:从对尾插入,和目前对尾作比较,如果会破坏单调性就删除,直到找到不会破坏单调性的位置为止;
- 获取:直接从对头获取就OK,如果对头的序号(插入时所维护的第二个值,也就是在一个序列中的编号,再单调队列里是递增的)不满足要求就删除找下一个;
-
用途:
-
用来求一段固定区间的最大值或最小值,例如滑动窗口就是道很典型的题;
-
优化有关固定区间长度转移的DP,例如转移方程形如:
\[dp[i]=min/max\{dp[j]\},(j\in(i-m,i)) \]可以使用单调队列来优化;
-
优化多重背包,这也是这篇博客的主题;
-
单调队列优化多重背包
(下面都是我的个人观点,个人认为比较通俗易懂,适合像我这样的蒟蒻食用(逃))
定义\(f[i][j]\)为前\(i\)个物品使用容量\(j\)所能获得的最大价值;
对于一个容量为\(C\)的背包,对于\(d\)个价值为$ w\(,体积为\)v$ 物品;
那么对于\(f[i][j]\)它只能从\(f[i-1][j-k*v](k\leq d\ and\ k*v\leq j)\)转移过来;
对于任何一个\(j\) ,都有\(j=a*v+b\) ,那么\(f[i-1][j-k*v]\Rightarrow f[i-1][(a-k)*v+b]\) ,由此可以看出我们可以把\(f[i][j]\)的转移由不同的余数\(b\)分为\(v\)类,这里给出一个直观的状态转移方程(\(b\)可以把它当做一个常数):
(ps:由此往上你都可以通过画图(我是把它想像成了一个能量条来理解(滑稽)) 来感性理解,下面的就不太好感性理解了,我曾试图感性理解,但发现很容易就会想偏,所以不太推荐感性理解)
手动变形一下:(ps:这里\(a\)还是原来的意思,然后用\(k\)代替了\((a-k)\))
这里可以把\(a*w_i\)提出来,这样就成了你平常见到的单调队列优化的方程了:
你看,这样是不是就很类似上面的单调队列的用途\(2\)的DP方程了;
根据这个方程,我们就可以用单调队列了,找\(f[i-1][k*v_i+b]+k*w_i\)在右端点为\(a\)长度为\(d\)的区间中的最大值(这里把\(f[i-1][k*v_i+b]+k*w_i\)看成序列中的元素,\(a,k\)都为序列的下标);
这里\(f[][]\)可以用滚动数组降一维;(这个大家应该都会)
如果还有什么不懂的地方,可以结合代码来加深理解;
code
#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std;
const int N=1e4+5;
int f[N];
int n,m,C;
int q[N],val[N];
inline int read()
{
int f=1;
char ch;
while((ch=getchar())<'0'||ch>'9') if(ch=='-') f=-1;
int res=ch-'0';
while((ch=getchar())>='0'&&ch<='9') res=res*10+ch-'0';
return res*f;
}
inline void solve(int v,int w,int d)//单调队列优化。
{
for(int b=0;b<v;b++)
{
int k=0,head=1,tail=0;
for(int j=b;j<=C;j+=v,k++)
{
int tmp=f[j]-k*w;
while(head<=tail&&tmp>val[tail]) tail--;//滚动数组,所以要先插入。
q[++tail]=k,val[tail]=tmp;
while(head<=tail&&k-q[head]>d) head++;
f[j]=val[head]+k*w;
}
}
}
int main()
{
#ifndef ONLINE_JUDGE
freopen("travel.in","r",stdin);
freopen("travel.out","w",stdout);
#endif
n=read(),m=read(),C=read();
for(int i=1,v,w,d;i<=n;i++)
{
v=read(),w=read(),d=read();
solve(v,w,d);
}
for(int i=1,a,b,c;i<=m;i++)
{
a=read(),b=read(),c=read();
for(int j=C;j>=0;j--)
for(int k=0;k<=j;k++)
f[j]=max(f[j],f[j-k]+k*k*a+k*b+c);
}
printf("%d",f[C]);
return 0;
}
对于第二种情况,直接\(c^2\)暴力枚举就好了,千万不要考虑二次函数的优化,数据有锅,越优化越慢,不信你去题解里找到那篇优化了的交一下,T飞。(不知道是不是我背)
这道题还考你卡常的技巧,卡卡常就过了,还要看评测机给不给力,运气好的话,一遍就过了,如果不行就多交几遍(本人亲身经历) 。
对于这道题,你还可以试试二进制拆分,不过据说会T两个点(卡卡常说不定就过了)。