由于最近做了几道单调数据结构,有点感慨,来做个小结。
单调数据结构这里主要指单调队列、单调栈两个。
基本特征
在队列/栈的基础上增加了“单调性”这一要求。即保持数据结构中的元素按某种值的大小顺序单调。
由于栈/队列的插入规则不能变,又要让元素能成功插入,所以要保持单调性,它们只能丢弃原有的某些信息 来维护单调性。
单调栈
保持单调性的方法:插入元素时,如果当前顶端和即将插入的元素之间单调性冲突,则将顶端pop,直到栈空或者顶端符合单调性。
例1-模拟
给定
个整数
依次放入“保持从顶到底递减”的单调栈中,要求输出单调栈最后的形态,按照“自顶向下”顺序输出。
例如:将 1 4 2 3 5 7 压入上述的单调栈,
1、4进入,都没有破坏单调性,2要进入,但是2作为栈顶会大于4,所以4要弹出,2进入
这样插入完,自顶向下的最终形态就是 7 5 3 2 1。
理解完单调栈的工作模式,接下来来看看它的应用
例2 - 单调栈的应用
给定 个整数 ,对于每个数,求一个最小的整数 ,使得 。如果不存在输出-1。 。
例2解答
暴力的话对于每个数往右找,复杂度平方。
我们想想重复枚举了什么?对于
枚举过的数,我们在处理
时很可能又把它枚举了一遍,这就浪费了。
我们要利用问题的单调性质。你找到一个数
的话,比
小的那些元素的答案也都可能是这个。
因此我们维护一个单调栈,如对于序列 5 4 2 1 3 2 4
- 首先5入栈,4比5小,4入栈,同理2入栈,1入栈
- 遇到3时,3比栈顶1大,因此 ,求完后将1弹出,发现3比栈顶2大,因此 ,弹出2,发现4比3大,3入栈(由于栈是单调的,4大于3,那么在4下面的栈的元素一定都大于3)
- 2入栈,比3小
- 4入栈,比2大, =1,2出,比3大, ,3出,大等于4, ,比5小,4入栈
- 找不到剩下的数了,栈内的元素都找不到到答案,赋值为-1
由于每个元素都进出栈一次,是线性的复杂度。
体会到优化在哪了吗?感觉就像“带着一堆元素一起往右移动”,遇到一个门槛就会被筛掉一些一样hhh。
单调队列
那单调队列比单调栈多了什么呢?可以这样理解,单调队列是单调栈的“升级”。
我们知道,队列不但允许在尾端插入,也允许在头端删除。而单调队列是一种特殊队列,它支持“尾端插入、删除”,“头端删除”。只考虑尾端的话,它和单调栈一毛一样。但是它多了“头端删除”。
如果当前队列的“头”超出了问题规模,可以将头扔掉。这就是单调队列更高明之处。
例3 - 滑动窗口问题-单调队列应用
有一个长度为
的数列,求所有长度为
的区间中的最大值和最小值,按区间左端点的大小顺序输出。
。
例3解答
暴力复杂度
,和刚刚那个例题一样也有很多重复状态,我们想想是否能优化。
对于每个区间,如果我们能极快地将这个区间保存为有序数列,那么直接输出两端点即可,但是这样每次加入排序都是
的,有没有什么办法能不要“每次都加入排序”呢?
因为只关心最值,我们便想到单调数据结构。比如我们用两个单调栈,一个递增,一个递减。
我们把数列的前
个数塞进这俩单调栈里,然后输出这两个栈的栈底,OK第一个区间搞定,接下来是第二个区间,这个区间只比第一个区间多了右边一个数,少了左边一个数。多了数好说,直接塞进单调栈里更新就完了,少了怎么办?单调栈能删除指定的元素嘛?
那如果少了的元素不删会怎么样?
假设第一个区间最小值刚刚好是1号元素,到第二个区间了,新加的元素仍不比1号元素大,那么1号元素还是在栈顶,你会输出一个不存在在第二个区间的最小值。
那我看到栈底已经不属于范围了,难道我就眼睁睁的看着它被输出,不能把它删掉然后输出下一个栈底吗?
可以是可以,但它已经不叫单调栈了,单调栈只能从栈顶操作,对栈底是不能动的。
但是单调队列可以。单调队列中,“栈底”“栈顶”改名叫“队首”“队尾”了。
对于这题,我们每次维护一个单调队列,新加一个元素,就按照单调栈的规则插到队尾,然后如果队首不在范围内,删除队首,直到遇到范围内的队首。
由于每个元素进出队一次,复杂度也是线性的。
总结
为什么单调栈和单调队列能优化效率呢?
不要只看复杂度分析里的“进出栈/队一次”,
其本质原因就是,它们都 舍弃了一些信息 。
而这些信息,是完全没贡献,或者能被更优的元素完全继承它的信息。
比如例2,我们发现较小的元素是可以跟着较大的元素走的,因为“比较大元素还要大的元素是较小元素答案的边界”。
要维护单调性,就要扔掉一些元素,这些元素恰好是已经处理完了的。
所以遇到“区间”、“最值”这样的东西,单调数据结构有可能会帮上你一把。