「学习笔记」单调队列优化dp

算法

使用单调队列优化dp 废话

对与一些dp的转移方程,我们可以通过拆使它与某个区间的最值相关。

这时可以用单调队列算出区间最值,进行优化。

例题

最大子段和

题意

给出一个长度为 \(n\) 的整数序列,从中找出一段长度不超过 \(m\) 的连续子序列,使得整个序列的和最大。

思路

\(sum_i\)\(i\) 的前缀和,易得答案为:

\[\max_\limits{1\le i\le n}\{sum_i-\min_\limits{i-m\le k\le i-1}\{sum_k\}\} \]

其中 \(\min_\limits{i-m\le k\le i-1}\{sum_k\}\) 这部分可以用单调队列快速求出。

那么算起来就变得简单多了。

代码

点击查看代码
#include<bits/stdc++.h>
#define _for(i,a,b) for(int i=a;i<=b;++i)
#define for_(i,a,b) for(int i=a;i>=b;--i)
#define ll long long
using namespace std;
const int N=3e5+10;
int n,m,a[N],sum[N],f[N],ans;
int q[N],h=1,t=0;
inline int rnt(){
	int x=0,w=1;char c=getchar();
	while(c<'0'||c>'9'){if(c=='-')w=-1;c=getchar();}
	while(c>='0'&&c<='9')x=(x<<3)+(x<<1)+(c^48),c=getchar();
	return x*w;
}
void tmp(int k){
	while(h<=t&&q[h]<k-m)++h;
	ans=max(ans,sum[k]-sum[q[h]]);
	while(h<=t&&sum[q[t]]>sum[k])--t;
	q[++t]=k;
}
int main(){
	n=rnt(),m=rnt();
	_for(i,1,n){
		a[i]=rnt();
		sum[i]=sum[i-1]+a[i];
		tmp(i);
	}
	printf("%d\n",ans);
	return 0;
}

修剪草坪

题意

给定一个 \(n\ (1\le n\le 10^5)\) 个元素的序列,可任选多个数,要求任意一段连续选的数长度不能超过 \(k\)

\(1\le N\le 10^5\)

思路

\(f_{i,0/1}\) 表示第 \(i\) 个数选(1)或是不选(0),\(sum_i\) 表示 \(i\) 的前缀和。

转移方程为:

\[f_{i,0}=\max\{f_{i-1,0},f_{i-1,1}\}\\\ f_{i,1}=\max_\limits{i-k\le j<i}\{f_{j,0}+(sum_i-sum_j)\} \]

\(f_{i,1}\) 转移方程可以拆成:

\[f_{i,1}=sum_i+\max_\limits{i-k\le j<i}\{f_{j,0}-sum_j\} \]

用单调队列维护 \(f_{j,0}-sum_j\) 即可。

代码

点击查看代码
#include<bits/stdc++.h>
#define _for(i,a,b) for(ll i=a;i<=b;++i)
#define for_(i,a,b) for(ll i=a;i>=b;--i)
#define ll long long
using namespace std;
const ll N=3e5+10;
ll n,k,a[N],sum[N],f[N][2],ans;
ll q[N],h=1,t=0;
inline ll rnt(){
	ll x=0,w=1;char c=getchar();
	while(c<'0'||c>'9'){if(c=='-')w=-1;c=getchar();}
	while(c>='0'&&c<='9')x=(x<<3)+(x<<1)+(c^48),c=getchar();
	return x*w;
}
void tmp(ll i){
    while(h<=t&&q[h]<i-k)++h;
    f[i][1]=f[q[h]][0]+sum[i]-sum[q[h]];
    while(h<=t&&f[q[t]][0]-sum[q[t]]<f[i][0]-sum[i])--t;
    q[++t]=i;
}
int main(){
	n=rnt(),k=rnt();
    tmp(0);
	_for(i,1,n){
		a[i]=rnt();
		sum[i]=sum[i-1]+a[i];
        f[i][0]=max(f[i-1][0],f[i-1][1]);
        tmp(i);
	}
	printf("%lld\n",max(f[n][0],f[n][1]));
	return 0;
}

瑰丽华尔兹

题意

给出一个 \(N*M\) 的船上地图,有空地也有家具。

船上有 \(K\) 段时间,在每段时间都会往不同的方向倾斜,钢琴也会朝着那个方向倾斜,但不允许碰上家具。

求钢琴最长的滑行距离。

\(1\le N,M\le200,K\le200\)

思路

\(f_{i,j,k}\) 表示在第 \(i\) 段时间滑行到 \(j,k\) 的位置。

