单调队列

单调队列

考虑在一个序列中维护一个类似于窗口的东西。

以下不妨设求得是窗口最大值。

首先根据贪心,如果当前数整个窗口中最大的,并且是最靠前的,那么这个数前面的所有数都不会对答案产生一点贡献。于是考虑维护一个单调递增的序列,需要从中找出答案。设置一个首指针,未指针代表这个窗口的开始和结束。

然后,考虑一个和莫队类似的操作:

Remove 操作:将比当前值小的数扔出维护序列即可。

Add 操作:直接将当前数加入到队伍的最后段(这里可以仔细想一想,为什么)。

然后按照这样的操作模拟即可。

时间复杂度分析

这是很简单的,因为每一个元素最多只被出队和入队各 \(1\) 次,所以时间复杂度是 \(O(n)\)。

模板

模板题目:P1886 滑动窗口

AC code

CPP 复制代码
#include<bits/stdc++.h>
using namespace std;
const int N=1e6+1000;
int n,k;
int number[N];
int num[N];

int main(){
	ios::sync_with_stdio(0);
	cin>>n>>k;
	for(int i=1;i<=n;i++)cin>>number[i];
	int fst=0,ed=-1;
	for(int i=1;i<k;i++){
		while(fst<=ed&&number[num[ed]]>=number[i])ed--;
		num[++ed]=i;
	}
	for(int i=k;i<=n;i++){
		while(fst<=ed&&number[num[ed]]>=number[i])ed--;
		num[++ed]=i;
		while(num[fst]<=i-k)fst++;
		cout<<number[num[fst]]<<' ';
	}
	for(int i=0;i<=ed;i++)num[i]=0;
	fst=0,ed=-1;
	cout<<'\n';
	for(int i=1;i<k;i++){
		while(fst<=ed&&number[num[ed]]<=number[i])ed--;
		num[++ed]=i;
	}
	for(int i=k;i<=n;i++){
		while(fst<=ed&&number[num[ed]]<=number[i])ed--;
		num[++ed]=i;
		while(num[fst]<=i-k)fst++;
		cout<<number[num[fst]]<<' ';
	}
	return 0;
}	

优点分析

单调队列可以在 \(O(n)\) 的时间复杂度之内求出一个给定区间长度的整个序列中的区间最大值,在这一点上,它比 线段树ST表 优化了一个 \(O(\log n)\) 的时间。

例题

P2698

题目形式化:

  • 给出一条线段上的 \(n\) 个点,每个点有一个权值 \(𝑦 𝑖\),你要找出一段区间,使区间中的 \(𝑦{𝑚𝑎𝑥}−𝑦_{𝑚𝑖𝑛}\) 大于 \(d\) 。

  • 输出这个区间的长度

考虑二分区间长度。

开两个单调队列记录最大值和最小值吗,每次用 \(O(n)\) 进行判断即可。总时间复杂度 \(O(n\log n)\)

AC code

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;
const int N=1e6+100;
struct node{
	int x;
	int height;
}rain[N];
bool cmp(node a,node b){
	return a.x==b.x?a.height<b.height:a.x<b.x;
}
#define L(i,j,k) for(int i=j;i<=k;i++)
int n,d;
int fst_Max,ed_Max;
int fst_Min,ed_Min;
int num_Max[N];
int num_Min[N];
bool check(int len){
	L(i,0,ed_Max)num_Max[i]=0;
	L(i,0,ed_Min)num_Min[i]=0;
	fst_Max=0,ed_Max=-1;
	fst_Min=0,ed_Min=-1;
	L(i,1,n){
		while(rain[num_Max[fst_Max]].x<rain[i].x-len&&fst_Max<=ed_Max)fst_Max++;
		while(fst_Max<=ed_Max&&rain[num_Max[ed_Max]].height<=rain[i].height)ed_Max--;
		num_Max[++ed_Max]=i;
		
		
		while(rain[num_Min[fst_Min]].x<rain[i].x-len&&fst_Min<=ed_Min)fst_Min++;
		while(fst_Min<=ed_Min&&rain[num_Min[ed_Min]].height>=rain[i].height)ed_Min--;
		num_Min[++ed_Min]=i;
		
//		cout<<fst_Max<<' '<<fst_Min<<'\n';
//		cout<<"Max:"<<rain[num_Max[fst_Max]].height<<'\n';
//		cout<<"Min:"<<rain[num_Min[fst_Min]].height<<'\n';
//		cout<<rain[num_Max[fst_Max]].height-rain[num_Min[fst_Min]].height<<' '<<d<<'\n';
		if(rain[num_Max[fst_Max]].height-rain[num_Min[fst_Min]].height>=d)return true;
	}
	return false;
}
int l,r;
int mid,ans=-1;
int main(){
	scanf("%d %d",&n,&d);
	L(i,1,n)scanf("%d %d",&rain[i].x,&rain[i].height);
	sort(rain+1,rain+n+1,cmp);
//	L(i,1,n)cout<<rain[i].x<<' '<<rain[i].height<<'\n';
	l=0,r=1e6+10;
//	check(2);
	while(l<=r){
		mid=(l+r)>>1;
		if(check(mid)){
			r=mid-1;
			ans=mid;
		}else l=mid+1;
	}
	printf("%d\n",ans);
	return 0;
}

