单调队列

前言

​ 单调队列并不是太难的东西,不应其应用到的题目困难而觉得单调队列困难.

​ 我第一次遇见单调队列时是在学图论时,遇到了Island这道题(见基环树专题),当时的我对单调队列一无所知,而对其优化更是懵,所以当时就懵着将题解半抄半写地打了出来,但还是不懂.现在来看,单论单调队列,它是不难的,难的是与其他算法的结合;

单调队列

限制与应用

​ 对于单调队列,有两个操作,入队和出队;

​ 有两个限制,队首元素满足区间条件,队列中数满足单调;

​ 应用限制,转移DP时应满足区间取max-min操作,即让单调有其发挥作用的空间;

入门

​ 推荐这道例题理解一下单调队列思想;

老板需要你帮忙浇花。给出N滴水的坐标,y表示水滴的高度,x表示它下落到x轴的位置。

每滴水以每秒1个单位长度的速度下落。你需要把花盆放在x轴上的某个位置,使得从被花盆接着的第1滴水开始,到被花盆接着的最后1滴水结束,之间的时间差至少为D。

我们认为,只要水滴落到x轴上,与花盆的边沿对齐,就认为被接住。给出N滴水的坐标和D的大小,请算出最小的花盆的宽度W。

​ 我们首先考虑到答案是具有单调性的,宽度W满足条件,那么大于W的也会满足条件,那么我们可以二分答案;

​ 二分答案后,我们可以得到一个区间,我们想要的即所有区间中是否存在 \(max-min>=D\) ;

​ 这里就引入了单调队列,单调队列的队首表示区间 \(i-mid-1\) ~ \(i\) 最大值或者最小值,其限制条件即 \(q[l]>=i-mid-1\) ,不满足条件时弹出.

​ 入队时,比较队尾元素和要加入元素值,比较方式是 "比我小的人还比我强,那我就要退役了" ,即未来会更新到我们要加入值,存在时间肯定比队尾元素长,而队尾元素又比要加入的值小,那么队尾元素的贡献就可以被要加入的值代替,故直接弹出;

​ 那么这里我们限制一下区间,求出区间最大值与最小值,更新最大差值即可;

#include<bits/stdc++.h>
using namespace std;
const int maxn=1e5+7,inf=1e9;
int n,d,lim,q[maxn],f[maxn];
struct node{
	int x,y;
}a[maxn];

template<typename type_of_scan>
inline void scan(type_of_scan &x){
	type_of_scan f=1;x=0;char s=getchar();
	while(s<'0'||s>'9'){if(s=='-')f=-1;s=getchar();}
	while(s>='0'&&s<='9'){x=x*10+s-'0';s=getchar();}
	x*=f;
}

bool check(int mid){
	int l=1,r=0,x=1,y=0,ans=0;
	for(int i=1;i<=n;i++){
		int maxx=-1,minx=inf;
		while(l<=r&&a[q[l]].x<=a[i].x-mid-1) l++;//限制区间弹出,注意区间大小为mid+1 
		while(l<=r&&a[q[r]].y<=a[i].y) r--;//根据原则弹出 
		q[++r]=i;maxx=max(a[q[l]].y,maxx);//更新,下面是一样的 
		while(x<=y&&a[f[x]].x<=a[i].x-mid-1) x++;
		while(x<=y&&a[f[y]].y>=a[i].y) y--;
		f[++y]=i;minx=min(a[f[x]].y,minx);
		ans=max(maxx-minx,ans);
	}
	return ans>=d;
}

bool cmp(node a,node b){
	return a.x<b.x;
}

int main(){
	scan(n),scan(d);
	for(int i=1;i<=n;i++) scan(a[i].x),scan(a[i].y),lim=max(a[i].x,lim);
	sort(a+1,a+1+n,cmp);//排序 
	int l=1,r=lim;
	while(r>l){
		int mid=l+r>>1;
		if(check(mid)) r=mid;
		else l=mid+1;
	}
	if(!check(l)) puts("-1");
	else printf("%d\n",l);
}

