【数据结构】维护队列最值——单调队列(优先队列)
单调队列
目录
简述
-
功能:维护队列内元素的最值
-
操作 $&$ 复杂度:询问当前队列内元素的最值 $& O(1)$,入队/出队时维护队列内元素的最值 $& O(1)$
-
条件:元素之间存在全序关系(如大小关系),询问的是这个关系上的最值。询问的集合内元素的变化有队列的特点(先进先出)。
- 可用于:
- 多次询问队列内元素的最值
- 滑动窗口内的最值
- ……
思路
问题
一个集合 $S$ ,有三种操作
- 输入一个元素 $x$ ,将 $x$ 置入集合 $S$。
- 把 $S$ 内最早进入的元素删除
- 询问当前 $S$ 内最大的元素
解决
由于对 $S$ 的修改操作符合元素 “先进先出” 的特点,可以使用队列来实现 $S$ 。现在操作 1 和 2 已经解决,接下来要解决操作 3 的实现方法。
朴素方法
每次询问当前 $S$ 内最大的元素时,遍历一遍 $S$ 的所有元素获得最大值。这种方式的每次询问都需要遍历一次 $S$ ,在可能有大量的询问时,速度无法令人满意。
分析
*本文的队列称插入侧为队尾,称弹出侧为队头,示意图上左侧为队头,右侧为队尾。
我们注意到,队列 $S$ 的元素可能被出队(删除)。如果我们记录一个 $max\_value$ 作为当前队列的最大值,在入队一个新元素 $x$ 时将 $x$ 与 $max\_value$ 进行比较,就可以在只有入队操作的情况下 $O(1)$ 地查询到当前 $S$ 内最大的元素。但当出现出队操作时,如果原先记录的最大值被从 $S$ 中删除了,就需要重新遍历 $S$ 以寻找新的最大值。
如果我们能够找到一种方式,在原最大值被删除时可以快速找到新的最大值,就能节约大量时间。
我们可以把队列表示为数轴上的区间的形式:元素按入队顺序从左到右排列,入队就是区间右端+1,出队就是区间左端+1。
观察下图,在 $[0,9]$ 的区间内的最大值是 $8$ 。在 $[1,9]$ 的区间内的最大值也是$8$。
在区间左端不断+1,即元素不断出队的过程中,区间 $[2,9] ~ [5,9]$ 的最大值都是 $7$ ,区间 $[6,9] ~ [8,9]$ 的最大值都是 $5$ 。
显然,区间 $[9,9]$ 的最大值就是 $2$ 。
把这些最大值标记出来,我们发现,从 $[0,9] ~ [9,9]$ 最大值的变化如下所示:
$[0,13] ~ [13,13]$ 最大值的变化如下所示:
蓝色框内的元素,右边都存在比它大的元素。可以说明,如果当前队列 $S$ 中两个元素的下标为 $i,j$,且 $i<j,S[i]<S[j]$ (即 $S[i]$ 的右边存在比它大的元素),$S[i]$ 不可能成为未来(任意次入队出队之后)某一时刻的队内最大元素:
如果比 $S[i]$ 下标更大的元素是当前的最大元素,那显然直到 $S[i]$ 出队,$S[i]$ 都不可能成为最大元素。
如果当前的最大元素比 $S[i]$ 的下标要小,那在其出队之后,新的最大元素将会在其右侧,最后会变为第一种情况,即最大元素在 $S[i]$ 右侧。
如果把这些不可能成为最大元素的从队列中去掉,我们就会得到一个单调非增的序列。这个序列具有这样的性质:
- 首元素是当前的最大元素。
- 第二个元素是下一个最大元素。
在这个序列里,取队列内最大值和求下一个最大值这两个操作都只需要常数步就能完成。这就是单调队列,单调队列只保存了部分原队列中的元素,这些元素具有单调性。
实现
(以维护最大值为例)
入队操作:
新元素入队之前,需要先把不满足单调性的元素删除。可以看出,需要删除的元素连续排列在队列尾端,所以可以通过从尾端弹出元素的方式来删除。
} void pop(std::deque<int>& Q, std::queue<int>& S){ // 出队之前判断空队列 if(S.empty())return; // “同步” if(S.front() == Q.front()){ Q.pop_front(); } S.pop_front(); }
因为新元素一定会在队中,所以这个队列在入队操作后一定是非空的。
出队操作:
由于单调队列 $Q$ 没有保存原队列的全部元素,所以在原队列出队时,$Q$ 不一定要出队。
“同步”:需要保证两个 $7$ 在同时出队。
void pop(std::deque<int>& Q, std::queue<int>& S){ // “同步” if(S.front() == Q.front()){ Q.pop_front(); } S.pop_front(); }
查询最大元素:
就是 $Q$ 的队头元素。
int query(std::deque<int>& Q){ return Q.front(); }