Files
python/TangDou/LanQiaoBei/LanQiao14ShengSai/4.md
HuangHai 1f397eca87 'commit'
2025-08-30 18:35:01 +08:00

191 lines
8.4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

## 【算法题】最大矩形面积,单调栈解法
柱状图中最大的矩形
给定 $n$ 个非负整数,用来表示柱状图中各个柱子的高度。每个柱子彼此相邻,且宽度为 $1$ 。
求在该柱状图中,能够勾勒出来的矩形的最大面积。
<center><img src='https://img-blog.csdnimg.cn/img_convert/1a4a10ab86dfc7436e040e1c76d677e5.jpeg#pic_center'></center>
题意很简单,翻译一下就是:求该图中最大矩形的面积。
那么,这道题的思路就是遍历。不过如何高效遍历是一个学问。
### 一、暴力法
**先上暴力法,一切美好都从暴力开始**
$Code$
```cpp {.line-numbers}
#include <bits/stdc++.h>
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、生成逻辑
以单调上升栈为例进行说明:
- ① 单调栈内保存的是 **序号**,<font color='red' size=4><b>不是数值,不是数值,不是数值</b></font>,是用序号去找的数值!
- ② 遇到新元素,如果栈为空,则直接入栈
- ③ 遇到新元素,如果栈不空,并且新元素比栈顶元素大,则直接入栈
- ④ 遇到新元素,如果栈不空,并且新元素比栈顶元素小,则栈顶元素出栈,重复步骤④,直到遇到场景②或③,完成入栈
- ⑤ 最终形成一个单调上升的序列(单调上升指的是数值单调上升)
#### 3、模拟求解过程
<center><img src='https://img-blog.csdnimg.cn/a82963f167ab48bca9578d0a205c591a.png#pic_center'></center>
<center><img src='https://img-blog.csdnimg.cn/7b28b564d0f84777ba1395ab40a8ad16.png#pic_center'></center>
<center><img src='https://img-blog.csdnimg.cn/e4695f5089034a0da020949682774993.png'></center>
以该矩形为例
以$2$为高的最大矩形是 $2 * 4 = 4$;
以$3$为高的最大矩形是$3 * 3 = 9$;
以$4$为高的最大矩形是$4 * 2 = 8$;
以$5$为高的最大矩形是$5 * 1 = 1$;
那么有人要问了,有聪明的小脑袋们要问了,哎呀。
实际上某些矩形中间还有矩形,并不是真正的递序列,会不会产生影响捏?
<center><img src='https://img-blog.csdnimg.cn/0144b085b8ed47cc950bba71114f1ca5.png'></center>
如果原图为这样,那么出栈之后维护的递增图与上图对应。由于中间的$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 <bits/stdc++.h>
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<int> 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、总结
经典代码就是经典,这个单调栈的数据结构,如果你原来没有学过,我就不相信你能在考场上现场做出这道题,像学习外语一下学习算法这句话不是白说的,大家一定要注意 **厚积薄发**!
<font color='red' size=4><b>金鳞岂是池中物,一遇风云便化龙。</b></font>
做好准备,等待蜕变吧。