算法学习笔记(15)——单调队列

单调队列

单调队列算法很多时候用手写的模拟队列比较方便,因为很多时候需要双口出队的队列,主要是在队尾也有删除元素的需求。而模拟队列都是游标移动来限定队列中的所有元素,所以用模拟队列很自然的可以做到双端队列的操作。

单调队列一个经典应用就是求滑动窗口里的最大(或者最小)值。

算法步骤:

  1. 把该滑出的滑出(由于每次移动一步,最多也就滑出一个,所以一个if就行了,不需要用while
  2. 在入队前,看看队尾元素和新元素是不是破坏的单调性(也可以从“又老又差”这个角度去思考),不断从队尾删除(这里要用while了,因为可能删多个)
  3. 新元素入队
  4. 如果窗口已经达到 \(k\) 这么大了才需要输出结果(滑动窗口最值,即单调队列队头)
    这里解释一下第四步,是因为滑动窗口的大小是固定的 \(k\) ,所以第一个窗口的形成其实不是一开始就能形成的,因为一开始只加了一个元素进来,要加够 \(k\) 个元素才能形成窗口:

题目链接:AcWing 154. 滑动窗口

#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)\)。它的思想也是在决策集合(队列)中及时排除一定不是最优解的选择。单调队列也是优化动态规划的一个重要手段。

posted @ 2022-12-09 22:06  S!no  阅读(44)  评论(0编辑  收藏  举报