【算法】数据结构_单调队列

目录

一、什么是单调队列?

二、单调队列解决的问题

三、例题解决

[1. P1886 【模板】单调队列 / 滑动窗口 - 洛谷](#1. P1886 【模板】单调队列 / 滑动窗口 - 洛谷)

[2. P2251 质量检测 - 洛谷](#2. P2251 质量检测 - 洛谷)

四、总结


一、什么是单调队列?

单调队列,顾名思义,就是存储的元素要么单调递增要么单调递减的队列。其实就是我们要维护一个单调递增要么单调递减的队列来解决问题。

注意:这里的队列不是普通的队列,他是一个双端队列 (链接:deque - C++ Reference)。


二、单调队列解决的问题

通过单调队列,我们解决的问题是在滑动窗口中寻找最大值或最小值的问题。除此之外,它还可以用来优化动态规划的问题(以后说)。


三、例题解决

下面我们就来通过两个例题来理解如何使用单调队列的。

1. P1886 【模板】单调队列 / 滑动窗口 - 洛谷

题目链接:P1886 【模板】单调队列 / 滑动窗口 - 洛谷

问题内容:

题目分析:这道题就是让我们在一个数组中,从前往后遍历,每次遍历都在区间 i, i + k - 1 区间中找到此时这段长度为k区间中的最大值和最小值,直到 i + k - 1 到达数组末尾。

解决思路:

这道题就是一个经典的关于双端队列 的模板题目,这里的队列中存储的是元素的下标(这里设置下标从1开始)。

如果我们将队列中存储的值设置为就是元素的值(而不是下标),则我们可以以示例:数组为 3, 1, 15, 10, 7, 2, 5, 14, 14 , 窗口大小 k = 3 ,那么我们可以来模拟一下(这里设置下标从1开始),初始时如图所示:其中 ret i 表示以i位置为结尾的窗口中的最大值的值。

假设我们要求滑动窗口中的最大值,模拟过程如下所示:我们需要从左向右遍历每一个元素,

  1. 遇到第1个元素 3 时,此时还没有形成长度为 k 的窗口,就将 3 入队,得:
  2. 遇到第 2 个元素1 时,此时仍没有形成长度为 k 的窗口,但是需要和前一个队尾元素比较,此时队尾元素大于当前元素,则需要就将 1 入队,因为1可能是后面窗口中的最大值(比如如果从1开始,此时窗口中是1, -1, -2, 则此时最大值就是 1),得:可以发现此时队列就呈现出来 递减的趋势了。
  3. 遇到第 3 个元素15 时,此时就形成了一个窗口。比较它和队尾元素的大小,此时队尾元素小于15,所以可以将队尾元素出队(这里可以出队的原因是因为15比1大且靠后,所以在包含15往后的窗口中的最大值一定是15而不会是1,即队尾元素必定不是之后某个窗口中的最大值),同理,1 从队尾出队后,此时队尾为3,也小于15,所以也要出队,此时队列为空,就需要将 15 入队,所以此时这里的窗口中最大值就是15,即完成上述操作后的队头。得:
  4. 遇到第4个元素 10 时,此时将窗口右移,窗口中的数就是 1, 15, 10 。队尾元素15大于10,此时就需要将10入队(因为10不是当前这个窗口的最大值,但也可能是后面摸个窗口中最大值),所以此时窗口中的最大值是 15,即队头元素的值,最后更新结果 ret 4 = 15。得:
  5. 遇到第 5 个元素 7 时,此时将窗口右移,窗口中的数就是 15, 10, 7 。队尾元素10大于7,此时就需要将7入队(理由同上),所以此时窗口中的最大值是 15,即队头元素的值,最后更新结果 ret 5 = 15。得:
  6. 遇到第 6 个元素 2 时,此时将窗口右移,窗口中的数就是 10, 7, 2 。队尾元素7大于2,此时就需要将2入队(理由同上),此时窗口中的最大值是应该是 10,但是此时队头元素还是15,因为此时15已经不在窗口中了,所以在更新结果前,我们还需要判断一下当前队列中的元素是否都在当前窗口中(判断方法就是在队列中存下标,判断队头和队尾之前的下标范围即可) ,最后更新结果 ret 6 = 10。得:
  7. 遇到第 7 个元素 5 时,此时将窗口右移,窗口中的数就是 7, 2, 5 。队尾元素2小于5,此时需要将队尾元素出队(因为当前队尾元素小于5,那么在之后包含5的所以窗口中的最大值一定会比5大,此时小于5的队尾元素就没有用了)。当前队列中的数据为: 10(队头), 7(队尾) ,再判断队尾元素为7大于5,此时就将5入队。入队后,此时需要判断一下当前队列中的元素是否都在窗口中,此时10已经不在窗口中了,所以需要在队头出队,此时队列中数据就是: 7(队头), 5(队尾) ,所以当前窗口中的最大值就是队头元素 7 ,即 ret 7 = 7。得:
  8. 遇到第 8 个元素 14 时,此时将窗口右移,窗口中的数就是 2, 5, 14 。此时队尾元素 5 小于 14,需要队尾出队。出队后此时队尾为 7 也小于 14,也出队,此时队列为空,将 14 入队,此时队头为 14 ,14 也满足在窗口内,所以直接更新结果 ret i = 14。得:
  9. 对到第9个元素 14 时,此时将窗口右移,窗口中的数就是 5, 14, 14 。此时队尾元素等于 14 。为了保证 14 这个值在后续窗口中判断窗口合法,所以我们需要将 队尾这个相等的 14 扔掉(即出队)。此时队列中队头为14(这是新加入的14,下标是9)。同时此时队列中元素都在窗口中,所以更新结果 ret 9 = 14。得:

通过上面这个找最大值的模拟过程,我们就可以总结出通过双端队列找最大值的思路:

  1. 设双端队列为q(注意:其中存的是下标),数组为 arr,窗口大小为 k。从左向右开始遍历所有大小为k的窗口。
  2. 如果队列为空,则将 arr i 的下标入队。
  3. 如果队列非空,则需要判断:若队尾 arr q.back() 小于等于 arr i ,则说明队尾元素不是后面某个窗口的最大值了,需要从队尾出队(即 q.pop_back() );若队尾 arr q.back() 大于 arr i ,需要将arr i 的下标入队(因为当前 元素 可能是包含自己的之后窗口中的最大值)。
  4. 然后判断窗口合法(即当前队列中的元素是否都是当前窗口中的元素),则以当前遍历到的元素为结尾的窗口中最大值就是当前队列的队头元素。
  5. 重复2,3,4步骤即可。

所以找最大值时,双端队列就是一个呈现递减的队列。

同理,对于寻找窗口的最小值的思路为:

  1. 如果队列为空,则将 arr i 的下标入队。
  2. 如果队列非空,则需要判断:若队尾 arr q.back() 大于等于 arr i ,则说明当前元素更小,队尾元素就再不是后面某个窗口的最小值了,需要从队尾出队(即 q.pop_back() );若队尾 arr q.back() 小于 arr i ,需要将arr i 的下标入队(因为当前 元素 可能是包含自己的之后窗口中的最小值)。
  3. 然后判断窗口合法(即当前队列中的元素是否都是当前窗口中的元素),则以当前遍历到的元素为结尾的窗口中最大值就是当前队列的队头元素。

所以找最小值时,双端队列就是一个呈现递增的队列。

在这道题中,我们不需要将它们的结构都保存起来,只需要找到后直接输出即可。最后我们实现一下这道题的代码:

cpp 复制代码
#include <iostream>
#include <deque>
using namespace std;

const int N = 1e6 + 1;

int n, k;
int arr[N];

int main()
{
    cin >> n >> k;
    for(int i = 1; i <= n; i++) cin >> arr[i];

    deque<int> q; // 双端队列 - 存下标

    // 找最小值
    for(int i = 1; i <= n; i++)
    {
        while(q.size() && arr[q.back()] >= arr[i]) q.pop_back(); // 当前比队尾小,则队尾一定不是当前窗口的最小,队尾就没用了

        // q为空
        // arr[i]比队尾大-->当前可能是后面某个窗口的最小值
        q.push_back(i);

        // 判断队列中的元素是否都在窗口中 -- 窗口合法检查
        if(q.back() - q.front() + 1 > k) q.pop_front(); // 队头出队

        // 输出合法窗口的结果
        if(i >= k) cout << arr[q.front()] << ' ';
    }
    cout << endl;

    q.clear(); // 清空队列
    
    // 找最大值
    for(int i = 1; i <= n; i++)
    {
        while(q.size() && arr[q.back()] <= arr[i]) q.pop_back(); // 当前比队尾大,则队尾一定不是当前窗口的最大,队尾就没用了

        q.push_back(i);

        if(q.back() - q.front() + 1 > k) q.pop_front(); 

        if(i >= k) cout << arr[q.front()] << ' ';
    }
    return 0;
}

2. P2251 质量检测 - 洛谷

题目链接:P2251 质量检测 - 洛谷

问题内容:

解决方法:这道题就可以转化为在一个分值数组 a 中,寻找以 m 长度大小的一段区间中分值的最小值,也就是以 i 为结尾的满足窗口大小为 m 的区间中数值的最小值。

所以这就是一个单调队列寻找最小值的应用题,直接使用单调队列模拟即可。

实现代码为:

cpp 复制代码
#include <iostream>
#include <deque>
using namespace std;

const int N = 1e5 + 10;
int n, m;
int a[N];

int main()
{
    cin >> n >> m;
    for(int i = 1; i <= n; i++) cin >> a[i];

    deque<int> q; // 递增 - 存下标

    // 找最小值
    for(int i = 1; i <= n; i++)
    {
        while(q.size() && a[q.back()] >= a[i]) q.pop_back(); // 递增,队尾大于a[i]就出队

        q.push_back(i);

        // 判断窗口合法
        if(q.back() - q.front() + 1 > m) q.pop_front();

        // 满足窗口大小就输出
        if(i >= m) cout << a[q.front()] << endl;
    }
    
    return 0;
}

四、总结

通过上面介绍内容比较多,这里总结了单调栈的使用即简单的记忆方法。

总结:

  1. 寻找滑动窗口最大值,递减的单调队列;
  2. 寻找滑动窗口最小值,递增的单调队列

关于代码,最关键的就是什么是在队尾出队的一段代码。我的建议的方法就是:

通过判断求的是最大还是最小来判断是递增还是递减?从而确认什么是否该写>=,什么时候写 <= 。即:

  • 如果是求最大值,则是递减队列,要保证递减,则只要遇到 ai 大于等于 队尾就出队,即:

    cpp 复制代码
    while(q.size() && a[q.back()] <= a[i]) q.pop_back();
  • 如果是求最小值,则是递增队列,要保证递增,则只要遇到 ai 小于等于 队尾就出队,即:

    cpp 复制代码
    while(q.size() && a[q.back()] >= a[i]) q.pop_back();

对于我自己理解的是(个人观点):

  • 大于和小于都试一下。
  • 求最大值,如果 ari 大于 队尾,则说明队尾没用了,就要出队(因为 ari 更大,说明队尾一定不是当前以及后面窗口的最大,所以就没用了)。
  • 求最小值,如果 ari 小于 队尾,则说明队尾没用了,就要出队(因为 ari 更小,说明队尾一定不是当前以及后面窗口的最小,所以就没用了)。

感谢各位观看!希望大家多多支持!

相关推荐
小四季豆1 小时前
《数据结构与算法》-顺序表:算法落地的第一个线性结构
c语言·数据结构·算法
8Qi81 小时前
LeetCode 96:不同的二叉搜索树(Unique Binary Search Trees)—— 题解 ✅
算法·leetcode·职场和发展·动态规划
189228048611 小时前
NV041固态MT29F16T08GSLCEM9-QBES:C
人工智能·算法·microsoft·缓存·性能优化
罗超驿2 小时前
15.LeetCode 30. 串联所有单词的子串(Java):滑动窗口+哈希表详解
算法·leetcode
Marianne Qiqi2 小时前
非hot100的力扣算法题
数据结构·算法·leetcode
CC数学建模2 小时前
2026第八届中青杯全国大学生数学建模竞赛C题:情绪维度耦合约束的脑电信号情绪识别 (1)完整思路、代码、模型、文章,全网首发高质量分享!
python·算法·数学建模
Dillon Dong2 小时前
【风电控制】双馈风机网侧高低穿控制策略——从VrtCal信号处理到状态机逻辑的完整解析
算法·变流器·风电控制·dfig
下午写HelloWorld2 小时前
同态加密(Homomorphic Encryption, HE)
人工智能·算法·密码学·同态加密
CC数学建模2 小时前
2026第八届中青杯全国大学生数学建模竞赛B题:AI生成内容的质量评估与参数优化完整思路、代码、模型、文章,全网首发高质量分享!
python·算法·数学建模