P2216

考虑使用多次单调队列。

可以先将整个矩阵按照行的顺序找出每个 \(1\times n\) 中的最大值,然后再在刚刚得出的这个矩阵中按照列的顺序找出每个 \(n\times 1\) 的矩阵的最大值。

这时候,你就得到了一个 \((a-n+1)\times (b-n+1)\) 的新矩阵,直接找出 \(\displaystyle \min_{1\le i\le a-n+1,1\le j\le b-n+1}(Max_{i,j}-Min_{i,j})\) 就是答案,证明是显然的。

AC code

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;
const int N=1e3+10;
int a,b,n;
#define L(i,j,k) for(int i=j;i<=k;i++)
int number[N][N];
vector<int> Max[N],Min[N];
vector<int> final_Max[N],final_Min[N];
int num[N];
int h_Max[N],l_Max[N];
int h_Min[N],l_Min[N];
int fst,ed;
int main(){
	scanf("%d %d %d",&a,&b,&n);
	memset(h_Min,0x3f3f3f3f,sizeof(h_Min));
	memset(l_Min,0x3f3f3f3f,sizeof(l_Min));
	L(i,1,a)L(j,1,b)scanf("%d",&number[i][j]);
	
	L(i,1,a){
		L(j,0,ed)num[j]=0;
		fst=0,ed=-1;
		L(j,1,n-1){
			while(fst<=ed&&number[i][num[ed]]<=number[i][j])ed--;
			num[++ed]=j;
		}
		L(j,n,b){
			while(num[fst]<=j-n)fst++;
			while(fst<=ed&&number[i][num[ed]]<=number[i][j])ed--;
			num[++ed]=j;
//			cout<<i<<' '<<j<<' '<<num[fst]<<'\n';
			Max[i].push_back(number[i][num[fst]]);
		}
	}
	
//	cout<<"finish!\n";
//	cout<<"Max:\n";
//	L(i,1,a){
//		int len=Max[i].size()-1;
//		L(j,0,len)cout<<Max[i][j]<<' ';
//		cout<<'\n';
//	}

	L(i,1,a){
		L(j,0,ed)num[j]=0;
		fst=0,ed=-1;
		L(j,1,n-1){
			while(fst<=ed&&number[i][num[ed]]>=number[i][j])ed--;
			num[++ed]=j;
		}
		L(j,n,b){
			while(num[fst]<=j-n)fst++;
			while(fst<=ed&&number[i][num[ed]]>=number[i][j])ed--;
			num[++ed]=j;
			Min[i].push_back(number[i][num[fst]]);
		}
	}
	
//	cout<<"Min:\n";
//	L(i,1,a){
//		int len=Max[i].size()-1;
//		L(j,0,len)cout<<Min[i][j]<<' ';
//		cout<<'\n';
//	}

	L(i,0,b-n){
		L(j,0,ed)num[j]=0;
		fst=0,ed=-1;
		L(j,1,n-1){
			while(fst<=ed&&Max[num[ed]][i]<=Max[j][i])ed--;
			num[++ed]=j;
		}
		L(j,n,a){
			while(num[fst]<=j-n)fst++;
			while(fst<=ed&&Max[num[ed]][i]<=Max[j][i])ed--;
			num[++ed]=j;
			final_Max[i].push_back(Max[num[fst]][i]);
		}
	}
	
	L(i,0,b-n){
		L(j,0,ed)num[j]=0;
		fst=0,ed=-1;
		L(j,1,n-1){
			while(fst<=ed&&Min[num[ed]][i]>=Min[j][i])ed--;
			num[++ed]=j;
		}
		L(j,n,a){
			while(num[fst]<=j-n)fst++;
			while(fst<=ed&&Min[num[ed]][i]>=Min[j][i])ed--;
			num[++ed]=j;
			final_Min[i].push_back(Min[num[fst]][i]);
		}
	}
	
//	cout<<"final_Max:\n";
//	L(i,0,b-n){
//		int len=final_Max[i].size()-1;
//		L(j,0,len)cout<<final_Max[i][j]<<' ';
//		cout<<'\n';
//	}
//	
//	cout<<"final_Min:\n";
//	L(i,0,b-n){
//		int len=final_Min[i].size()-1;
//		L(j,0,len)cout<<final_Min[i][j]<<' ';
//		cout<<'\n';
//	}
	
	
	int ans=2e9;
	L(i,0,b-n){
		L(j,0,a-n){
			ans=min(ans,final_Max[i][j]-final_Min[i][j]);
		}
	}
	printf("%d",ans);
	return 0;
}

单调队列优化 DP

