【单调队列是什么】
按我的理解,单调队列就是一个可以一端进两端出的队列。只能从队尾进但是可以同时从队首和队尾出。用的不是很多,但是在某些情况下会有特殊之处。
【单调队列的性质】
单调队列满足这样一个性质:队内的元素单调,并且队内前面一个元素的位置一定小于后面一个元素的位置(这里的位置是相对于原序列中的位置)。
很容易证明性质的正确性:如果只从队尾入队的话,一定满足前面一个元素的位置一定小于后面一个元素的位置。这样的性质为二分提供了方便(单调队列是可以进行二分优化的,下文会提到)。而保证队内元素的单调正是我们需要进行维护的操作,维护单调队列使队内元素始终递增或递减(维护最小值时往往递减,维护最大值时往往递增,查找时返回队首元素)。
【单调队列的维护】
单调队列的维护方法是(以递增为例):一个元素入队时,将队内前面的所有比当前元素大的元素全部出队,然后再将当前元素入队。
以递增的单调队列为例:
数列(1,3,-1,5,3,6)进入单调队列
①元素1入队,队列为(1);
②元素3入队前,发现1<3,3入队,队列为(1,3);
③元素-1入队前,发现3>-1,1>-1,3出队,1出队,-1入队,队列为(-1);
④元素5入队前,发现-1<5,5入队,队列为(-1,5);
⑤元素3入队前,发现5>3,5出队,3入队,队列为(-1,3);
⑥元素6入队前,发现6>3,6入队,队列为(-1,3,6);
这样就保证了在每一个状态队列都是单调递增的,每次查找时查找队首元素即为当前范围的最小值。维护最大值的话则相反。
【单调队列解决的简单问题】
poj2823 Sliding Window 单调队列裸题
题目大意:给出一个初始序列,给出一个区间长度,求出每一个此长度的区间的最小值和最大值。
题解
这道题属于单调队列的简单应用。可以用上述维护单调队列的方法来实现最大值和最小值的维护。但是还要注意一点,这道题比普通的单调队列维护时多了一个步骤。因为要求每一个区间的最值,那么在入队之前除了要判断是否满足单调队列的性质之外,还要判断这个元素是否在当前查询的区间里。
什么意思呢?
你可能已经发现,单调队列是可以两头出的队列,但是上面的最简单的单调队列维护中并没有用到从队首出的这一功能。而这个应用中就会用到。
假如说一共有n个元素,区间长度为k,那么就一共有(n-k+1)个区间。如果我们按照元素的序号i循环的话,那么区间首的序号就可以表示为(i-k+1)。所以,当序号为i的元素入队的时候,除了要判断大小以外,还要判断队内元素的序号是否小于(i-k+1),如果是,则将该元素出队。
这一个出队的操作是从队列首进行的,想一想为什么?这就用到了单调队列的另一个性质:队内前面一个元素的位置一定小于后面一个元素的位置。所以只可能是队首的元素小于区间首而出队,从队首进行操作就可以了。
可以发现,单调队列的性质和用法一层层紧密相连,用心体会才能发现算法的精妙。
【代码实现】
#include<iostream> #include<cstring> #include<cstdio> using namespace std; int a[1000005]; int queue[1000005]; int n,k,head,tail; inline void push1(int x,int i){ if (!head&&!tail||head>=tail){ queue[++tail]=x; return; } while (queue[head+1]<i&&head<tail) head++; while ((a[queue[tail]]>a[x])&&head<tail) tail--; queue[++tail]=x; } inline void push2(int x,int i){ if (!head&&!tail||head>=tail){ queue[++tail]=x; return; } while (queue[head+1]<i&&head<tail) head++; while ((a[queue[tail]]<a[x])&&head<tail) tail--; queue[++tail]=x; } int main(){ scanf("%d%d",&n,&k); for (int i=1;i<=n;++i) scanf("%d",&a[i]); for (int i=1;i<=k;++i) push1(i,1); printf("%d ",a[queue[head+1]]); for (int i=k+1;i<=n;++i){ push1(i,i-k+1); printf("%d",a[queue[head+1]]); if (i==n) printf("\n"); else printf(" "); } head=tail=0; memset(queue,0,sizeof(queue)); for (int i=1;i<=k;++i) push2(i,1); printf("%d ",a[queue[head+1]]); for (int i=k+1;i<=n;++i){ push2(i,i-k+1); printf("%d",a[queue[head+1]]); if (i==n)printf("\n"); else printf(" "); } }
【单调队列的二分优化】
明白了基本原理了之后,二分其实是很简单的。我们知道,可以进行二分的重要要求就是单调,而单调队列无论是数值还是序号都是单调的。这就为二分优化提供了方便。
二分主要是体现在队首和队尾的出队上。只要找到了最后一个或者最先一个满足条件的点,那么它前面的元素或者后面的元素都可以出队,也就把时间缩短到了log级别。