单调队列:
顾名思义,就是队列中元素是单调的(单增或者单减)。
在某些问题中能够优化复杂度。
在dp问题中,有一个专题动态规划的单调队列优化,以后会更新(现在还是太菜了不会)。
在你看到类似于滑动定长度区间的类似问题时可以考虑单调队列优化。
就像这道题:P1886 滑动窗口。
一道模板题。那么我们从题目入手讲解。
首先,你看完了题。。(??)
1.暴力
朴素的入门级想法就是双重for循环枚举当前区间里的每一个元素并取min和max。
因为外层for枚举区间头,内层for枚举区间内位置,所以复杂度O(外层区间长度*内层区间长度)也就是O(nk)。这题是1e6的数据啊。。T飞了。。
2.st表
学过较为高级的数据结构的同学可能会想到线段树和st表。
其中,线段树可以O(nlogn)查询,O(nlogn)修改。st表只支持查询,但是可以O(nlogn)预处理,在这个题中,没有修改的操作,所以我们可以使用st表,相对于线段树更优。
但是计算一下,实际复杂度在两千万?。。可能过,但是我没敢试。。。qwq。一般上千万的复杂度都得看脸过了。。
所以还有更稳定的算法:
3.单调队列
复杂度O(n),为线性算法。
主角来啦!
我们定义一个双端队列q,它两端都可以任意缩短增加长度,我们尝试维护它的长度为k。
当这个窗口向右移动一格时,自然的,要进队一个元素,出队一个元素。
我们尝试保证出队的那个元素为当前区间的最大(举例子)值,那么为了维护队列的单调性,我们必须使得队头的元素要比后面的元素都大。
考虑这样一个oi俗语:有些人比你小,他还比你强,那么你就可以退役了(深有同感)!
对于当前要进队的啊a[i],如果你设一个j,保证a[j]在队中切a[j]比a[i]先入队(head<j<=tail),如果a[j]比当前a[i]小(比你强大),它还先进队(比你小),是不是就抹杀掉了a[j]当最大值的机会?
那么a[j]以及那些像a[j]一类的元素(人)都可以被弹出队列(集体退役)了。。
为什么写着写着莫名辛酸。。去世吧
这也是一个重要的基础,你一定要想懂这个道理!
于是他转化成了这样一句代码:
弹出队尾。
那么,每次只要输出队头(班里的rk1)就可以了。
代码如下:
#include<cstdio> using namespace std; int read() { char ch=getchar(),last=' '; int ans=0; while(ch>'9'||ch<'0')last=ch,ch=getchar(); while(ch>='0'&&ch<='9')ans=(ans<<3)+(ans<<1)+ch-'0',ch=getchar(); return last=='-'?-ans:ans; } int n,a[1000001],k,q[1000001]; inline int minnn()//求最小值 { int hea=0,tai=1; for(int i=1;i<=n;i++) { while(hea<=tai&&q[hea]+k<=i)hea++; while(hea<=tai&&a[i]<a[q[tai]])tai--;//又小又强 q[++tai]=i; if(i>=k)printf("%d ",a[q[hea]]);//输出rk1 } printf(" "); } inline int maxnn()//求最大值 { int hea=0,tai=1; for(int i=1;i<=n;i++) { while(hea<=tai&&q[hea]+k<=i)hea++; while(hea<=tai&&a[i]>a[q[tai]])tai--;//又小又强 q[++tai]=i; if(i>=k)printf("%d ",a[q[hea]]);//输出rk1 } printf(" "); } int main(){ n=read(),k=read(); for(int i=1;i<=n;i++) { a[i]=read(); } minnn(); maxnn(); return 0; }
一篇辛酸的讲解。
完结。
希望自己不要退役