## 【算法题】最大矩形面积,单调栈解法
柱状图中最大的矩形
给定 $n$ 个非负整数,用来表示柱状图中各个柱子的高度。每个柱子彼此相邻,且宽度为 $1$ 。
求在该柱状图中,能够勾勒出来的矩形的最大面积。
题意很简单,翻译一下就是:求该图中最大矩形的面积。
那么,这道题的思路就是遍历。不过如何高效遍历是一个学问。
### 一、暴力法
**先上暴力法,一切美好都从暴力开始**
$Code$
```cpp {.line-numbers}
#include
using namespace std;
const int N = 10000010;
int a[N];
int res;
/*
测试样例I
6
3 2 1 4 5 2
输出:
8
测试样例II
5
2 4 3 4 5
输出:
12
*/
int main() {
int n;
cin >> n;
for (int i = 0; i < n; i++) cin >> a[i];
for (int i = 0; i < n; i++) {
int width = 1;
for (int j = i - 1; j >= 0; j--) {
if (a[j] < a[i]) break;
width++;
}
for (int j = i + 1; j < n; j++) {
if (a[j] < a[i]) break;
width++;
}
res = max(res, width * a[i]);
}
cout << res << endl;
return 0;
}
```
很明显,暴力的算法在特定给定数据的情况下是效率是$O(N^2)$的,比如共$1e7$个数据,每个数据的高度都是$100$,那么,遍历到每个柱子时,都需要向左+向右共检查$1e7-1$个柱子,也就是一个完整的双重循环,需要计算$1e14$次,$C++$一秒的计算大约是$1e8$,肯定要超时。当然,一般的普通数据,用暴力来求解还是没有问题的,可以在考场中骗一半左右的分数。
### 二、单调栈优化
#### 1、什么是单调栈
- 本质就是一个栈
- 单调上升或单调下降,比如$1 \ 2 \ 3 \ 4,$或者$4 \ 3 \ 2 \ 1$
#### 2、生成逻辑
以单调上升栈为例进行说明:
- ① 单调栈内保存的是 **序号**,不是数值,不是数值,不是数值,是用序号去找的数值!
- ② 遇到新元素,如果栈为空,则直接入栈
- ③ 遇到新元素,如果栈不空,并且新元素比栈顶元素大,则直接入栈
- ④ 遇到新元素,如果栈不空,并且新元素比栈顶元素小,则栈顶元素出栈,重复步骤④,直到遇到场景②或③,完成入栈
- ⑤ 最终形成一个单调上升的序列(单调上升指的是数值单调上升)
#### 3、模拟求解过程
以该矩形为例
以$2$为高的最大矩形是 $2 * 4 = 4$;
以$3$为高的最大矩形是$3 * 3 = 9$;
以$4$为高的最大矩形是$4 * 2 = 8$;
以$5$为高的最大矩形是$5 * 1 = 1$;
那么有人要问了,有聪明的小脑袋们要问了,哎呀。
实际上某些矩形中间还有矩形,并不是真正的递序列,会不会产生影响捏?
如果原图为这样,那么出栈之后维护的递增图与上图对应。由于中间的$4$大于$3$也大于$2$,所以,中间的矩形应该是最大的,可以把$4$当成$3$即可。 **我们可以以出栈为契机**,计算矩形的面积
**语言描述**:
1、 栈中为空栈,将矩形$0$入栈。 此时栈中矩形为:$0$
2、 矩形$1$的高为$4$,大于栈顶元素$2$,将矩形$1$入栈,此时栈中矩形为 $0 ,1$
3、 矩形$2$的高为$3$,$3$小于栈顶元素的高$4$,所以将栈顶矩形$1$出栈,同时计算矩形$1$高的最大
矩形,为 $4 * 1 = 4$;同时将$3$入栈,此时栈中矩形为: $0, 2$
4、 因为矩形$3$的高大于栈顶矩形$2$的高,所以将矩形$3$入栈,此时栈中矩形为: $0, 2,3$
5、 因为矩形$4$的高大于栈顶矩形$3$的高,所以将矩形$4$入栈,此时栈中矩形为: $0, 2,3,4$
6、 此时所有的元素已经入栈完毕,且栈中元素为地址矩形,**依次出栈计算所有值即可**。
#### 4、深入理解
**$Q$:为什么单调栈的求解过程是正确的呢?**
其实单调栈并不是什么天外飞仙,不是什么牛哄哄,我们可以通过先暴力后优化的思路来思考它是怎么工作的:
**暴力算法**:
1、枚举每个柱子
2、针对每个柱子,向左找到第一个比自己小的,向右找到第一个比自己小的,两个位置之间的长度就是以自己为根据地最大矩形的宽度,再乘上自己的高度,就得到的以自己为根据地的最大矩形。
**单调栈优化**
1、该枚举还是得枚举,不枚举是不可能的,第一层循环不能省。
2、那就只能考虑省第二层循环了,需要把第二层循环干掉。
3、怎么干掉呢?思考一下它这是在干什么:向左找出第一个比自己小的柱子位置,向右找出第一个比自己小的柱子位置。
4、联想一下单调栈的处理逻辑,先思考一下向右的情况:
- 当遇到的新元素比栈顶元素大时,就直接加入栈,此时表示栈内的旧元素还没有找到比自己小的柱子。
- 当遇到的新元素比栈顶元素小时,旧的元素要出栈了!为什么要出栈呢?是因为它已经找到了右边第一个比它小的位置,它的目标实现了,不出栈还等着上菜吗?
5、现在我们整明白了向右是怎么探测第一个比自己小的元素的,那么左边我也没看到它处理啊,为什么左边也能正确的找到位置呢?
6、我们要理解清楚,栈里放的是序号,是序号,序号,不是柱子高度!然后再来进行思考:
当某个柱子出栈后,那么现在栈顶的元素是哪个呢?当然是左侧第一个比自己小的啊!$OK$,完美啊!一个单调栈的数据结构,一次出栈就把向左找比自己小,向右找比自己小的全部搞定!而且是一路走来动态维护,方便又快捷!
7、最后一个问题:那如果最终形成了类似于$1,2,3,4$这样的单调上升的栈内情况,它们不是还得出栈吗?要不它们不是还没有找到自己负责的最大矩形吗?
是的,它们最终不在呆在栈内,也是要出栈的。但是问题是它们后面已经没有新元素加入了,不是死在里面了吗?嗯,此时 **哨兵** 的伟大思想放光芒!我们在最后虚拟的加入一个高度为$0$的哨兵,前面不管多高都会比它高,也就是它们都会遇到一个比自己小的数字! 加入后面的哨兵后,就把所有逻辑统一了,也就都会出栈了。
#### 5、$Code$
```cpp {.line-numbers}
#include
using namespace std;
const int N = 10000010;
int a[N];
int res;
/*
测试样例I
6
3 2 1 4 5 2
输出:
8
测试样例II
5
2 4 3 4 5
输出:
12
*/
int main() {
int n;
cin >> n;
for (int i = 0; i < n; i++) cin >> a[i];
stack stk;
for (int i = 0; i <= n; i++) { // 注意:这里是0~n,其实数组中有效数据是0~n-1,之所以最后要多枚举一个n,是因为栈内可能存在一直找不到右手边比自己小的数据,最后补一个0,让它们都出栈
while (stk.size() && a[i] <= a[stk.top()]) { // 栈不为空,并且,当前柱子高度小于栈顶柱子高度,此时,栈顶柱子找到了右手边的最远距离
int height = a[stk.top()]; // 栈顶柱子高度
stk.pop(); // 栈顶柱子出栈
int width = (stk.size() == 0 ? i : i - 1 - stk.top());
res = max(res, height * width);
}
stk.push(i);
}
cout << res << endl;
return 0;
}
```
#### 6、总结
经典代码就是经典,这个单调栈的数据结构,如果你原来没有学过,我就不相信你能在考场上现场做出这道题,像学习外语一下学习算法这句话不是白说的,大家一定要注意 **厚积薄发**!
金鳞岂是池中物,一遇风云便化龙。
做好准备,等待蜕变吧。