动态规划之单调队列单调栈优化

一.前言

这是一种典型的线性动态规划类型,这种题型的题目通常的朴素方法是两层循环暴力解决,但第二维可以使用单调队列或单调栈来对算法的时间复杂度进行优化。

二.一般情景如下

线性动态规划通过第二维循环前面的点来维护第一维的当前结点的最优值。
寻找区间的最优值
但并不是所有点都对当前结点有dp的意义,即有些点不可能更新当前结点的最优值。
我们用队列或栈去存储有dp意义的点,让其去维护当前结点。
通常这样的队列都满足单调性

三.例题讲解

1.例题一:单调栈

例题1

朴素算法的主循环保持不变,对第二层循环进行优化。可以看出:如果下标大的点,值也比前面的值大,那么其前面的数就没有存在的必要,即没有dp的意义。我们用一个栈存储有dp意义的点,根据前面的分析,这个栈显然是单调递减的。而若再次出现下标大的点,值也比前面的值大,则将该栈顶中无意义的点依次排除。

#include<bits/stdc++.h>
using namespace std;

long long a[1000005],stk[1000005],top;

int main(){
    
    
	long long n,ans=-1e9;
	cin>>n;
	for(int i=1;i<=n;i++)scanf("%d",&a[i]);
	
	for(int i=1;i<=n;i++){
    
    
		for(int j=1;j<=top;j++)ans=max(ans,(stk[j]+i)*min(a[stk[j]],a[i]));
		ans=max(ans,a[i]*i);
		while(a[i]>=a[stk[top]]&&top!=0)top--;//将无意义的点依次丢弃 
		stk[++top]=i; 
	}
	cout<<ans;
	return 0;
} 

仍然超时
能否再优化一下这个单调栈呢???

在对第i个点处理时,在单调栈中的值比第i个点值大的点中,下标最大的点最优,而其他点在与第i个点的配对过程中,暂时没有dp的必要,因为min(stk[j],a[i])

#include<bits/stdc++.h>
using namespace std;

long long a[10000005],stk[10000005],top;

int main(){
    
    
	long long n,ans=-1e9;
	cin>>n;
	for(int i=1;i<=n;i++)scanf("%d",&a[i]);
	
	for(int i=1;i<=n;i++){
    
    
		ans=max(ans,a[i]*i);
		while(a[i]>=a[stk[top]]&&top!=0){
    
    //将无意义的点依次丢弃
			ans=max(ans,(stk[top]+i)*a[stk[top]]);//更新最优结果 
			top--;
		}
		ans=max(ans,(i+stk[top])*a[i]);//在值大于a[i]的点中,只有下标最大的点有dp的必要 
		stk[++top]=i;
	}
	cout<<ans;
	return 0;
} 

2.例题2:单调队列

例题2

朴素算法两层循环用 [ i-r,i-l ] 区间去维护第i个点的值
代码如下

#include<bits/stdc++.h>
using namespace std;
int a[1000010],f[1000010];

int main(){
    
    
	int n,l,r;
	cin>>n>>l>>r;
	for(int i=0;i<=n;i++)cin>>a[i];
	
	f[0]=a[0];
	for(int i=1;i<=n+r;i++){
    
    
		f[i]=-1e9;
		for(int j=i-r;j<=i-l;j++){
    
    
			if(j<0)continue;
			f[i]=max(f[i],f[j]+a[i]);
		}
	}
	int ans=-1e9;
	for(int i=n+1;i<=n+r;i++)ans=max(ans,f[i]);
	cout<<ans;
	return 0;
}

超时,想想方法
用单调队列的思想思考下, [ i-r,i-l ] 区间上的点并不都对第i个点有dp的意义

判断一下什么时候有dp的意义
设 l-r <= x < y <= i-l,
若f[y]>=f[x],则x点没有dp的意义
若f[y]<f[x],随 i 的逐渐变大,x点可能会消失,此时y点存在意义

易知:能维护第i个点的点是一个单调队列

代码如下
ps:此时的队列head指向头,rear指向尾的下一个

#include<bits/stdc++.h>
using namespace std;

int a[1000010],f[1000010],q[1000010],head,rear;

int main(){
    
    
	int n,l,r;
	cin>>n>>l>>r;
	for(int i=0;i<=n;i++)cin>>a[i];
	f[0]=a[0];
	for(int i=1;i<=n+r;i++)f[i]=-1e9;
	
	for(int i=l;i<=n+r;i++){
    
    
		while(head<rear&&q[head]<i-r)head++;//前面的点因为距离,失去意义
		while(head<rear&&f[q[rear-1]]<=f[i-l])rear--;//无意义的点剔除
		q[rear++]=i-l;
		f[i]=f[q[head]]+a[i];
	}
	int ans=-1e9;
	for(int i=n+1;i<=n+r;i++)ans=max(ans,f[i]);
	cout<<ans;
	return 0;
}

ps:其实这道题 f[i] 从区间的最小值中转移而来,可以用树状数组或线段树来维护区间最值也可以做

例题3:单调队列

例题3

朴素的算法两层循环用区间 [ i-k,i ] 去维护前 i 个点的最优值 f[i]
区间 [ i-k,i ] 的点需要有一个点不被取,循环判断即可

#include<bits/stdc++.h>
using namespace std;

int a[200010],f[200010],qian[200010];

int main(){
    
    
	int n,k;
	cin>>n>>k;
	for(int i=1;i<=n;i++){
    
    
		cin>>a[i];
		qian[i]=qian[i-1]+a[i];
	}
	for(int i=1;i<=k;i++)f[i]=qian[i];
	
	for(int i=k+1;i<=n;i++){
    
    
		for(int j=i-k;j<=i;j++)f[i]=max(f[i],qian[i]-qian[j]+f[j-1]);
	} 
	cout<<f[n];
	return 0;
}

超时,想想办法优化

判断一下什么时候有dp的意义
不妨设 g[j] = f[j-1]-qian[j],易知:g[j]越大,g[j]+qian[i]越大
设 i-k <= x < y <= i
若g[y]>=g[x],则x点没有dp的意义
若g[y]<g[x],随 i 逐渐增大,x点可能会消失,此时y点存在dp意义
发现和第二题非常相似

代码如下

#include<bits/stdc++.h>
using namespace std;

int a[200010],f[200010],qian[200010],g[200010],q[200010],head,rear;

int main() {
    
    
	int n,k;
	cin>>n>>k;
	for(int i=1; i<=n; i++) {
    
    
		cin>>a[i];
		qian[i]=qian[i-1]+a[i];
	}
	for(int i=1; i<=k; i++) {
    
    
		f[i]=qian[i];
		g[i]=f[i-1]-qian[i];
		
		while(head<rear&&g[q[rear-1]]<=g[i])rear--;
		q[rear++]=i;
	}
	for(int i=k+1; i<=n; i++) {
    
    
		g[i]=f[i-1]-qian[i];//计算g[i]
        
		while(head<rear&&i-q[head]>k)head++;//前面的点因为距离,失去意义
		while(head<rear&&g[q[rear-1]]<=g[i])rear--;//无意义的点剔除
		q[rear++]=i;
		
		f[i]=qian[i]+g[q[head]];
	}
	cout<<f[n];
	return 0;
}

猜你喜欢

转载自blog.csdn.net/weixin_43602607/article/details/113354200