代码随想录训练营day60| 84.柱状图中最大的矩形

@TOC


前言

代码随想录算法训练营day60


一、Leetcode 84.柱状图中最大的矩形

1.题目

给定 n 个非负整数,用来表示柱状图中各个柱子的高度。每个柱子彼此相邻,且宽度为 1 。

求在该柱状图中,能够勾勒出来的矩形的最大面积。

示例 1:

输入:heights = [2,1,5,6,2,3] 输出:10 解释:最大的矩形为图中红色区域,面积为 10

示例 2:

输入: heights = [2,4] 输出: 4

提示:

1 <= heights.length <=105
0 <= heights[i] <= 104

来源:力扣(LeetCode) 链接:https://leetcode.cn/problems/largest-rectangle-in-histogram

2.解题思路

方法一:单调栈

思路

我们归纳一下枚举「高」的方法:

首先我们枚举某一根柱子 ii 作为高 h=heights[i]h=heights[i];

随后我们需要进行向左右两边扩展,使得扩展到的柱子的高度均不小于 hh。换句话说,我们需要找到左右两侧最近的高度小于 hh 的柱子,这样这两根柱子之间(不包括其本身)的所有柱子高度均不小于 hh,并且就是 ii 能够扩展到的最远范围。

那么我们先来看看如何求出一根柱子的左侧且最近的小于其高度的柱子。除了根据「前言」部分暴力地进行枚举之外,我们可以通过如下的一个结论来深入地进行思考:

对于两根柱子 j0j0​ 以及 j1j1​,如果 j0<j1j0​<j1​ 并且 heights[j0]≥heights[j1]heights[j0​]≥heights[j1​],那么对于任意的在它们之后出现的柱子 ii(j1<ij1​<i),j0j0​ 一定不会是 ii 左侧且最近的小于其高度的柱子。

换句话说,如果有两根柱子 j0j0​ 和 j1j1​,其中 j0j0​ 在 j1j1​ 的左侧,并且 j0j0​ 的高度大于等于 j1j1​,那么在后面的柱子 ii 向左找小于其高度的柱子时,j1j1​ 会「挡住」j0j0​,j0j0​ 就不会作为答案了。

这样以来,我们可以对数组从左向右进行遍历,同时维护一个「可能作为答案」的数据结构,其中按照从小到大的顺序存放了一些 jj 值。根据上面的结论,如果我们存放了 j0,j1,⋯ ,jsj0​,j1​,⋯,js​,那么一定有 height[j0]

当我们枚举到第 ii 根柱子时,我们的数据结构中存放了 j0,j1,⋯ ,jsj0​,j1​,⋯,js​,如果第 ii 根柱子左侧且最近的小于其高度的柱子为 jiji​,那么必然有

height[j0]

这样我们就可以使用二分查找的方法找到 ii 对应的 jiji​,但真的需要吗?当我们枚举到 i+1i+1 时,原来的 ii 也变成了 jj 值,因此 ii 会被放入数据结构。由于所有在数据结构中的 jj 值均小于 ii,那么所有高度大于等于 height[i]height[i] 的 jj 都不会作为答案,需要从数据结构中移除。而我们发现,这些被移除的 jj 值恰好就是

ji+1,⋯ ,jsji+1​,⋯,js​

这样我们在枚举到第 ii 根柱子的时候,就可以先把所有高度大于等于 height[i]height[i] 的 jj 值全部移除,剩下的 jj 值中高度最高的即为答案。在这之后,我们将 ii 放入数据结构中,开始接下来的枚举。此时,我们需要使用的数据结构也就呼之欲出了,它就是栈。

栈中存放了 jj 值。从栈底到栈顶,jj 的值严格单调递增,同时对应的高度值也严格单调递增;

当我们枚举到第 ii 根柱子时,我们从栈顶不断地移除 height[j]≥height[i]height[j]≥height[i] 的 jj 值。在移除完毕后,栈顶的 jj 值就一定满足 height[j]<height[i]height[j]<height[i],此时 jj 就是 ii 左侧且最近的小于其高度的柱子。
    这里会有一种特殊情况。如果我们移除了栈中所有的 jj 值,那就说明 ii 左侧所有柱子的高度都大于 height[i]height[i],那么我们可以认为 ii 左侧且最近的小于其高度的柱子在位置 j=−1j=−1,它是一根「虚拟」的、高度无限低的柱子。这样的定义不会对我们的答案产生任何的影响,我们也称这根「虚拟」的柱子为「哨兵」。

