leetcode_84. 柱状图中最大的矩形

0 阅读3分钟

题目链接: 84. 柱状图中最大的矩形

一、题目描述:

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

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

示例 1:

image.png

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

示例 2:

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

提示:

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

题目模版

/**
 * @param {number[]} heights
 * @return {number}
 */
var largestRectangleArea = function(heights) {
    
};

思路分析

这道题可以用两个 for 循环做一个暴力求解

function largestRectangleArea(heights) {
    const n = heights.length;
    let ans = 0;
    for (let left = 0; left < n; ++left) {
      let minHeight = Infinity;
      for (let right = left; right < n; ++right) {
        minHeight = Math.min(minHeight, heights[right]);
        ans = Math.max(ans, (right - left + 1) * minHeight);
      }
    }
    return ans;
}

其示例如下

GPT-6.1 Sol 出的这个 Visualize,我看完直接瘫坐在椅子上,久久不能平静。

我只是贴了一段算法,它就把运行过程做成了能播放、暂停、逐步查看的交互动画。

这一刻,代码真的在我眼前活过来了。 --- codex

暴力解法的复杂度是 O(N^2),空间复杂度是 O(1),但其实还有另一种思路,就是从单个柱子的左右两边循环查找

它的一个查找思路就是:

  • 在左边找到最近并且比它矮的柱子
  • 在右边找到最近并且比它矮的柱子

那么以这个这个柱子能够辐射的范围/边界就确定了,因为最近两个比它矮的柱子,把它的柱子复制粘贴挡住了,以 index: 2 的柱子举例

image.png

学过高数的同学们肯定知道了,这就像夹逼定理

纯属虚构:本题采用暴力枚举,与夹逼定理无关

image.png

还有些小伙伴又有疑问,你这样夹逼,万一我宽度取胜呢?比如

image.png

但是又要注意了,我们的单柱+左右扩散是每个柱子都会去计算,所以我们还是能够知道面积为 12 的柱子最大

image.png

既然思路已经确定,那么我们该怎么实现呢?我们复制粘贴一下上面说的过程

  • 在左边找到最近并且比它矮的柱子
  • 在右边找到最近并且比它矮的柱子
    其实这是单调栈对应的 case,空间换时间,以「在右边找到最近并且比它矮的柱子」为例,我们用一个 mono_stack 存一个单调递增的栈,当在 curr_i 的找「在右边找到最近并且比它矮的柱子」,可以去遍历 mono_stack 里找到,算法如下
const r = new Array(heights.length);
mono_stack = [];
for(let i = heights.length - 1; i >= 0; i--) {
  while(mono_stack.length && heights[mono_stack[mono_stack.length - 1]] >= height[i]) mono_stack.pop();
  // 三元表达式的两种 case 可见紧跟代码块的示例图片
  r[i] = !mono_stack.length ? heights.length : mono_stack[mono_stack.length - 1];
  // 中止 while 循环的条件必然是 mono_stack[mono_stack.length - 1]
  // 即 栈顶高度 < heightByCurrI
  // 因此根据我们单调栈的单调递增的定义,此时需要入栈
  mono_stack.push(i);
}

image.png

这个时候又有同学有疑问了,你整个所谓的单调栈,也是 for 里面套个循环(即 while),它的复杂度不还是 O(N^2)?

其实不是,举个例子

image.png

无论是 [3,2,1] 还是 [2,1,2,1] 这种极端 case,它的遍历次数都是 <= n,也就是一个常数范围内,在「在右边找到最近并且比它矮的柱子」的遍历里,它的最差循环次数是 O(N + N) -> O(2*N) -> O(N)

在思路确定以后,就可以得到我们的 AC 代码,如下

AC 代码

/**
 * @param {number[]} heights
 * @return {number}
 */
var largestRectangleArea = function (heights) {
    let mono_stack = [];
    const l = new Array(heights.length);
    for (let i = 0; i < heights.length; i++) {
        while (mono_stack.length && heights[mono_stack[mono_stack.length - 1]] >= heights[i]) mono_stack.pop();
        l[i] = mono_stack.length === 0 ? -1 : mono_stack[mono_stack.length - 1];
        mono_stack.push(i);
    }
    mono_stack = [];
    const r = new Array(heights.length);
    for (let i = heights.length - 1; i >= 0; i--) {
        while (mono_stack.length && heights[mono_stack[mono_stack.length - 1]] >= heights[i]) mono_stack.pop();
        r[i] = mono_stack.length === 0 ? heights.length : mono_stack[mono_stack.length - 1];
        mono_stack.push(i);
    }
    let ret = 0;
    for (let i = 0; i < heights.length; i++) {
        ret = Math.max(ret, (r[i] - l[i] - 1) * heights[i]);
    }
    return ret;
};

相似题目

  1. 42. 接雨水 - leetcode
  2. 85. 最大矩形 - leetcode