那么转移方程就是:

\[f_{i,j,k}=\max_\limits{(jj,kk)是i时刻可以滑到(j,k)的点}{f_{i,jj,kk}+|jj-j|+|kk-k|} \]

瞎写的

按着倾斜的方向遍历,再用单调队列优化优化就好了。

代码

点击查看代码
#include<bits/stdc++.h>
#define _for(i,a,b) for(int i=a;i<=b;++i)
#define for_(i,a,b) for(int i=a;i>=b;--i)
#define ll long long
using namespace std;
const int N=210,inf=0x3f3f3f3f;
int n,m,x,y,k,mp[N][N];
int f[N][N][N],la[N][N][N],ans;
inline ll rnt(){
	ll x=0,w=1;char c=getchar();
	while(c<'0'||c>'9'){if(c=='-')w=-1;c=getchar();}
	while(c>='0'&&c<='9')x=(x<<3)+(x<<1)+(c^48),c=getchar();
	return x*w;
}
inline char rch(){
	char c=getchar();
	while(c!='.'&&c!='x')c=getchar();
	return c;
}
struct dq{//deque
	int q[N],h,t;
	void nw(){
		memset(q,0,sizeof(q));
		h=1,t=0;
	}
	bool empty(){return h>t;}
	int front(){return q[h];}
	int back(){return q[t];}
	void pop_f(){++h;}
	void pop_b(){--t;}
	void push(int x){q[++t]=x;}
};
void dp(int d,int s,int t,int fx){
	int len=(t-s+1);
	if(fx==1){
		_for(j,1,m){
			dq q;q.nw();
			for_(i,n,1){
				if(mp[i][j]){
					while(!q.empty())q.pop_f();
					continue;
				}
				while(!q.empty()&&q.front()>i+len)q.pop_f();
				while(!q.empty()&&f[d-1][q.back()][j]+q.back()-i<f[d-1][i][j])q.pop_b();
				q.push(i);
				if(f[d-1][q.front()][j]>-1)
					f[d][i][j]=f[d-1][q.front()][j]+q.front()-i;
				ans=max(ans,f[d][i][j]);
			}
		}
	}
	else if(fx==2){
		_for(j,1,m){
			dq q;q.nw();
			_for(i,1,n){
				if(mp[i][j]){
					while(!q.empty())q.pop_f();
					continue;
				}
				while(!q.empty()&&q.front()<i-len)q.pop_f();
				while(!q.empty()&&f[d-1][q.back()][j]+i-q.back()<f[d-1][i][j])q.pop_b();
				q.push(i);
				if(f[d-1][q.front()][j]>-1)
					f[d][i][j]=f[d-1][q.front()][j]+i-q.front();
				ans=max(ans,f[d][i][j]);
			}
		}
	}
	else if(fx==3){
		_for(i,1,n){
			dq q;q.nw();
			for_(j,m,1){
				if(mp[i][j]){
					while(!q.empty())q.pop_f();
					continue;
				}
				while(!q.empty()&&q.front()>j+len)q.pop_f();
				while(!q.empty()&&f[d-1][i][q.back()]+q.back()-j<f[d-1][i][j])q.pop_b();
				q.push(j);
				if(f[d-1][i][q.front()]>-1)
					f[d][i][j]=f[d-1][i][q.front()]+q.front()-j;
				ans=max(ans,f[d][i][j]);
			}
		}
	}
	else{
		_for(i,1,n){
			dq q;q.nw();
			_for(j,1,m){
				if(mp[i][j]){
					while(!q.empty())q.pop_f();
					continue;
				}
				while(!q.empty()&&q.front()<j-len)q.pop_f();
				while(!q.empty()&&f[d-1][i][q.back()]+j-q.back()<f[d-1][i][j])q.pop_b();
				q.push(j);
				if(f[d-1][i][q.front()]>-1)
					f[d][i][j]=f[d-1][i][q.front()]+j-q.front();
				ans=max(ans,f[d][i][j]);
			}
		}
	}
}
int main(){
	n=rnt(),m=rnt(),x=rnt(),y=rnt(),k=rnt();
	_for(i,1,n)
		_for(j,1,m)
			mp[i][j]=(bool)(rch()=='x');
	memset(f,-inf,sizeof(f));
	f[0][x][y]=0;
	_for(i,1,k){
		int s=rnt(),t=rnt(),fx=rnt();
		dp(i,s,t,fx);
	}
	printf("%d\n",ans);
	return 0;
}

股票交易

