单调栈
单调栈 = 单调 + 栈
模拟单调递增栈的操作:
- 如果栈为 空 或者栈顶元素 大于 入栈元素,则入栈;
- 否则,入栈则会破坏栈内元素的单调性,则需要将不满足条
件的栈顶元素全部弹出后,将入栈元素入栈。
单调递增栈的伪代码:
单调栈的作用:
- 线性的时间复杂度
- 单调递增栈 可以找到数组中往左/往右第一个比当前元素严格小 < < <(或者不小于 ≥ \geq ≥)的元素
- 从压栈的角度理解,从左至右压入单调栈,将元素 x x x压栈时,此时栈顶元素 t o p top top是往左第一个比 x x x小的元素,因为不存在元素 y y y在 t o p top top右侧且比 x x x小, y y y一定会成为新的栈顶,这和 t o p top top是栈顶相矛盾
- 也可以从弹栈的角度理解,元素 x x x第一个弹出元素 y y y是 x x x往左第一个不小于它的元素,反过来,元素 x x x是 y y y往右第一个小于它的元素
- 可能有些元素不存在以上关系
- 单调递减栈 可以找到数组中往左/往右第一个比当前元素严格大 > > >(或者不大于 ≤ \leq ≤)的元素
- 可以求得以当前元素为最值的最大区间
代码模板
- 最大矩形问题:给定 n 个非负整数,用来表示柱状图中各个柱子的高度。每个柱子彼此相邻,且宽度为 1 。求在该柱状图中,能够勾勒出来的矩形的最大面积。
- 数组
R,L
的含义是往左/右最后一个不小于当前元素的元素索引,这个可以通过找到左右第一个小于当前元素的元素,索引 ± 1 \pm1 ±1得到 - 弹栈时,栈顶元素
h[stk.top()]
往右第一个小于它的元素是h[i]
;压栈时,h[i]
往左第一个小于它的元素是h[stk.top()]
- 压栈时,栈空代表左侧没有元素小于
h[i]
,因此L[i]=0
;元素最后仍然在栈中,R[stk.top]=n-1
int R[N], L[N];
int largestRectangleArea(vector<int>& h) {
int n = h.size();
stack<int> stk;
for (int i = 0; i < n; i++) {
while (!stk.empty() && h[stk.top()] >= h[i]) {
R[stk.top()] = i - 1;
stk.pop();
}
if (!stk.empty()) L[i] = stk.top() + 1;
else L[i] = 0;
stk.push(i);
}
while (!stk.empty()) {
R[stk.top()] = n - 1;
stk.pop();
}
int ans = 0;
for (int i = 0; i < n; i++) {
ans = max(ans, h[i] * (R[i] - L[i] + 1));
}
return ans;
}
- 接雨水问题:给定 n 个非负整数表示每个宽度为 1 的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。
- 维护一个单调递减栈,从左到右遍历至下标 i i i,若栈中至少有两个元素,记栈顶元素索引为 t o p top top,栈顶的下一个元素为 l l l,首先根据单调递减性 h [ l ] > h [ t o p ] h[l]>h[top] h[l]>h[top],若 h [ i ] > h [ t o p ] h[i]>h[top] h[i]>h[top],则可以形成宽为 i − l − 1 i-l-1 i−l−1,高为 m i n ( h [ i ] , h [ l ] ) − h [ t o p ] min(h[i],h[l])-h[top] min(h[i],h[l])−h[top]的积水区域
- 实际上 h [ i ] > h [ t o p ] h[i]>h[top] h[i]>h[top]产生弹栈操作
- 也可以动态规划解法,对于下标 i i i,下雨后水能到达的最大高度等于下标 i i i两边的最大高度的最小值,下标 i i i处能接的雨水量等于下标 i i i处的水能到达的最大高度减 h [ i ] h[i] h[i]
- 单调栈方法是水平地划分积水区域,动态规划是竖直地划分区域
int trap(vector<int>& h) {
int ans = 0;
int n = h.size();
stack<int> stk;
for (int i = 0; i < n; i++) {
while (!stk.empty() && h[stk.top()] <= h[i]) {
int itop = stk.top(); stk.pop();
if (stk.size() > 0) {
ans += (min(h[stk.top()], h[i]) - h[itop]) * (i - stk.top() - 1);
}
}
stk.push(i);
}
return ans;
}
单调队列
单调队列 = 单调 + 队列
- 单调队列的维护过程与单调栈相似
- 区别在于
- 单调栈只维护一端(栈顶), 而单调队列可以维护两端(队首和队尾)
- 单调栈通常维护 全局 的单调性, 而单调队列通常维护 局部 的单调性
- 由于单调队列 可以队首出队 以及 前面的元素一定比后面的元素先入队 的
性质,使得它可以维护局部的单调性 - 考虑区间
[
L
,
R
]
[L,R]
[L,R],依次将区间内的数加入单调递增队列,则队首是区间最小
值。若区间要变成 [ L + 1 , R + 1 ] [L+1,R+1] [L+1,R+1],将 R + 1 R+1 R+1加入队列。判断第 L L L个元素是不是队首。如果不是队首,说明队首元素在 L L L后面,是区间 [ L + 1 , R + 1 ] [L+1,R+1] [L+1,R+1]最小值。如果是队首,将队首出队,新队首是区间 [ L + 1 , R + 1 ] [L+1,R+1] [L+1,R+1]最小值。 - 时间复杂度与单调栈一致
代码模板
- 滑动窗口最大值问题:给你一个整数数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。返回滑动窗口中的最大值 。
- 可能存在同时求最大最小值的情况,为了提高代码复用性,将单调队列封装为
find()
方法 - 单调递减栈可以维护区间最大值,我们将序列元素全部取反,又可以维护最小值,记录答案时通过参数
opt
还原序列
vector<int> find(int opt, vector<int>& a, int k) {
deque<int> Q;
int n = a.size();
vector<int> ans;
for (int i = 0; i < n; i++) {
while (!Q.empty() && Q.front() < i - k + 1) Q.pop_front();
while (!Q.empty() && a[Q.back()] < a[i]) Q.pop_back();
Q.push_back(i);
if (i >= k - 1) {
ans.push_back(opt * a[Q.front()]);
}
}
return ans;
}
vector<int> maxSlidingWindow(vector<int>& nums, int k) {
return find(1, nums, k);
}
more
之前补的一道单调队列优化dp,b题
https://blog.csdn.net/qq_23096319/article/details/119935127
一个网格图,一些格子可以着陆,一些不可以,一次可以至多跳
k
k
k步,问从最左上角跳到最右下角的最少步数。
重新整理一下思路:定义状态f[i][j]
代表从最左上角跳到该格子的最少步数,有状态转换方程f[i][j]=min(f[x][y])
,其中(i-x<=k && j==y) || (j-y<=k && i==x) && ok[x][y] && ok[i][j]
,由于是按行扫描,所以只需要维护一个行队列,到下一行清空,每一列均需维护一个单调队列。