单调队列基础模板
一天我在luogu上看到这么一道题:
仔细思考之后...
恩,这不ST表吗!
然后一看数据范围:
(这图片是不是特别熟悉...)
听说有同学改了改数组大小,把ST板子往上一粘...
并且一看这两个点一定是无可挽回的,无论怎么优化,
所以说即使这个算法拿到了这个题的大部分分,这个算法还是不适合的
这说明什么?
luogu数据水
说明这题并不能用ST表过
那么有没有一种东西,它能够处理特定区间内的最值问题,还能跑的快(最好是O(n))呢?
下面引出今天的主角:单调队列!
在刚开始听的时候,觉得ta可能就是一个能够选取特定区间然后进行sort,最后还能处理出元素原来的入队顺序的STL?
其实并不是
首先ta并不是STL,一开始看到大佬打上\(deque\)时我还以为这就是单调队列,然而这只是双端队列
其次,单调队列指的单调并不是"单调函数"中的那个"单调",那个"单调"指的是"单调性",就是在指定的区间不上升或不下降
这里的单调指的是满足特定规则,
也就是说,这个队列中的元素既可以不上升,不下降,上升或下降,也可以在指定区间做像这样的运动:
只要ta符合一定的法则
具体的代码实现大纲就是在入队时写个函数,将不符合规则的元素做不影响结果的处理
而且这个东西还可以用于优化DP
以为大多DP需要枚举前面状态的若干结果,找最优解,如果用了单调队列,可以避免枚举步骤,直接状态转移
下面分析这道题
要求特定长度区间的最小值,那么这个特定长度可以看成是队列长度
然后分别将这个队列用来维护其元素的不下降,每次查询取队首就好辣
那么这个入队函数怎么写呢?
来模拟一遍:
每次遇到一个元素,将其入队时的操作考虑这样两种准则:
一个是保留最新的,就是当前入队元素,就是无论如何将当前新元素入队
另一个是保留对本题贡献最大的元素,要是一个元素老当益壮,就是即使老也仍是最大,就先不考虑退休问题,先保留
操作完是这样一种状态:
队首是最优元素,那么询问可以直接询问队首,当然需要先判断一步,就是这个队首元素是否已经要退休,是的话则出队,如果出现这种情况时并不会出现队空的情况,因为毕竟刚入一个"新秀"
除队首外的元素是一个不下降子序列,也就是后面的元素虽然大,但是等老元素退役时还是有较小的元素在队首
那么以后再询问的时候就可以将年轻的,较优的结果输出
总结一下,主要思想就是"退役思想",对于每个入队的元素,观察ta的值,如果ta比前一项优(并且比ta年轻,已保证,毕竟晚入队),那么排掉ta之前所有不如ta优的元素,因为前面的元素不够"优秀",就像某个奥赛团队来了一个新人,这个新人比你小并且比你强,那你就该退役了...
再见了同志们
或者说后面这个元素不够小,那么满足数列不下降这个需求,可以保留在队尾
经过以上步骤既可以把当前元素保留,又可以保证队首为最优,
如果同时要维护最大值和最小值(比如某个叫滑动窗口的题)
要么开两个队列,要么同一个队列两次用,先后处理最大值最小值
代码如下:
#include<cstdio>
#include<queue>
#include<algorithm>
using namespace std;
int n,k;
struct node{
int id,num;
};
int a[2000005];
deque<node> q;
inline void minn(const int &i,const int &v){
while(!q.empty()){
node now=q.back();
if(now.num>v) q.pop_back();
else break;
}q.push_back((node){i,v});
}
int main(){
scanf("%d%d",&n,&k);
for(int i=1;i<=n;i++)
scanf("%d",&a[i]);
printf("0\n");
for(int i=2;i<=n;i++){
minn(i-1,a[i-1]);
node now=q.front();
while(now.id<(i-k)){
q.pop_front();
now=q.front();
}
printf("%d\n",now.num);
}return 0;
}
再次说下,这里的deque是双端队列,front和back作用不言而喻..