​ 另一道例题 ,主要是依靠单调队列原则进行;

求一段最短的区间,其区间中包含了所有类型的颜色;

​ 根据题目中区间的信息,我们可以隐约地想到单调队列,但是并没有单调性.

​ 考虑队首弹出条件,当一个颜色在一个区间多次出现时,有贡献的只会是一个,那么当队首颜色重复时,其贡献已经被之后的颜色代替,也就没用了,那么既可以弹出;

​ 队尾弹出就不再需要了,我们直接更新满足条件的区间即可;

​ 那么思路即,对每种颜色计数,当队首元素颜色个数>=2时,将其弹出;

#include<cstdio>
#include<algorithm>
using namespace std;
int n,m,cnt,num;
int q[1000007],ans=2147483647,vis[1000007];

struct node{
	int id,sp;	
}a[1000007];

bool operator <(node a,node b){
	return a.sp<b.sp;
}

int main(){
	scanf("%d%d",&n,&m);
	for(int k=1,to;k<=m;k++){
		scanf("%d",&to);
		for(int i=1;i<=to;i++)
			scanf("%d",&a[++cnt].sp),a[cnt].id=k;
	}
	sort(a+1,a+n+1);
	int l=1,r=0;
	for(int i=1;i<=n;i++){
		if(vis[a[i].id]) vis[a[i].id]++;
		if(!vis[a[i].id]) vis[a[i].id]++,num++;
		while(vis[a[q[l]].id]>1) vis[a[q[l]].id]--,l++;//超过一个,弹出
		q[++r]=i;
		if(num==m) ans=min(ans,a[q[r]].sp-a[q[l]].sp);
	}
	printf("%d",ans);
	return 0;
}

进阶

​ 二维单调队列,在一个固定矩形中,求其中最大值和最小值.

例1

[HAOI2007]理想的正方形

在一个 \(a*b\) 的矩形中,求一个最大值与最小值差值最小的 \(n*n\) 的正方形,输出其差值;

​ 首先求出横向一维区间最大值,之后在原有横向区间最大值中求纵向一维区间最大值,这里由于用到了横向一维的值,使其变成了二维,那么思路就很显然了,注意区间大小和边界限制;

#include<bits/stdc++.h>
using namespace std;
int a,b,n,maps[1007][1007],work1[1007][1007],qmax[1007],qmin[1007],ans1[1007][1007];
int q[100007],work2[1007][1007],ans2[1007][1007],ans=2147483647;

int main(){
	scanf("%d%d%d",&a,&b,&n);
	for(int i=1;i<=a;i++){
		for(int j=1;j<=b;j++){
			scanf("%d",&maps[i][j]);
		}
	}
	for(int x=1;x<=a;x++){
		int l=1,r=0,k=n;
		memset(qmax,0,sizeof(qmax));
		memset(qmin,0,sizeof qmin);
		for(int i=1;i<=b;i++){
			while(l<=r&&i-k>=qmax[l]) l++;
			while(l<=r&&maps[x][qmax[r]]<=maps[x][i]) r--;
			qmax[++r]=i;
			if(i>=n) work1[x][i-k+1]=maps[x][qmax[l]];
		}
		l=1,r=0,k=n;
		for(int i=1;i<=b;i++){
			while(l<=r&&i-k>=qmin[l]) l++;
			while(l<=r&&maps[x][qmin[r]]>=maps[x][i]) r--;
			qmin[++r]=i;
			if(i>=n) work2[x][i-k+1]=maps[x][qmin[l]];
		}
	}//横向区间最大最小值
	for(int x=1;x<=b;x++){
		int l=1,r=0,k=n;
		memset(qmax,0,sizeof(qmax));
		memset(qmin,0,sizeof qmin);
		for(int i=1;i<=a;i++){
			while(l<=r&&i-k>=qmax[l]) l++;
			while(l<=r&&work1[qmax[r]][x]<=work1[i][x]) r--;
			qmax[++r]=i;
			if(i>=n) ans1[i-k+1][x]=work1[qmax[l]][x];
		}
		l=1,r=0,k=n;
		for(int i=1;i<=a;i++){
			while(l<=r&&i-k>=qmin[l]) l++;
			while(l<=r&&work2[qmin[r]][x]>=work2[i][x]) r--;
			qmin[++r]=i;
			if(i>=n) ans2[i-k+1][x]=work2[qmin[l]][x];
		}
	}//纵向区间最大最小值
	for(int i=1;i<=a-n+1;i++){
		for(int j=1;j<=b-n+1;j++)
			ans=min(ans,ans1[i][j]-ans2[i][j]);
	}
	printf("%d",ans);
}