题意

一共有 \(T\) 天,知道了每天股票的买入金额 \(ap_i\),卖出金额 \(bp_i\),买入限制 \(as_j\),卖出限制 \(bs_j\)。要求持有股票数不能超过 \(MaxP\) ,两次交易相隔 \(w\) 天。

\(1\le w<T\le 2*10^3,1\le MaxP\le 2*10^3\)

思路

\(f_{i,j}\) 表示第 \(i\) 天持有 \(j\) 个股票的最大钱数。

有四种情况:

  • 凭空买
    转移方程:

\[f_{i,j}=-ap_i*j \]

  • 不买不卖
    转移方程:

\[f_{i,j}=\max\{f_{i,j},f_{i-1,j}\} \]

  • 只买
    转移方程:

\[f_{i,j}=\max\{f_{i,j},\max_\limits{j-as_i\le k\le j}\{f_{i-w-1,k}-(j-k)*ap_i\}\} \]

  • 只卖
    转移方程:

\[f_{i,j}=\max\{f_{i,j},\max_\limits{j\le k\le j+bs_i}\{f_{i-w-1,k}+(k-j)*bp_i\}\} \]

此时时间复杂度是 \(O(T*MaxP^2)\)

观察只买和只卖的情况,可以发现它们的转移方程可以用单调队列优化掉一维。

那么时间复杂度就被优化成了 \(O(T*MaxP)\),完全可过。

代码

点击查看代码
#include<bits/stdc++.h>
#define _for(i,a,b) for(ll i=a;i<=b;++i)
#define for_(i,a,b) for(ll i=a;i>=b;--i)
#define ll long long
using namespace std;
const ll N=4010,inf=0x3f3f3f3f;
ll t,mxp,w,ap[N],bp[N],as[N],bs[N];
inline ll rnt(){
	ll x=0,w=1;char c=getchar();
	while(c<'0'||c>'9'){if(c=='-')w=-1;c=getchar();}
	while(c>='0'&&c<='9')x=(x<<3)+(x<<1)+(c^48),c=getchar();
	return x*w;
}
struct dq{//deque
	ll q[N],h,t;
	void nw(){
		memset(q,0,sizeof(q));
		h=1,t=0;
	}
	bool empty(){return h>t;}
	ll front(){return q[h];}
	ll back(){return q[t];}
	void pop_f(){++h;}
	void pop_b(){--t;}
	void push(int x){q[++t]=x;}
};
namespace SOLVE{
	ll f[N][N];
	void DpPkm(ll i){//凭空买
		_for(j,0,min(mxp,as[i]))
			f[i][j]=-ap[i]*j;
		return;
	}
	void DpBmbm(ll i){//不买也不卖
		_for(j,0,mxp)
			f[i][j]=max(f[i][j],f[i-1][j]);
		return;
	}
	void DpBuy(ll i){//原基础上买来
		dq q;q.nw();
		_for(j,0,mxp){
			while(!q.empty()&&j-q.front()>as[i])q.pop_f();
			while(!q.empty()&&f[i-w-1][j]>f[i-w-1][q.back()]-(j-q.back())*ap[i])q.pop_b();
			q.push(j);
			f[i][j]=max(f[i][j],f[i-w-1][q.front()]-(j-q.front())*ap[i]);
		}
		return;
	}
	void DpSell(ll i){//原基础上卖出
		dq q;q.nw();
		for_(j,mxp,0){
			while(!q.empty()&&q.front()-j>bs[i])q.pop_f();
			while(!q.empty()&&f[i-w-1][j]>f[i-w-1][q.back()]+(q.back()-j)*bp[i])q.pop_b();
			q.push(j);
			f[i][j]=max(f[i][j],f[i-w-1][q.front()]+(q.front()-j)*bp[i]);
		}
		return;
	}
	ll Solve(){	
		memset(f,-inf,sizeof(f));
		_for(i,1,t){
			DpPkm(i);
			DpBmbm(i);
			if(i-w>1){
				DpBuy(i);
				DpSell(i);
			}
		}
		return f[t][0];
	}
}
int main(){
	t=rnt(),mxp=rnt(),w=rnt();
	_for(i,1,t)
		ap[i]=rnt(),bp[i]=rnt(),as[i]=rnt(),bs[i]=rnt();
	printf("%lld\n",SOLVE::Solve());
	return 0;
}/*

*/
$$ \Huge{\mathfrak{The\ End}} $$
posted @ 2022-03-19 20:52  K8He  阅读(146)  评论(0编辑  收藏  举报