算法学习笔记(15)——单调队列
单调队列
单调队列算法很多时候用手写的模拟队列比较方便,因为很多时候需要双口出队的队列,主要是在队尾也有删除元素的需求。而模拟队列都是游标移动来限定队列中的所有元素,所以用模拟队列很自然的可以做到双端队列的操作。
单调队列一个经典应用就是求滑动窗口里的最大(或者最小)值。
算法步骤:
- 把该滑出的滑出(由于每次移动一步,最多也就滑出一个,所以一个
if
就行了,不需要用while
) - 在入队前,看看队尾元素和新元素是不是破坏的单调性(也可以从“又老又差”这个角度去思考),不断从队尾删除(这里要用
while
了,因为可能删多个) - 新元素入队
- 如果窗口已经达到 \(k\) 这么大了才需要输出结果(滑动窗口最值,即单调队列队头)
这里解释一下第四步,是因为滑动窗口的大小是固定的 \(k\) ,所以第一个窗口的形成其实不是一开始就能形成的,因为一开始只加了一个元素进来,要加够 \(k\) 个元素才能形成窗口:
#include <iostream>
using namespace std;
const int N = 1e6 + 10;
int n, k;
int q[N]; // 队列:存储数组元素对应的下标
int a[N]; // 数组
int main()
{
cin >> n >> k;
for (int i = 0; i < n; i ++ ) cin >> a[i];
// 初始化队列为空,hh队头指针,tt队尾指针
int hh = 0, tt = -1;
// 遍历数组
for (int i = 0; i < n; i ++ ) {
// 当队列不空,且队头元素(数组下标)小于当前窗口的左边界时
if (hh <= tt && q[hh] < i - k + 1) hh ++;
// 当队列不为空,且队尾元素大于当前遍历到的元素时,删除队尾元素,保持队列内元素的单调性
while (hh <= tt && a[q[tt]] >= a[i]) tt --;
// 插入当前所指的元素
q[++ tt] = i;
// 当遍历至窗口内的元素个数达到k时,输出每次遍历到的窗口内的最小元素
if (i >= k - 1) cout << a[q[hh]] << ' ';
}
puts("");
// 与上述过程同理
hh = 0, tt = -1;
for (int i = 0; i < n; i ++ ) {
if (hh <= tt && q[hh] < i - k + 1) hh ++;
// 只需修改>=为<=,保证队列单调递减
while (hh <= tt && a[q[tt]] <= a[i]) tt --;
q[++ tt] = i;
if (i >= k - 1) cout << a[q[hh]] << ' ';
}
puts("");
return 0;
}
每个元素至多入队一次、出队一次,所以时间复杂度是\(O(N)\)。它的思想也是在决策集合(队列)中及时排除一定不是最优解的选择。单调队列也是优化动态规划的一个重要手段。