例2

[HAOI2007]修筑绿化带 ,

如果把公园看成一个 \(M * N\) 的矩形,那么花坛可以看成一个 \(C*D\) 的矩形,绿化带和花坛一起可以看成一个 \(A*B\) 的矩形。

如果将花园中的每一块土地的“肥沃度”定义为该块土地上每一个小块肥沃度之和,那么,

绿化带的肥沃度= \(A*B\) 块的肥沃度 \(- C*D\)块的肥沃度

为了使得绿化带的生长得旺盛,我们希望绿化带的肥沃度最大。

震惊!某HA省竟在同一年考了两道相同算法的题

​ 这道题看起来是求最大区间值和,但是如果我们先用二维前缀和预处理出 \(C*D\) 矩形的权值和,然后再限制在 \(A*B\) 的矩形中,即上面那道题,有些不同的是,花坛不能触碰边界,注意边界的划分.

#include<bits/stdc++.h>
using namespace std;
int m,n,c,d,a,b,maps[1007][1007],num[1007][1007];
int qmin[1007],mms[1007][1007];
int work2[1007][1007],ans2[1007][1007],ans;

int main(){
	scanf("%d%d%d%d%d%d",&m,&n,&a,&b,&c,&d);
	for(int i=1;i<=m;i++){
		for(int j=1;j<=n;j++){
			scanf("%d",&maps[i][j]);
			num[i][j]=maps[i][j]+num[i-1][j]+num[i][j-1]-num[i-1][j-1];
		}
	}
	for(int i=c;i<=m;i++){
		for(int j=d;j<=n;j++){
			maps[i-c+1][j-d+1]=num[i][j]-num[i-c][j]-num[i][j-d]+num[i-c][j-d];
			if(i>=a&&j>=b) mms[i-a+1][j-b+1]=num[i][j]-num[i-a][j]-num[i][j-b]+num[i-a][j-b];
		}
	}
	for(int x=1;x<=m-1;x++){
		int l=1,r=0,k=b-d-1;
		memset(qmin,0,sizeof qmin);
		for(int i=2;i<=n;i++){
			while(l<=r&&i-k>=qmin[l]) l++;
			while(l<=r&&maps[x][qmin[r]]>=maps[x][i]) r--;
			qmin[++r]=i;
			if(i-k+1>=0) work2[x][i-k+1]=maps[x][qmin[l]];
		}
	}
	for(int x=1;x<=n-1;x++){
		int l=1,r=0,k=a-c-1;
		memset(qmin,0,sizeof qmin);
		for(int i=1;i<=m;i++){
			while(l<=r&&i-k>=qmin[l]) l++;
			while(l<=r&&work2[qmin[r]][x]>=work2[i][x]) r--;
			qmin[++r]=i;
			if(i-k+1>0) ans2[i-k+1][x]=work2[qmin[l]][x];
		}
	}
	for(int i=1;i<=m-a+1;i++){
		for(int j=1;j<=n-b+1;j++){
			ans=max(ans,mms[i][j]-ans2[i+1][j+1]);
		}
	}
	printf("%d",ans);
}

DP与单调队列

[SCOI2010]股票交易 ;

