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

8.4 KiB
Raw Blame History

【算法题】最大矩形面积,单调栈解法

柱状图中最大的矩形 给定 n 个非负整数,用来表示柱状图中各个柱子的高度。每个柱子彼此相邻,且宽度为 1 。 求在该柱状图中,能够勾勒出来的矩形的最大面积。

题意很简单,翻译一下就是:求该图中最大矩形的面积。

那么,这道题的思路就是遍历。不过如何高效遍历是一个学问。

一、暴力法

先上暴力法,一切美好都从暴力开始

Code

#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、生成逻辑

以单调上升栈为例进行说明:

  • ① 单调栈内保存的是 序号,不是数值,不是数值,不是数值,是用序号去找的数值!
  • ② 遇到新元素,如果栈为空,则直接入栈
  • ③ 遇到新元素,如果栈不空,并且新元素比栈顶元素大,则直接入栈
  • ④ 遇到新元素,如果栈不空,并且新元素比栈顶元素小,则栈顶元素出栈,重复步骤④,直到遇到场景②或③,完成入栈
  • ⑤ 最终形成一个单调上升的序列(单调上升指的是数值单调上升)

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

#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、总结

经典代码就是经典,这个单调栈的数据结构,如果你原来没有学过,我就不相信你能在考场上现场做出这道题,像学习外语一下学习算法这句话不是白说的,大家一定要注意 厚积薄发!

金鳞岂是池中物,一遇风云便化龙。

做好准备,等待蜕变吧。