数据结构------单调栈和单调队列
单调栈原理
单调栈,顾名思义,就是具有单调性的数据结构栈 。它依旧是一个栈结构,只不过里面存储的数据是严格(非)递增或者严格(非)递减的。栈可以参考栈的c语言实现-CSDN博客,也可以使用STL的工具stack或deque模拟。
单调栈能解决以下四个问题(或者说是因为有这些问题,才出现的单调栈):
- 寻找当前元素左侧,离它最近,并且比它大的元素在哪(左近大)。
- 寻找当前元素右侧,离它最近,并且比它大的元素在哪(右近大)。
- 寻找当前元素左侧,离它最近,并且比它小的元素在哪(左近小)。
- 寻找当前元素右侧,离它最近,并且比它小的元素在哪(右近小)。
虽然是四个问题,但是原理是一致的。因此,只要解决一个,举一反三就可以解决剩下的几个。
左侧最近最大问题
例如寻找当前元素左侧 ,离它最近 ,并且比它大的元素在哪 。
测试样例:
9
1 4 10 6 3 3 15 21 8
预期输出
0 0 0 3 4 4 0 0 8
利用单调栈解决:
从左往右遍历元素,构造一个单调递减的栈。插入当前位置的元素时:
- 如果栈为空,则左侧不存在比当前元素大的元素;
- 如果栈非空,插入当前位置元素时的栈顶元素就是所找的元素。
注意,因为我们要找的是最终结果的位置。因此,栈里面存的是每个元素的下标。
这一整个流程可以看成贪心 + 栈,即贪心策略通过栈来模拟实现。
参考程序:
cpp
#include<bits/stdc++.h>
using namespace std;
int main() {
int n; cin >> n;
vector<int>a(n + 1, 0), pos(n + 1, 0);
for (int i = 1; i <= n; i++)
cin >> a[i];
//构建单调栈
stack<int>sk;
for (int i = 1; i <= n; i++) {
while (sk.size() && a[sk.top()] <= a[i])//构建单调不递增的栈
sk.pop();
if (sk.size())//栈内有元素,则栈顶元素就是结果
pos[i] = sk.top();
sk.push(i);
}
for (int i=1;i<= n;i++)
cout << pos[i] << ' ';
return 0;
}
根据代码进行样例分析:
数组 a 1 4 10 6 3 3 15 21 8 下标 1 2 3 4 5 6 7 8 9 p o s 0 0 0 \begin{array}{|c|c|c|c|c|c|c|c|c|c|}\hline 数组a&1&4&10&6&3&3&15&21&8\\\hline 下标&1&2&3&4&5&6&7&8&9\\\hline pos&0&0&0&&&&&&\\\hline \end{array} 数组a下标pos110420103064353615721889
遍历1、4、10时,左边不存在比当前数大的数,因此标记为0.
单调栈变化:
1 ( 1 ) → 2 ( 4 ) → 3 ( 10 ) \begin{array}{|c|}\hline 1(1)&&&\\\hline\end{array}\rightarrow\begin{array}{|c|}\hline 2(4)&&&\\\hline\end{array}\rightarrow\begin{array}{|c|}\hline 3(10)&&&\\\hline\end{array} 1(1)→2(4)→3(10)
关于 4 的下标 2 入栈, 1 的下标 1 出栈的情况( a[st.top()]<=a[i] ):
- 栈顶元素不是元素要找的数。
- 栈顶元素必定不是后面的元素要找的数。
例如这里,小于 1 的数必定小于 4 ,只会优先考虑 4 而不是 0,例如 [1,4,0] 中的 0 ,左边最近的比 0 大的数是 4 而不是 1,填写表格时不会填 1 。
之后单调栈的变化:
3 ( 10 ) → 3 ( 10 ) 4 ( 6 ) \begin{array}{|c|}\hline 3(10)&&\\\hline\end{array}\rightarrow\begin{array}{|c|}\hline 3(10)&4(6)&\\\hline\end{array} 3(10)→3(10)4(6)
→ 3 ( 10 ) 4 ( 6 ) 5 ( 3 ) → 3 ( 10 ) 4 ( 6 ) → 3 ( 10 ) 4 ( 6 ) 6 ( 3 ) \rightarrow\begin{array}{|c|}\hline 3(10)&4(6)&5(3)\\\hline\end{array}\rightarrow\\\begin{array}{|c|}\hline 3(10)&4(6)&\\\hline\end{array}\rightarrow\\\begin{array}{|c|}\hline 3(10)&4(6)&6(3)\\\hline\end{array} →3(10)4(6)5(3)→3(10)4(6)→3(10)4(6)6(3)
继续填表:
数组 a 1 4 10 6 3 3 15 21 8 下标 1 2 3 4 5 6 7 8 9 p o s 0 0 0 3 4 4 \begin{array}{|c|c|c|c|c|c|c|c|c|c|}\hline 数组a&1&4&10&6&3&3&15&21&8\\\hline 下标&1&2&3&4&5&6&7&8&9\\\hline pos&0&0&0&3&4&4&&&\\\hline \end{array} 数组a下标pos110420103064335436415721889
这里的出栈条件是 a[sk.top()]>a[i] ,例如这里 4(6) 小于栈顶元素 3(10) ,则更新结果:4(6) 的左边最近的大于 6 的值是 10 ,于是下标 3 填进表格,同时将 6 本身的下标压进栈。 5(3) 同理。
6(3) 看栈顶代表元素 6(3) 和自己一样大,则出栈,直到找到第 1 个比它大的数 4(6) , 6 的下标 4 压进栈。
时间复杂度:这里看似2个循环,实际上在遍历的过程中只会进栈一次,出栈的部分相对于原来的数组,元素个数非常少几乎忽略不计,整体相当于遍历数组 2 次,也就是 O ( 2 n ) \text{O}(2n) O(2n) ,省略系数,时间复杂度就是 O ( n ) \text{O}(n) O(n) 。
P5788 【模板】单调栈 - 洛谷
寻找当前元素右侧,离它最近,并且比它大的元素在哪。比起找左侧,也只是将顺序进行逆向枚举。
cpp
#include<bits/stdc++.h>
using namespace std;
int main() {
int n; cin >> n;
vector<int>a(n + 1, 0), pos(n + 1, 0);
for (int i = 1; i <= n; i++)
cin >> a[i];
//构建单调栈
stack<int>sk;
for (int i = n; i >= 1; i--) {//找右侧则逆向枚举
while (sk.size() && a[sk.top()] <= a[i])//构建单调不递增的栈
sk.pop();
if (sk.size())//栈内有元素,则栈顶元素就是结果
pos[i] = sk.top();
sk.push(i);
}
for (int i=1;i<= n;i++)
cout << pos[i] << ' ';
return 0;
}
最近最小问题和总结
找最大值或最小值,只需要在上述代码的基础上更改 while 循环内不等式的符号即可。
例如寻找当前元素左侧 ,离它最近 ,并且比它小的元素在哪:
cpp
//构建单调栈
stack<int>sk;
for (int i = 1; i <= n; i++) {//寻找左侧
while (sk.size() && a[sk.top()] >= a[i])//构建单调不递减的栈
sk.pop();
if (sk.size())//栈内有元素,则栈顶元素就是结果
pos[i] = sk.top();
sk.push(i);
}
测试样例:
9
1 4 10 6 3 3 15 21 8
输出结果:
0 1 2 2 1 1 6 7 6
寻找当前元素右侧 ,离它最近 ,并且比它小的元素在哪,只需要在上个代码的基础上修改遍历方向即可。
cpp
//构建单调栈
stack<int>sk;
for (int i = n; i >= 1; i--) {//找右侧则逆向枚举
while (sk.size() && a[sk.top()] >= a[i])//构建单调不递减的栈
sk.pop();
if (sk.size())//栈内有元素,则栈顶元素就是结果
pos[i] = sk.top();
sk.push(i);
}
测试样例:
9
1 4 10 6 3 3 15 21 8
输出结果:
0 5 4 5 0 0 9 9 0
总结这4个问题用单调栈解决:
- 找左侧,正遍历;找右侧,逆遍历;
- 比它大,单调减;比它小,单调增。
P1901 发射站 - 洛谷
分别对左侧和右侧使用单调栈,找到比当前发射站高的发射站,将能量叠加到它们的名下,然后取最大值即可。
cpp
#include <bits/stdc++.h>
using namespace std;
using vi = vector<int>;
void get_left(vi &enr, vi &a, vi &V) {
stack<int> sk;
for (int i = 1; i < a.size(); i++) {
while (sk.size() && a[sk.top()] <= a[i])
sk.pop();
if (sk.size())
enr[sk.top()] += V[i];
sk.push(i);
}
}
void get_right(vi &enr, vi &a, vi &V) {
stack<int> sk;
for (int i = a.size() - 1; i >= 1; i--) {
while (sk.size() && a[sk.top()] <= a[i])
sk.pop();
if (sk.size())
enr[sk.top()] += V[i];
sk.push(i);
}
}
int main() {
int n;
cin >> n;
vi H(n + 1, 0), V(n + 1, 0);
vi enr(H); // 每个塔最多能接收的能量
for (int i = 1; i <= n; i++)
cin >> H[i] >> V[i];
get_left(enr, H, V);
get_right(enr, H, V);
int ans = 0;
for (int i = 1; i <= n; i++)
ans = max(ans, enr[i]);
cout << ans;
return 0;
}
B4273 最大的矩形纸片 - 洛谷
B4273 [蓝桥杯青少年组省赛 2023\] 最大的矩形纸片 - 洛谷](https://www.luogu.com.cn/problem/B4273)
可定义一个扫描线,这个扫描线从上往下扫描,找到该扫描线能分割出的矩形,找出最大值即可。这个实现可以用线段树、树状数组、单调栈。这里使用单调栈实现。
当扫描线和某个柱子的顶部重合时,可在两端**离它最近的小于它的柱子** 之间找到高为它,宽为两端离它最近的小于它的柱子的坐标之差减一的矩形。例如下图的 x x x 和 y y y 。
但也有找不到的,则说明左边或右边没有小于它的柱子,扫描线可以截取多个高度一样的柱子。或者说在两端存在一个高度为 0 的柱子,两端离它最近的小于它的柱子就是这俩虚幻的柱子。例如下图标红的 x ′ x\^{\\prime} x′ 和 y ′ y\^{\\prime} y′ 。

因此对每个柱子使用单调栈求它的两端最近的比它小的柱子,然后再模拟扫描线遍历矩阵即可。
```cpp
#include