单调队列优化动态规划

前置知识:单调队列

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\) 个,所以要whilewhile,将所有新元素判断并入队。
④再更新队头:如果队头超出长度限制,从队头出队。
⑤这时已经保证了单调队列中有 \(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 习题

咕咕咕

posted @ 2023-04-17 08:12  lrxQwQ  阅读(108)  评论(0)    收藏  举报