单调队列

来源:互联网 发布:著名 淘宝店主 90后 编辑:程序博客网 时间:2024/06/07 00:13

预备知识介绍:

1. 队列

  • 队列 (queue) 是一种特殊的线性表,是符合“先进先出”(first in first out)原则的基本数据结构,因此也常被称作 FIFO 队列或公平队列。
  • 一个队列由队首 (front)、队尾 (rear) 和储存元素的线性表构成。当元素入队时,队尾后移并向线性表中添加新元素;当元素出队时,删除线性表中队首指向的元素,队首后移。一般地,只在队尾入队,在队首出队。对队中元素的讨论都必须建立在队列非空的基础上,否则没有意义。
  • C++ STL 中内置了 queue 容器,实现了普通队列的基本操作,使用时需引用 头文件。

2. 双端队列

  • 双端队列 (deque) 打破了队列的出入队位置限制,允许元素从队首入队或从队尾出队。类似地,在 C++ STL 中内置了 deque 容器,实现了双端队列。

3. 单调队列

  • 单调队列 (monotonic queue) 是指一种所储存的元素具有单调性的特殊双端队列,可根据具体的元素单调性分为单调递增队列、单调递减队列、单调不增队列、单调不减队列四种。前两种要求元素间具有严格的大小关系,不允许相等的元素同时出现在队列中,而后两者允许。
  • 单调队列常常用于维护一个具有特殊性质的区间,求区间极值。一般会对区间长度或其他因素进行限制,因此队列中的元素常常为 (pos,val) 的二元组。在任意时刻,队列中元素的两个值都分别具有单调性,但其性质可能不同。如 pos 一般为单调上升,而 val 则四种情况都有可能。
  • 新元素入队时,为了维护队列的单调性,要不断将队尾元素出队,直到当前元素入队后不会破坏队列单调性为止。取值计算时,为了保证对区间的限制是合法的,要将队首元素不断出队,直到队首元素满足对区间的限制为止。出队和入队的顺序可以调换。详见下面的例子。
  • 在竞赛中单独考察单调队列的情况极少出现,但往往会配合动态规划,对其时间复杂度进行优化。近年来,NOI 系列赛事和部分省选都考察过。

举例分析

下面试举一例,分析单调队列的具体题目中的应用。

给定 nn 个数,分别为 a_1, a_2, \ldots, a_na1,a2,,an。要求输出每相邻 kk 个数中的最大值。换言之,即求 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(ank+1,ank+2,,an).

朴素的算法为枚举所有待求的 max(a_i, a_{i+1}, \ldots, a_{i+k-1})max(ai,ai+1,,ai+k1) 中的 ii,考察连续的 kk 个数,即可得解,总考察次数为 (n-k+1)\times k(nk+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[]={8712542169178} 为例,分析队列的维护和问题的求解过程:

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)) { i3,输出队首 12}
i=5:插入 4,队列为:((3,12),(4,5),(5,4)) { i3,输出队首 12}
i=6:插入 2,队列为:((3,12),(4,5),(5,4),(6,2)){ i3,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)) { i3,输出队首 16}
i=9:插入 17,队列为:((9,17),(7,16),(8,9)) { i3,输出队首 17}
i=10:插入 8,队列为((9,17),(10,8)) { i3,输出队首 17}

在这个过程中,可以发现队列 Q 始终要满足我们上面提出的限制条件。一旦出现非法情况,就要作出相应的出队处理。

因此作为合法的单调递减队列,任意时刻处理完之后队首元素的 val 值总是当前区间内的最大值,即当前的理想答案。

下面来思考上面的问题,为什么不使 val 保持单调不增队列呢?这个问题是我自己在读论文学习的时候想到的。

后来仔细想想,其实也是可以的,但单调递减会更好。不妨这样考虑:有元素 (posi,vali)(posj,valj),且 posi<posjvali=valj

因为我们要求的是最大值,看上去 ij 一样好,其实不然。不难想象,随着区间的向右移动,i 会比 j 更早被淘汰。既然如此,为什么不使i趁早出队呢?

顺便提一句,类似于单调队列,还有一种单调栈,原理和单调队列极其相似,只是通常情况下少了对于区间的限制,如 RQNOJ“诺诺的队列”一题。

为了方便理解,甚至可以把单调栈看成单调队列的一个特殊版本,只允许在队尾出入队。

最后来考虑时间复杂度的问题。

我最初在小学阶段学习单调队列的时候在担心一个问题:对于每一个新元素,维护单调队列都要进行一定的出入队操作,会不会出现最坏情况导致很慢?

其实是不可能的。因为每个元素是被按照一定的顺序依次考虑的,最多入队一次,出队一次,总的时间复杂度其实是 O(n) 级别的。

相比于线段树、胜者树、堆等传统的维护最值数据结构,单调队列具有其独特的优点:编程复杂度、时间复杂度和空间复杂度都比较低。但是其短处也是不可不考虑的,即单调队列是离线的,无法像上述数据结构一样支持动态操作。

当然有时候也可以通过灵活使用单调队列,发挥出很大的作用。总的来说,在具体的题目中,还是应该具体考虑,分析题目的特点,选择最合适的方法。熟练掌握各种常用数据结构的特点及编写对 NOIp 提高组及以上级别的选手还是很有必要的。

参考文献

  • 应用单调队列求解指定区间的最值. 湖南省醴陵市第一中学: 曾妞妞.
  • 论单调队列在高效动态规划算法中的应用. 绵阳中学: 李青林.
原创粉丝点击