单调队列及模板

单调队列及模板

1.单调队列的应用场景

    单调队列的应用场景也比较单一,主要围绕着如下问题进行展开:
    求滑动窗口中的最大值/最小值。

2.暴力做法分析

    我们可以用队列来维护一个滑动窗口。在窗口的滑动过程中,实际上就是在往队列中添加/删除元素的过程。(具体来说,当窗口往右移动时,实际上就是在队尾中添加一个元素,在队头删除一个元素的过程。)在算法的执行过程中,队列里面始终保存当前窗口中的元素。
    那么,对于上述问题的暴力做法是什么呢?我们可以每次遍历队列当中的元素,进而找到最大值和最小值即可。这样的话,就找到了滑动窗口中的最大值/最小值。
    因此,对于上述的暴力做法,如果滑动窗口的长度为k,那么遍历一遍,时间复杂度就是O(k)。再加上滑动窗口的滑动过程,每个元素最多都是进队一次,出队一次。因此,滑动窗口本身滑动过程的时间复杂度就是O(n)。由于滑动窗口每滑动一次,就需要将窗口遍历一遍,因此时间复杂度就是O(nk)。

3.单调队列的执行过程

img

    我们以上图的滑动窗口来进行举例。
    我们可以发现,当我们求滑动窗口的最小值时,由于当前窗口中3和-1均在-3的左边(代表在窗口的滑动过程中,3和-1均比-3要更早的出队),且比-3要大(代表只要-3在窗口中,这两个数字永远都不会被当作答案来进行输出)。因此,满足以上两个条件的3和-1就是没有用的元素(与-3构成了逆序对),应当进行删除。
    因此,根据上述例子,我们可以发现:
    只要在队列中存在满足上述条件的情况(ax >= ay 且 x < y),我们就应该把没有用的元素(ax)进行删除。这样的话,当删除之后,队列里面的元素均是满足严格单调递增的,这也是单调队列这个名称的由来。并且,当我们求滑动窗口的最小值时,由于队列当中的元素都是严格单调递增的,因此队头元素即可满足要求。
    如果求得是滑动窗口中的最大值,那么队列当中的元素应满足严格单调递减这样的性质。
    根据上述算法,我们如何知道队列在什么时候出队呢?
    在这道题中,i始终指向队列的尾部,k代表窗口的长度,并且队列中存储的是值对应的下标,而不是值本身。因此,我们只需要判断:当下标不在i-k+1~i这个区间内,那么这个下标所对应的值就不在窗口中,此时就需要出队了。

4.单调栈和单调队列的共性

    对于单调栈和单调队列,我们可以发现:
    1.  都是先通过栈和队列进行暴力求解。
    2.  在暴力求解中寻找单调性,进一步实现优化。
    3.  在进行优化的过程中,我们可以发现栈/队列中的元素均满足严格单调递增或递减。这样的话,如果我们想寻找栈/队列中的最大值/最小值,我们直接找端点即可。如果我们想要寻找栈/队列中的某一个元素,那么我们可以利用二分法寻找即可。

5. 单调队列模板

// 常见模型:找出滑动窗口中的最大值/最小值
int hh = 0, tt = -1;
for (int i = 0; i < n; i ++ )
{
    while (hh <= tt && check_out(q[hh])) hh ++ ;  // 判断队头是否滑出窗口
    while (hh <= tt && check(q[tt], i)) tt -- ;
    q[ ++ tt] = i;
}

6.单调队列例题

https://www.acwing.com/problem/content/156/
#include <iostream>
#include <cstdio>

using namespace std;

int a[1000010],q[1000010];
int hh = 0,tt = -1;

int main(){
    int n,k;
    scanf("%d %d",&n,&k);
    for(int i=0;i<n;i++){
        scanf("%d",&a[i]);
    }
    for(int i=0;i<n;i++){
        //如果队列不为空且队头元素所在下标并不在指定范围内
        //那么将队头划出窗口
        while(hh <= tt && q[hh] < i-k+1){
            hh++;
        }
        //如果队列不为空且ai左侧的窗口中的元素均大于等于ai,那么就应该踢出队列。
        while(hh <= tt && a[q[tt]] >= a[i]){
            tt--;
        }
        //优化过后,把ai(下标)放入到队列中
        q[++tt] = i;
        //满足题目条件,再进行输出
        if(i >= k-1){
            printf("%d ",a[q[hh]]);
        }
    }
    printf("\n");
    //重新进行队列的初始化
    hh = 0,tt = -1;
    for(int i=0;i<n;i++){
        //如果队列不为空且队头元素所在下标并不在指定范围内
        //那么将队头划出窗口
        while(hh <= tt && q[hh] < i-k+1){
            hh++;
        }
        //如果队列不为空且ai左侧的窗口中的元素均小于等于ai,那么就应该踢出队列。
        while(hh <= tt && a[q[tt]] <= a[i]){
            tt--;
        }
        //优化过后,把ai(下标)放入到队列中
        q[++tt] = i;
        //满足题目条件,再进行输出
        if(i >= k-1){
            printf("%d ",a[q[hh]]);
        }
    }
    return 0;
}
    作者:gao79138
    链接:https://www.acwing.com/
    来源:本博客中的截图、代码模板及题目地址均来自于Acwing。其余内容均为作者原创。
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
posted @   夏目^_^  阅读(64)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 25岁的心里话
· 按钮权限的设计及实现
点击右上角即可分享
微信分享提示