首先看看为什么单调队列可以优化 DP

DP 中,如果需要一个固定区间长度 \(k_0\) 的最小值:

\(dp_i=\displaystyle \min_{i-k_0-1\le k\le i-1}dp_k+value_i\)

那么这时候,就可以使用单调队列优化 DP,只是每次加入元素值的时候,需要先算出当前的 DP 值,然后在加入单调队列中即可。

时间复杂度分析

在以上的例子中,将原来 \(dp\) 的 \(O(nk_0)/O(n\log k_0)\)(暴力/线段树) 的时间复杂度优化为了 \(O(n)\)。所以,单调队列的时间优化是十分优秀的。

例题

P2034

首先,将问题转化为反面,即为最小的删数大小。

设立 \(dp_i\) 表示删除第 \(i\) 个数所能得到的最小贡献值。

考虑 DP

When i≤k+1,\(dp_i=\displaystyle number_i\)

When i≥k+2,\(dp_i=\displaystyle \min_{i-k-1\le j\le i-1} dp_j+number_i\)

发现当 \(i\ge k+2\) 时,正好符合对于单调队列优化 DP 的要求。

于是直接写即可。

AC code

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;
const int N=1e6+100;
#define L(i,j,k) for(int i=j;i<=k;i++)
#define ll long long
ll dp[N],tot;
int n,k;
int number[N];
int fst,ed=-1;
int num[N];
int main(){
	scanf("%d %d",&n,&k);k++;
	L(i,1,n)scanf("%d",&number[i]),tot+=number[i];
	L(i,1,k){
		dp[i]=number[i];
		while(fst<=ed&&dp[num[ed]]>=dp[i])ed--;
		num[++ed]=i;
	}
	L(i,k+1,n){
		dp[i]=dp[num[fst]]+number[i];
		while(fst<=ed&&dp[num[ed]]>=dp[i])ed--;
		num[++ed]=i;
		while(num[fst]<=i-k)fst++;
	}
	ll Min=1e18;
	L(i,n-k+1,n)Min=min(dp[i],Min);
	printf("%lld\n",tot-Min);
	return 0;
}

CF372C

注意到 \(m\le 300\),于是考虑一个 \(O(n^2m)\) 的 DP

设定 \(dp_{i,j}\) 表示当第 \(i\) 个烟花发射时,在位置 \(j\) 所能得到的最大收益。

然后可以得出如下转移方程:

定义 \(T=t_i-t_{i-1}\)。

\(dp_{1,j}=b_1-|a_1-j|\)

\(dp_{i,j}=\displaystyle \max_{\max(1,j-d\times T)\le k\le \min(n,j+d\times T)}dp_{i-1,k}+b_i-|a_i-j|\)。

然后突然发现 \(\displaystyle \max_{\max(1,j-d\times T)\le k\le \min(n,j+d\times T)}dp_{i-1,k}\) 可以单调队列,于是直接写。

可以将 \(\displaystyle \max_{\max(1,j-d\times T)\le k\le \min(n,j+d\times T)}dp_{i-1,k}\) 分成比 \(j\) 小的和比 \(j\) 大的两部分来计算。

然后突然发现 \(O(nm)\) 的空间根本过不了,于是滚动数组优化即可。

AC code

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define L(i,j,k) for(register int i=j;i<=k;i++)
#define R(i,j,k) for(register int i=j;i>=k;i--)
const int N=1e5+5e4+100;
const int M=310;
ll dp[2][N];
ll a[M],b[M],t[M];
int n,m,d;
int num[N];
int abs(int x){if(x<0)return -x;return x;}
int main(){
	scanf("%d %d %d",&n,&m,&d);
	memset(dp,0xFE,sizeof(dp));
	L(i,1,m)scanf("%lld %lld %lld",&a[i],&b[i],&t[i]);
	L(i,1,n)dp[1][i]=b[1]-abs(a[1]-i);
	L(i,2,m){
		int now=i&1,last=now^1;
		ll T=t[i]-t[i-1];
		int fst=0,ed=-1;
		memset(dp[now],0xFE,sizeof(dp[now]));
		L(j,1,n){
			while(fst<=ed&&num[fst]<j-T*d)fst++;
			while(fst<=ed&&dp[last][num[ed]]<=dp[last][j])ed--;
			num[++ed]=j;
			dp[now][j]=max(dp[now][j],dp[last][num[fst]]+b[i]-abs(a[i]-j));
		}
		fst=0,ed=-1;
		R(j,n,1){
			while(fst<=ed&&num[fst]>j+T*d)fst++;
			while(fst<=ed&&dp[last][num[ed]]<=dp[last][j])ed--;
			num[++ed]=j;
			dp[now][j]=max(dp[now][j],dp[last][num[fst]]+b[i]-abs(a[i]-j));
		}
	}
	ll ans=-1e18;
	L(i,1,n)ans=max(ans,dp[m&1][i]);
	printf("%lld\n",ans);
	return 0;
}