单调栈
基本概念
单调栈的基础结构是“栈”,即元素是LIFO的,只能从栈顶控制数据出入;单调表示从栈的一端到另一端遍历,元素是单调递增或者单调递减的。比如单调递减栈,从栈底到栈顶的元素可以是:
bottom 10 7 5 3 2 1 top
单调栈获取和处理元素都是在线的方式。
假设有一个有序序列,元素是
1 6 3 4 2 3 4 5 8 9 7
我们想用一个单调递增的栈来遍历这个序列,则栈内元素变化是:
1
1 6
1 3
1 3 4
1 2
1 2 3
1 2 3 4
1 2 3 4 5
1 2 3 4 5 8
1 2 3 4 5 8 9
1 2 3 4 5 7
单纯看上面的栈,可能没有意义。但是,设想一个这样的场景,我们需要在线获取满足下面条件的区间[l, r)
对于任意的m, l <= m <= r,都有arr[m] >= arr[l]
我们想要获取所有满足这种条件区间,反观上面的单调递减栈的进栈顺序,确实可以 满足这种条件,以前两个位例子:
1 # 一个元素必然是
1, 6 # 1, 6 也满足这个区间
1, 3 # 1, 6, 3 这个序列也是,只不过6没有在栈中
为了记录区间范围,我们可以把栈存储元素,改成存储下标,这样可以动态的获取满足条件的区间。
应用
首先明确,单调栈适合处理依赖序列单调性质的问题,同时注意,单调栈在某种意义上是在线算法,因为是动态增删元素。
最经典的应用是求解直方图最大矩形的面积,题目分析在最后的参考文章中有,这里不再赘述。明确一点,从当前柱子向右扩展,那么扩展的最大边界是第一个比该柱子低的柱子。如果柱子是递增的,则每一个都可以作为新的启点。
#include <iostream>
#include <stack>
#include <vector>
#include <algorithm>
// 直方图最大矩形面积
int maxMartixArea(const std::vector<int>& line) {
if (line.empty()) {
return 0;
}
std::stack<int> st;
int maxArea = -1;
int N = line.size();
for (int i = 0; i < line.size(); ++i) {
while (!st.empty() && line[st.top()] >= line[i]) {
int t = st.top();
int s = (i - t) * line[t];
maxArea = std::max(s, maxArea); // 在线计算最大面积,处理连续递增的面积
st.pop();
}
st.push(i); // 每个元素入栈一次
}
// 处理栈中多余的元素
if (!st.empty()) { // 占位符
N = st.top() + 1;
}
while (!st.empty()) {
int t = st.top();
int s = (N - t) * line[t];
maxArea = std::max(s, maxArea);
}
return maxArea;
}
int main() {
return 0;
}
单调队列
基本概念
基于单调栈的一个扩展,当单调栈的栈底元素可以弹出的时候,单调栈即可转化单调队列。单调队列是一个受限的双端队列,因为队头只能弹出元素。由单调栈可以知道,单调栈维护的是单调区间。以上面的单调递增队列为例子,栈底的元素,永远是当前区间中最小的值,因此单调队列可以在 O ( 1 ) O(1) O(1)的时间内,获取队首队尾区间的最值,因此单调的区间的最大用处,也在于获取区间的最值上。
一般来说,我们的区间需要满足某个递减条件,然后确定区间的长度,并求解区间的最值,队首元素会不断更替,以满足区间长度的需求。
应用
单调队列的最典型应用是求解滑动窗口最值。直接给出代码
std::vector<int> slideWinMaxNum(const std::vector<int>& arr, int winSize) {
std::vector<int> res;
if (arr.empty() || winSize <= 0 || arr.size() < winSize) {
return res;
}
std::deque<int> deq;
int N = arr.size();
for (int i = 0; i < N; ++i) {
while (!deq.empty() && arr[deq.back()] <= arr[i]) {
deq.pop_back();
}
deq.push_back(i);
if (i >= winSize - 1) {
res.push_back(arr[deq.front()]);
}
if (i - deq.front() >= winSize) {
deq.pop_front();
}
}
}
最大值减去最小值大于等于num的区间个数:
#include <iostream>
#include <vector>
#include <deque>
/*
* 基本思路是,利用两个单调队列,在以某个元素作为区间左边界的情况下,
* 不断更替区间范围,并动态获取最值,之后利用最值之差和下标之差,计算
* 符合条件的子数组的个数
*/
int MaxSubMinNum(const std::vector<int>& arr, int num) {
if (arr.empty()) {
return 0;
}
std::deque<int> qmax; // 递减队列
std::deque<int> qmin; // 递增队列
int res = 0;
int i = 0; // 区间左边界
int j = 0; // 区间右边界
int N = arr.size();
while (i < N) {
while (j < N) {
while (!qmax.empty() && arr[qmax.back()] <= arr[j]) {
qmax.pop_back(); // 维护递减队列性质
}
qmax.push_back(j);
while (!qmin.empty() && arr[qmin.back()] >= arr[j]) {
qmin.pop_back(); // 维护递增队列性质
}
qmin.push_back(j);
if (qmax.front() - qmin.front() >= num) {
break; // 找到满足性质的区间,不再需要递增区间右边界范围
}
++j;
}
if (qmax.front() == i) {
qmax.pop_front(); // 以arr[i]为边界的区间查找结束
}
if (qmin.front() == i) {
qmin.pop_front();
}
res += (j - i); // arr[i]为边界的个数
i++; // 移动窗口右边界
}
}
int main() {
return 0;
}
参考文献
- https://endlesslethe.com/monotone-queue-and-stack-tutorial.html
- https://cloud.tencent.com/developer/article/1109268
- https://blog.csdn.net/Dacc123/article/details/50545577