Welcome to my cnblo|

lzy20091001

园龄:1年3个月粉丝:5关注:2

单调栈与单调队列

单调栈

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

原理

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

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

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


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

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

考虑 a 加入一个元素 ai 时单调栈将如何变化。

  • 若栈顶元素 j 满足 ajai,则此时 aj 不再是后缀最大值,因此弹出 j

  • 若栈顶元素 j 满足 aj>ai,则此时 aj 仍然是后缀最大值,不操作。

  • 由后缀最大值的定义知 ai 一定是后缀最大值,i 入栈。

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

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

实现

使用 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 的整数数列 a1n

定义函数 f(i) 代表数列中第 i 个元素之后第一个大于 ai 的元素的下标,即 f(i)=mini<jn,aj>ai{j}。若不存在,则 f(i)=0

试求出 f(1n)


每一个被弹出的栈顶元素 j 都满足 aj 是区间 [1,i1] 中的后缀最大值,因此 ai 就是在 aj 之后第一个大于 aj 的元素。

#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) 时间内求出以任意一个 ai 为最值的区间位置。

单调队列

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

原理

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

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

后缀最大值的定义同上。


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

因此当询问到 [l,r] 时,[1,l1] 内的数将不再有价值。这部分数是储存在栈底的,将其删去后栈中的元素就是 [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 的整数数列 a1n 和一个正整数 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;
}
posted @   lzy20091001  阅读(31)  评论(0编辑  收藏  举报
点击右上角即可分享
微信分享提示
评论
收藏
关注
推荐
深色
回顶
收起