通过一段时间的观察,\(\text{lxhgww}\) 预测到了未来 \(T\) 天内某只股票的走势,第 \(i\) 天的股票买入价为每股 \(AP_i\) ,第 \(i\) 天的股票卖出价为每股 \(BP_i\)(数据保证对于每个 \(i\),都有 \(AP_i > BP_i\) ),但是每天不能无限制地交易,于是股票交易所规定第 \(i\) 天的一次买入至多只能购买 \(AS_i\) 股,一次卖出至多只能卖出 \(BS_i\) 股。

另外,股票交易所还制定了两个规定。为了避免大家疯狂交易,股票交易所规定在两次交易(某一天的买入或者卖出均算是一次交易)之间,至少要间隔 \(W\) 天,也就是说如果在第 \(i\) 天发生了交易,那么从第 \(i+1\) 天到第 \(i+W\) 天,均不能发生交易。同时,为了避免垄断,股票交易所还规定在任何时间,一个人的手里的股票数不能超过 \(\text{MaxP}\)

在第 \(1\) 天之前,\(\text{lxhgww}\) 手里有一大笔钱(可以认为钱的数目无限),但是没有任何股票,当然,\(T\) 天以后,\(\text{lxhgww}\) 想要赚到最多的钱,聪明的程序员们,你们能帮助他吗?

​ 首先推出DP式子,设 \(f[i][j]\) 为第 \(i\) 天手持 \(j\) 个股票是的最大收入,那么分类转移;

​ CASE1: 没有买股票, \(f[i][j]=f[i-1][j]\)

​ CASE2: 买了股票,但是有 \(W\) 天的限制,所以转移应为 \(f[i][j]=max(f[i-w-1][j-k] - k*AP_i ,f[i][j])\) ;

​ CASE3: 卖了股票, \(f[i][j]=max(f[i-w-1][j+k]+k*BP_i , f[i][j])\) ;

​ 那么 \(k\) 的枚举我们可以用单调队列优化掉,时间复杂度 \(O(tm)\) .

#include<bits/stdc++.h>
using namespace std;
int t,m,w,f[2007][2007],q[2007],ans=-INT_MAX;
int max(int a,int b){return a > b ? a : b;}

int main(){
	scanf("%d%d%d",&t,&m,&w);
	memset(f,128,sizeof(f));//初始化最小值
	for(int i=1,ap,bp,as,bs;i<=t;i++){
		scanf("%d%d%d%d",&ap,&bp,&as,&bs);
		memset(q,0,sizeof q);
		for(int j=0;j<=as;j++) f[i][j]=-ap*j;
		for(int j=0;j<=m;j++) f[i][j]=max(f[i][j],f[i-1][j]);
		if(i<=w) continue;
		int l=1,r=0;
		for(int j=0;j<=m;j++){
			while(l<=r&&q[l]<j-as) l++;
			while(l<=r&&f[i-w-1][q[r]]+q[r]*ap<=f[i-w-1][j]+j*ap) r--;
			q[++r]=j;
			if(l<=r) f[i][j]=max(f[i][j],f[i-w-1][q[l]]+q[l]*ap-j*ap);
		}//买的时候从比自己股票少的转移,正序枚举 
		memset(q,0,sizeof q);
		l=1,r=0;
		for(int j=m;j>=0;j--){
			while(l<=r&&q[l]>j+bs) l++;
			while(l<=r&&f[i-w-1][q[r]]+q[r]*bp<=f[i-w-1][j]+j*bp) r--;
			q[++r]=j;
			if(l<=r) f[i][j]=max(f[i][j],f[i-1-w][q[l]]+q[l]*bp-j*bp);
		}//卖的时候从比自己股票多的转移,倒序枚举 
	}
	printf("%d",f[t][0]);
	return 0;
} 

总结

​ 单调队列可以有效处理区间最大最小值信息,这在DP方程转移过程中有重要意义,但在应用时应注意其应用范围,而不是盲目的套;

​ 应用时要注意细节问题,区间范围应卡好,出队时注意判断范围;

练习题

P2254 [NOI2005]瑰丽华尔兹 ;

TO BE CONTINUE
posted @ 2019-11-12 15:44  Mr_Leceue  阅读(166)  评论(0编辑  收藏  举报