下面试举一例,分析单调队列的具体题目中的应用。
给定 n n 个数,分别为 a_1, a_2, \ldots, a_na1,a2,…,an。要求输出每相邻 k k 个数中的最大值。换言之,即求 max(a_1, a_2, \ldots, a_k),\\ max(a_2, a_3, \ldots, a_{k 1}),\\ \ldots\\ max(a_{n-k 1}, a_{n-k 2}, \ldots, a_n).max(a1,a2,…,ak),max(a2,a3,…,ak 1),…max(an−k 1,an−k 2,…,an).。
朴素的算法为枚举所有待求的 max(a_i, a_{i+1}, \ldots, a_{i+k-1}) max(ai,ai+1,…,ai+k−1) 中的 i i ,考察连续的 kk 个数,即可得解,总考察次数为 (n-k+1)\times k (n−k+1)×k ,时间复杂度为 O(n\times k) O(n×k) 级别,当数据范围较大时会 TLE。
我们发现,求 max(a_1, a_2, \ldots, a_k) max(a1,a2,…,ak) 与 max(a_2, a_3, \ldots, a_{k+1}) max(a2,a3,…,ak+1) 中都有 a2,…,ak 这个公共部分。在朴素算法中,它们被重复考察了,显然会造成大量时间浪费,导致效率低下。为什么不能把考察结果保存下来呢?
其实我们的任务是维护一个长度为 k 的区间,要求区间内数字的最大值,这其实就是一个单调队列的基本应用。
我们不妨维护这样一个单调队列 Q,使得任意时刻 Q 都满足:
队尾的 pos 减去队首的 pos 都必须小于 k 队中的 val 保持单调递减(为什么不用单调不增?)以 n=10,k=3,a[]={8,7,12,5,4,2,16,9,17,8} 为例,分析队列的维护和问题的求解过程:
i=1 :插入 8,队列为:((1,8)) { i=1<3 ,不输出队首 8} i=2 :插入 7,队列为:((1,8),(2,7)) { i=2<3 ,不输出队首 8} i=3 :插入 12,队列为:((3,12),(1,8),(2,7)){当前的 val 12 大于 7 和 8,依次删除队尾的 7 和 8,并插入 12 到队列,且 i=3 了,故输出队首的 12} i=4 :插入 5,队列为:((3,12),(4,5)) { i≥3 ,输出队首 12} i=5 :插入 4,队列为:((3,12),(4,5),(5,4)) { i≥3 ,输出队首 12} i=6 :插入 2,队列为:((3,12),(4,5),(5,4),(6,2)){ i≥3 ,2 插入后,因为当前下标 6 减去队首元素的 pos 3 等于 k ,故队首元素出队,输出此时的队首 5,} i=7:插入 16,队列为:((7,16),(4,5),(5,4),(6,2)) {当前的 val 16 大于队尾的 2、4、5,依次删除 2、4、5,并输出队首 16} i=8 :插入 9,队列为:((7,16),(8,9)) { i≥3 ,输出队首 16} i=9 :插入 17,队列为:((9,17),(7,16),(8,9)) { i≥3 ,输出队首 17} i=10 :插入 8,队列为((9,17),(10,8)) { i≥3 ,输出队首 17}
在这个过程中,可以发现队列 Q 始终要满足我们上面提出的限制条件。一旦出现非法情况,就要作出相应的出队处理。
因此作为合法的单调递减队列,任意时刻处理完之后队首元素的 val 值总是当前区间内的最大值,即当前的理想答案。
下面来思考上面的问题,为什么不使 val 保持单调不增队列呢?这个问题是我自己在读论文学习的时候想到的。
后来仔细想想,其实也是可以的,但单调递减会更好。不妨这样考虑:有元素 (posi,vali) 和 (posj,valj) ,且 posi<posj , vali=valj 。
因为我们要求的是最大值,看上去 i 和 j 一样好,其实不然。不难想象,随着区间的向右移动, i 会比 j 更早被淘汰。既然如此,为什么不使i趁早出队呢?
顺便提一句,类似于单调队列,还有一种单调栈,原理和单调队列极其相似,只是通常情况下少了对于区间的限制,如 RQNOJ“诺诺的队列”一题。
为了方便理解,甚至可以把单调栈看成单调队列的一个特殊版本,只允许在队尾出入队。
最后来考虑时间复杂度的问题。
我最初在小学阶段学习单调队列的时候在担心一个问题:对于每一个新元素,维护单调队列都要进行一定的出入队操作,会不会出现最坏情况导致很慢?
其实是不可能的。因为每个元素是被按照一定的顺序依次考虑的,最多入队一次,出队一次,总的时间复杂度其实是 O(n) 级别的。
相比于线段树、胜者树、堆等传统的维护最值数据结构,单调队列具有其独特的优点:编程复杂度、时间复杂度和空间复杂度都比较低。但是其短处也是不可不考虑的,即单调队列是离线的,无法像上述数据结构一样支持动态操作。
当然有时候也可以通过灵活使用单调队列,发挥出很大的作用。总的来说,在具体的题目中,还是应该具体考虑,分析题目的特点,选择最合适的方法。熟练掌握各种常用数据结构的特点及编写对 NOIp 提高组及以上级别的选手还是很有必要的。