笔者大概看了一下单调队列对于DP的优化,故撰此文,望有帮助。
(dp还是推式子难啊qwq)
题目大意:在n个数的序列中,选择数字,使得其连续不超过k个数,且和最大。
本题的方程相对好推:设dp[i][0/1]为到了第i个数,且第i个数不选/选的最大值。
则有转移:dp[i][0]=max(dp[i-1][0],dp[i-1][1])
dp[i][1]=max{dp[j][0]-sum[j]+sum[i]},i-k<=j<i
枚举j即可。
但是题目会这么让你水过吗?
发现会超时,优化不可避免。
仔细观察方程,考虑它的特殊性。
对于方程1,我们不便再多做什么。但是对于方程2,显然仍有优化余地。
我们将sum[i]提出来,得到:
dp[i][1]=max{dp[j][0]-sum[j]}+sum[i].
这个方程只与j有关,我们让max里面的最大就好了。
维护它,我们可以用堆,也可以用单调队列,线段树。
本文主要讲对于单调队列的优化。它可以保证O(n)的时间复杂度。
首先,我们明确一点,我们维护的队首元素最大。显然,队列中的数字要单调递减。
其次,我们要严格确保我们查询的区间,保证队列中没有没用的数字。
并且每次枚举到下一个数的时候,注意更新队列。
考虑何时更新更优:
首先,当这个数字不在需要的范围的时候,删除即可。
其次,对于新插入的数字,我们要从队尾插入,比较哪个值更优:
判断它们的“浪费情况”即可。
用q[]表示队列,s[]表示前缀和,则判断:
s[q[head]]-f[q[head]][0]>s[i]-f[i][0]&&head<=tail
如果符合的话,就把它删掉吧。因为队尾的元素所浪费的比新插入的值多,显然一定不如它优。
由此,我们已经保证了单调队列的稳定复杂度。
给出代码:
#include<cstdio> #include<iostream> using namespace std; long long q[2000000],f[2000000][2]; long long n,k,s[2000000],a[2000000]; long long tail,head; int main(){ scanf("%lld%lld",&n,&k); for(int i=1;i<=n;++i){ scanf("%lld",&a[i]); s[i]=s[i-1]+a[i];//sum } tail=head=1;//初始化 for(int i=1;i<=n;++i){ f[i][0]=max(f[i-1][0],f[i-1][1]);//对于不选i,只考虑前面两个即可 while(q[head]<i-k&&head<=tail)head++;//判断队头是否在所找区间内 f[i][1]=f[q[head]][0]-s[q[head]]+s[i];//取MAX转移 while(f[i][0]-s[i]>f[q[tail]][0]-s[q[tail]]&&head<=tail)tail--; q[++tail]=i;//更新队尾,当队列有数且当前队尾若插入i不满足单调性时 //写成s[i]-f[i][0]<s[q[tail]]-f[q[tail]][0]也可以 //可以理解为选到i和队尾时,两者不选的奶牛的效率和相比较,显然浪费少的更优,不优的删除即可 }printf("%lld ",max(f[n][0],f[n][1])); return 0; }
双倍经验:P2034
持续更新中。