单调栈与单调队列

发布时间 2024-01-08 23:58:59作者: lzy20091001

单调栈

单调栈是一种内部元素具有单调性的栈,可以解决求“以某个值为最值的最大区间”等问题。

原理

以下摘自 B3666 求数列所有后缀最大值的位置 - 洛谷 题目描述:

给定一个数列 \(a\),初始为空。有 \(n\) 次操作,每次在 \(a\) 的末尾添加一个正整数 \(x\)

每次操作结束后,请你找到当前 \(a\) 所有的后缀最大值的下标(下标从 1 开始)。一个下标 \(i\) 是当前 \(a\) 的后缀最大值下标当且仅当:对于所有的 \(i < j \leq |a|\),都有 \(a_i > a_j\),其中 \(|a|\) 表示当前 \(a\) 的元素个数。


考虑用一个栈维护当前数列所有后缀最大值的位置(即下标),初始时栈为空。

易证栈中元素对应的数是单调递减的,因此称为单调栈。

考虑 \(a\) 加入一个元素 \(a_i\) 时单调栈将如何变化。

  • 若栈顶元素 \(j\) 满足 \(a_j \le a_i\),则此时 \(a_j\) 不再是后缀最大值,因此弹出 \(j\)

  • 若栈顶元素 \(j\) 满足 \(a_j > a_i\),则此时 \(a_j\) 仍然是后缀最大值,不操作。

  • 由后缀最大值的定义知 \(a_i\) 一定是后缀最大值,\(i\) 入栈。

由于单调栈中的元素对应的数单调递减,所以应删去的元素仅存在于栈顶,因此只需要重复执行第一步直到 \(a_j > a_i\) 即可。

类似地,单调栈可以维护数列所有后缀最值或前缀最值,这是单调栈维护的本质信息。

实现

使用 STL 的 std::vector 容器而非 std::stack 实现单调栈,原因是 std::stack 时空常数巨大且 std::vector 支持 std::stack 的全部功能。

以下为核心代码:

std::vector<int> s;
for (int i = 1; i <= n; i++)
{
    while (!s.empty() && a[s.back()] <= a[i])
        s.pop_back();
    s.push_back(i);
}
for (auto i : s)
    std::cout << i << " ";
std::cout << "\n";

算法分析

每个元素只会在被插入时入栈一次,也只会出栈至多一次,所以总时间复杂度是 \(O(n)\),均摊单次插入 \(O(1)\)

基础应用

单调栈可以解决求“以某个值为最值的最大区间”的问题。

以下为 P5788 【模板】单调栈 - 洛谷 题目描述:

给出项数为 \(n\) 的整数数列 \(a_{1 \dots n}\)

定义函数 \(f(i)\) 代表数列中第 \(i\) 个元素之后第一个大于 \(a_i\) 的元素的下标,即 \(f(i)=\min_{i<j\leq n, a_j > a_i} \{j\}\)。若不存在,则 \(f(i)=0\)

试求出 \(f(1\dots n)\)


每一个被弹出的栈顶元素 \(j\) 都满足 \(a_j\) 是区间 \([1, i - 1]\) 中的后缀最大值,因此 \(a_i\) 就是在 \(a_j\) 之后第一个大于 \(a_j\) 的元素。

#include <iostream>
#include <vector>

int a[3000005], ans[3000005];
std::vector<int> s;

int main()
{
    std::ios::sync_with_stdio(false);
    std::cin.tie(nullptr);
    std::cout.tie(nullptr);

    int n;

    std::cin >> n;
    for (int i = 1; i <= n; i++)
        std::cin >> a[i];

    for (int i = 1; i <= n; i++)
    {
        while (!s.empty() && a[s.back()] < a[i])
        {
            ans[s.back()] = i;
            s.pop_back();
        }
        s.push_back(i);
    }

    for (int i = 1; i <= n; i++)
        std::cout << ans[i] << " ";
    std::cout << "\n";

    return 0;
}

类似地,对于一个数列 \(a\),单调栈可以在 \(O(n)\) 时间内求出以任意一个 \(a_i\) 为最值的区间位置。

单调队列

单调队列是一种内部元素具有单调性的队列,可以解决求“区间内最值”等问题。

原理

以下摘自 B3667 求区间所有后缀最大值的位置 - 洛谷 题目描述:

给定一个长度为 \(n\) 的数列 \(a\),对于其中每个长度为 \(k\) 的子区间,请你求出这个这个子区间构成的数列的所有后缀最大值的位置。

后缀最大值的定义同上。


仍考虑单调栈的思想。设当前处理的子区间为 \([l, r]\),则问题的本质是\([1, r]\) 的后缀最大值里落在 \([l, r]\) 的部分

因此当询问到 \([l, r]\) 时,\([1, l − 1]\) 内的数将不再有价值。这部分数是储存在栈底的,将其删去后栈中的元素就是 \([l, r]\) 的后缀最值了。

栈无法删除底部元素,因此使用双端队列实现,因此这种数据结构被称为单调队列。

类似地,单调队列可以维护数列中所有长度为 \(k\) 的子区间的前缀最值或后缀最值,这是单调队列维护的本质信息。

实现

使用 STL 的 std::deque 容器实现单调队列。但 std::deque 时空常数巨大,必要时应改为手写队列。

以下为核心代码:

std::deque<int> s;
for (int i = 1; i <= n; i++)
{
    int l = std::max(i - k + 1, 1);
    while (!s.empty() && s.front() < l)
        s.pop_front();
    while (!s.empty() && a[s.back()] < a[i])
        s.pop_back();
    s.push_back(i);
    if (i >= k)
    {
        for (auto i : s)
            std::cout << i << " ";
        std::cout << "\n";
    }
}

算法分析

与单调栈类似,每个元素只会在被插入时入队一次,也只会出队至多一次,所以总时间复杂度是 \(O(n)\),均摊单次插入 \(O(1)\)

基础应用

单调队列可以解决求“给定长度的区间内最值”的问题。

以下为 P1886 滑动窗口 /【模板】单调队列 - 洛谷 题目简述:

给出项数为 \(n\) 的整数数列 \(a_{1 \dots n}\) 和一个正整数 \(k\),求 \(a\) 中每个长度为 \(k\) 的子区间中的最大值和最小值。


以最大值为例,单调队列中存储的是每个区间中所有后缀最大值的位置,易证单调队列中元素对应的数单调递减,因此队首对应的即为这个区间的最大值。

#include <iostream>
#include <deque>
#include <algorithm>

int a[1000005];
std::deque<int> s;

int main()
{
    std::ios::sync_with_stdio(false);
    std::cin.tie(nullptr);
    std::cout.tie(nullptr);

    int n, k;

    std::cin >> n >> k;
    for (int i = 1; i <= n; i++)
        std::cin >> a[i];

    for (int i = 1; i <= n; i++)
    {
        int l = std::max(i - k + 1, 1);

        while (!s.empty() && s.front() < l)
            s.pop_front();
        while (!s.empty() && a[s.back()] >= a[i])
            s.pop_back();
        s.push_back(i);

        if (i >= k)
            std::cout << a[s.front()] << " ";
    }
    std::cout << "\n";

    s.clear();
    for (int i = 1; i <= n; i++)
    {
        int l = std::max(i - k + 1, 1);

        while (!s.empty() && s.front() < l)
            s.pop_front();
        while (!s.empty() && a[s.back()] <= a[i])
            s.pop_back();
        s.push_back(i);

        if (i >= k)
            std::cout << a[s.front()] << " ";
    }
    std::cout << "\n";

    return 0;
}