首先介绍一下单调栈(或单调数组)。
单调栈是一个这样的数据结构:
1. 这个数组一定是单调递增(或单调递减)的数组;
2. 可以在末尾添加元素;
3. 在添加元素的过程中,允许弹出尽可能少的元素是数组依旧保持单调性;
例如:原来的单调数组是(1,3,5,7,9),现在希望添加一个元素6。此时我们把末尾的9,7两个元素均删除,插入6在末尾,新的单调数组变成(1,3,5,6)。
可以看出,当我们需要在这个数组末尾添加元素时,需要删除末尾所有大于新元素的元素,再进行添加。
用std::stack实现单调栈的过程很简单,在这里不多赘述。
但是其时间复杂度的常数很大,效率并没有手写的单调数组高(是手写的单调数组的近两倍)。
下面给出手写单调队列的伪代码:
void run(){
for(int i=1;i<=n;i++){
for(;nstack>0&&h[stack[nstack]]>h[i];nstack--){
弹出栈顶的元素,并用该元素处理更新答案
//stack[]数组中储存的是下标
}
stack[++nstack]=i;//存入i这个元素
}
}
下面给出一个最常规的单调数组的例题:
(POJ2559)如下图所示,在一条水平线上方有n(n<=100000)个矩形,求包含于这些矩形的并集内部的最大矩形的面积。
不难想到,对于包含第i根柱子,高度为h[i]的矩形,其宽度最大是向左右两边延伸,直到遇到第一根比h[i]矮的柱子。
如果对于每一个i,暴力枚举其左右两边的柱子,其复杂度为O(n²)。
思考我们的算法需要对于每个i,都找到h[i]前后恰好第一个比它低的柱子,从而求出一个候选长方形的面积,用这个面积去更新答案。
为什么单调数组可以起到这个作用?
来看这个例子,第2、5个元素因为比后面的元素大,从而从单调数组中被弹出,单调数组中储存的下标为(1,3,4,5,7)。
在这个单调数组中,每一个元素的前一个元素都是“左边第一个比它矮的柱子”,因为比它高的都被它弹掉了,那么它向左最大能延伸的宽度就是和它前一个元素的下标之差。
那么如何去寻找右边后一个比它矮的元素呢?
- 数组在插入时强制删除比新元素大的元素
- 对于删除的元素,由于数组单调递增的原因,新元素就是右边第一个比它矮的。
- 对于每个被弹出的元素,算出其对应的候选矩形,用来更新答案即可。
运用单调栈这种数据结构,使得复杂度降低到了O(n);
代码:
#include <cstdio>
#include <algorithm>
inline int read() {
int x = 0;
char ch = getchar();
while ('0' <= ch && ch <= '9') {
x = x * 10 + ch - '0';
ch = getchar();
}
return x;
}
const int maxn = 1000011;
int n, h[maxn], stack[maxn];
int top, ans;
int main() {
n = read();
h[n + 1] = 0;
for (int i = 1; i <= n; i++) h[i] = read();
for (int i = 1