单调队列
前言
单调队列并不是太难的东西,不应其应用到的题目困难而觉得单调队列困难.
我第一次遇见单调队列时是在学图论时,遇到了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
在一个 \(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方程转移过程中有重要意义,但在应用时应注意其应用范围,而不是盲目的套;
应用时要注意细节问题,区间范围应卡好,出队时注意判断范围;