我们再将 ii 放入栈顶。

栈中存放的元素具有单调性,这就是经典的数据结构「单调栈」了。

例子

我们用一个具体的例子 [6,7,5,2,4,5,9,3][6,7,5,2,4,5,9,3] 来帮助读者理解单调栈。我们需要求出每一根柱子的左侧且最近的小于其高度的柱子。初始时的栈为空。

我们枚举 66,因为栈为空,所以 66 左侧的柱子是「哨兵」,位置为 -1。随后我们将 66 入栈。
    栈:[6(0)]。(这里括号内的数字表示柱子在原数组中的位置)

我们枚举 77,由于 6<76<7,因此不会移除栈顶元素,所以 77 左侧的柱子是 66,位置为 00。随后我们将 77 入栈。
    栈:[6(0), 7(1)]

我们枚举 55,由于 7≥57≥5,因此移除栈顶元素 77。同样地,6≥56≥5,再移除栈顶元素 66。此时栈为空,所以 55 左侧的柱子是「哨兵」,位置为 −1−1。随后我们将 55 入栈。
    栈:[5(2)]

接下来的枚举过程也大同小异。我们枚举 22,移除栈顶元素 55,得到 22 左侧的柱子是「哨兵」,位置为 −1−1。将 22 入栈。
    栈:[2(3)]

我们枚举 44,55 和 99,都不会移除任何栈顶元素,得到它们左侧的柱子分别是 22,44 和 55,位置分别为 33,44 和 55。将它们入栈。
    栈:[2(3), 4(4), 5(5), 9(6)]

我们枚举 33,依次移除栈顶元素 99,55 和 44,得到 33 左侧的柱子是 22,位置为 33。将 33 入栈。
    栈:[2(3), 3(7)]

这样以来,我们得到它们左侧的柱子编号分别为 [−1,0,−1,−1,3,4,5,3][−1,0,−1,−1,3,4,5,3]。用相同的方法,我们从右向左进行遍历,也可以得到它们右侧的柱子编号分别为 [2,2,3,8,7,7,7,8][2,2,3,8,7,7,7,8],这里我们将位置 88 看作「哨兵」。

在得到了左右两侧的柱子之后,我们就可以计算出每根柱子对应的左右边界,并求出答案了。

分析

单调栈的时间复杂度是多少?直接计算十分困难,但是我们可以发现:

每一个位置只会入栈一次(在枚举到它时),并且最多出栈一次。

因此当我们从左向右/总右向左遍历数组时,对栈的操作的次数就为 O(N)O(N)。所以单调栈的总时间复杂度为 O(N)O(N)。

3.代码实现

```java class Solution { public int largestRectangleArea(int[] heights) { int n = heights.length; int[] left = new int[n]; int[] right = new int[n];

Deque<Integer> mono_stack = new ArrayDeque<Integer>();
    for (int i = 0; i < n; ++i) {
        while (!mono_stack.isEmpty() && heights[mono_stack.peek()] >= heights[i]) {
            mono_stack.pop();
        }
        left[i] = (mono_stack.isEmpty() ? -1 : mono_stack.peek());
        mono_stack.push(i);
    }

    mono_stack.clear();
    for (int i = n - 1; i >= 0; --i) {
        while (!mono_stack.isEmpty() && heights[mono_stack.peek()] >= heights[i]) {
            mono_stack.pop();
        }
        right[i] = (mono_stack.isEmpty() ? n : mono_stack.peek());
        mono_stack.push(i);
    }

    int ans = 0;
    for (int i = 0; i < n; ++i) {
        ans = Math.max(ans, (right[i] - left[i] - 1) * heights[i]);
    }
    return ans;
}

}

猜你喜欢

转载自blog.csdn.net/HHX_01/article/details/131285451