单调队列优化动态规划
前置知识:单调队列
1.1例题
例题1:琪露诺
常见思路:首先容易推出朴素转移方程:
令 \(f_i\) 表示琪露诺在在 \(i\) 格时累计能获得多少冰冻指数,\(f_i=a_i+\min\limits_{i-r\le j\le i-l} f_j\),时间复杂度为 \(O(n^2)\),考虑优化。
容易发现 \(f_j\) 中 \(j\) 的限制就像是一个滑动窗口,所以可以用单调队列优化。
操作:①令 \(h=1\),\(t=0\)
②遍历到一个新的 \(i\)。
③先更新队尾:将在新元素(指能转移到当前 \(f_i\) 的元素却无法转移到 \(f_{i-1}\) 的元素)前面且不比它优的元素从队尾出队,再加入新元素。
④再更新队头:如果队头超出长度限制,从队头出队。
⑤这时已经保证了单调队列中有 \(i-r≤j≤i-l\) 的所有 有可能有用的 \(j\)(即有可能转移到后面的 \(j\))都在队列里且有单调性,此时队头为最优转移的位置,用队头更新 \(f_i\) 即可。
时间复杂度:\(O(n)\) 。
点击查看代码
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
int n,l,r,f[200010],a[200010],q[200010],ans=-0x7fffffff;
int main(){
ios::sync_with_stdio(0);
cin>>n>>l>>r;
for(int i=0;i<=n;i++)cin>>a[i];
for(int i=1;i<=n;i++)f[i]=-0x7fffffff;
for(int i=l,h=1,t=0;i<=n;i++){
while(f[q[t]]<=f[i-l]&&h<=t)t--;
q[++t]=i-l;
while(q[h]+r<i)h++;
f[i]=f[q[h]]+a[i];
if(i+r>n)ans=max(ans,f[i]);
}
cout<<ans<<'\n';
return 0;
}
例题2:[NOIP2017 普及组] 跳房子
由于题目要求满足某个条件至少要花多少金币,而且容易发现 \(x\) 个金币可以做到的事 \(x+1\) 个金币也可以,所以考虑用二分解决。
如何判断花了 \(g\) 枚金币能否获得至少 \(k\) 分?求出花了 \(g\) 枚金币最多能获得多少分即可。
考虑用动态规划,可以令 \(f_i\) 表示小 \(R\) 在第 \(i\) 格时累计能获得多少分数,得到转移方程:\(f_i=s_i+\max\limits_{x_i-g-d≤x_j≤min(x_i-1,x_i+g-d)}{f_j}\)。
动态规划的时间复杂度为 \(O(n^2)\),考虑优化。
操作:①令 \(h=1\),\(t=0\)。
②遍历到一个新的 \(i\)。
③先更新队尾:将在新元素(指能转移到当前 \(f_i\) 的元素却无法转移到 \(f_{i-1}\) 的元素)前面且不比它优的元素从队尾出队,再加入新元素。但是注意这里的新元素可能不止 \(1\) 个,所以要while
套while
,将所有新元素判断并入队。
④再更新队头:如果队头超出长度限制,从队头出队。
⑤这时已经保证了单调队列中有 \(x_i-g-d≤x_j≤min(x_i-1,x_i+g-d)\) 的所有有可能有用的 \(j\)(即有可能转移到后面的 \(j\))都在队列里且有单调性,此时队头为最优转移的位置,用队头更新 \(f_i\) 即可。
动态规划的时间复杂度:\(O(n)\),可以通过此题。
点击查看代码
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
ll l=0,r=0,s=0,n,d,k,f[500010],q[500010],a[500010],x[500010],ans=0;
bool ch(ll p){
memset(f,-0x3f3f3f3f3f3f3f3f,sizeof(f));
memset(q,0,sizeof(q));
f[0]=0;
ll res=-0x3f3f3f3f3f3f3f3f;
for(ll i=1,j=0,h=1,t=0;i<=n;i++){
while(j<i&&x[i]-x[j]>=d-p){
while(h<=t&&f[q[t]]<=f[j])t--;
q[++t]=j++;
}
while(h<=t&&x[i]-x[q[h]]>d+p)h++;
if(h<=t)f[i]=f[q[h]]+a[i];
res=max(res,f[i]);
}
return res>=k;
}
int main(){
cin>>n>>d>>k;
for(ll i=1;i<=n;i++){
cin>>x[i]>>a[i];
if(a[i]>0)s+=a[i];
r=max(r,x[i]);
}
if(s<k){cout<<-1<<'\n';return 0;}
while(l<=r){
ll mid=(l+r)>>1;
if(ch(mid))ans=mid,r=mid-1;
else l=mid+1;
}
cout<<ans<<'\n';
return 0;
}
例题3:P6040
这一题有些复杂,但是看完题后还是可以列出转移方程:令 \(f_i\) 表示在辅导第 \(i\) 个同学时在前 \(i\) 个同学身上花费的最小精力,则枚举上一个辅导的同学 \(j\) ,表示 \(f_i=a_i+\min\limits_{i-x≤j<i}{fj+(i-j-1)×d+k}\)。
这里好像让人无从下手,但是我们充分发扬乱搞推式子精神,拆开发现式子等同于 \(f_i=k+a_i+(i-1)×d+\min\limits_{i-x≤j<i}{f_j-j×d}\)。那我们可以把 \(f_j-j×d\) 作为一个整体,让单调队列维护的是 \(f_j-j×d\) 单调递减的 \(j\) 即可。
注意,这里之所以先h++
是因为在所有操作开始前已经将 \(1\) 塞进了队列里。
点击查看代码
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
inline int read(){
int x=0,flag=1;char c;
while((c=getchar())<'0'||c>'9')if(c=='-')flag=0;
while(c>='0'&&c<='9')x=(x<<3)+(x<<1)+(c^48),c=getchar();
return flag?x:-x;
}
ll n,k,d,x,tp,Seed,h=1,t=0,q[10000010],f[10000010];
inline int rnd(){
static const int MOD = 1e9;
return Seed = ( 1LL * Seed * 0x66CCFF % MOD + 20120712 ) % MOD;
}
int main(){
n=read();k=read();d=read();x=read();tp=read();
if(tp){
Seed=read();
f[1]=rnd();q[++t]=1;
for(ll i=2;i<=n;i++){
ll qwq=rnd();
while(h<=t&&q[h]<i-x)h++;
f[i]=f[q[h]]+k+(i-q[h]-1)*d+qwq;
while(h<=t&&f[q[t]]-q[t]*d>=f[i]-i*d)t--;
q[++t]=i;
}
cout<<f[n]<<'\n';
}else{
f[1]=read();q[++t]=1;
for(ll i=2;i<=n;i++){
ll qwq=read();
while(h<=t&&q[h]<i-x)h++;
f[i]=f[q[h]]+k+(i-q[h]-1)*d+qwq;
while(h<=t&&f[q[t]]-q[t]*d>=f[i]-i*d)t--;
q[++t]=i;
}
cout<<f[n]<<'\n';
}
return 0;
}
总结:单调队列优化动态规划适用于式子可以化为 \(f_i=q(i)+\max\limits_{l(i)≤j≤r(i)}{f_j+p(j)}\) ,并且满足 \(l(i)\) 和 \(r(i)\) 都单不减的 \(1D/1D\) 型动态规划。
有三个步骤:入队、出队、转移。
1.2 习题
咕咕咕