最大矩形面积问题 | 豆包MarsCode AI刷题

78 阅读6分钟

哎~来到了天津卫,是嘛也没学会,学会了单调栈,您看这对不对!
今天来做一道单调栈的题目:

题目链接:最大矩形面积问题

题目:

小S最近在分析一个数组 h1,h2,...,hN,数组的每个元素代表某种高度。小S对这些高度感兴趣的是,当我们选取任意 k 个相邻元素时,如何计算它们所能形成的最大矩形面积。
对于 k 个相邻的元素,我们定义其矩形的最大面积为: R(k)=k×min(h[i],h[i+1],...,h[i+k−1])
即,R(k) 的值为这 k 个相邻元素中的最小值乘以 k。现在,小S希望你能帮他找出对于任意 k,R(k) 的最大值。

思路分析

暴力解法

对于每个 i,我们可以计算从柱子 i 开始的所有连续区间的最小高度,进而计算矩形面积,或者计算以柱子 i 为最小高度的区间最大宽度。但是这样的暴力做法会导致 O(n^2) 的时间复杂度,不适合大规模输入。

优化:单调栈

为了更高效地计算矩形面积,我们使用单调栈来维护高度的索引,从而快速找到每个柱子的左右边界

单调栈的核心思想是:

  1. 栈内维护一个单调递增的高度索引序列。

  2. 遇到破坏递增性的位置时,弹出栈顶元素并计算矩形面积。

    • 以弹出的柱子为高度,确定其矩形的宽度范围(即左右边界)。
    • 更新当前的最大矩形面积。

为什么使用单调栈?

  • 快速找到左右边界:

    • 通过栈顶元素与当前元素的索引关系,快速确定柱子 h[i] 能向左右扩展的最大宽度。
  • 高效处理每个柱子:

    • 每个柱子最多进栈一次、出栈一次,时间复杂度为 O(n)。

代码实现

以下是基于Java的单调栈实现:

public static int solution(int n, int[] array) {
    // Edit your code here
    // 使用栈来存储柱子的索引
    Stack<Integer> stack = new Stack<>();
    int maxArea = 0;

    // 遍历每个柱子的高度
    for (int i = 0; i < n; i++) {
        // 当栈不为空,且当前柱子高度小于等于栈顶柱子高度时,弹出栈顶元素并计算面积
        while (!stack.isEmpty() && array[stack.peek()] >= array[i]) {
            int height = array[stack.pop()];
            int width = stack.isEmpty() ? i : i - stack.peek() - 1;
            maxArea = Math.max(maxArea, height * width);
        }
        // 将当前柱子的索引压入栈
        stack.push(i);
    }

    // 处理栈中剩余的柱子
    while (!stack.isEmpty()) {
        int height = array[stack.pop()];
        int width = stack.isEmpty() ? n : n - stack.peek() - 1;
        maxArea = Math.max(maxArea, height * width);
    }

    return maxArea;
}

代码详解

1. 数据结构与变量初始化

Stack<Integer> stack = new Stack<>();
int maxArea = 0;
  • stack:存储柱子的索引,保证栈内的柱子高度单调递增。
  • maxArea:记录目前为止找到的最大矩形面积。

2. 遍历柱子高度

for (int i = 0; i < n; i++) {
    while (!stack.isEmpty() && array[stack.peek()] >= array[i]) {
        int height = array[stack.pop()];
        int width = stack.isEmpty() ? i : i - stack.peek() - 1;
        maxArea = Math.max(maxArea, height * width);
    }
    stack.push(i);
}
  • 逻辑:

    • 当前柱子 h[i] 高度小于栈顶柱子高度时:

      • 弹出栈顶柱子,计算以该柱子高度为矩形的最大面积。

      • 宽度的计算:

        • 如果栈为空,说明弹出的柱子能向左扩展到边界 0。
        • 否则宽度是 i - stack.peek() - 1。
    • 将当前柱子的索引压入栈。


3. 处理剩余柱子

while (!stack.isEmpty()) {
    int height = array[stack.pop()];
    int width = stack.isEmpty() ? n : n - stack.peek() - 1;
    maxArea = Math.max(maxArea, height * width);
}
  • 逻辑:

    • 遍历完成后,栈中可能还有柱子未处理。
    • 对每个柱子计算矩形面积,宽度向右扩展到数组末尾 n。

4. 返回结果

return maxArea;

返回遍历过程中找到的最大矩形面积。


复杂度分析

  1. 时间复杂度: O(n)

    • 每个柱子最多进栈一次、出栈一次。
  2. 空间复杂度: O(n)

    • 栈在最坏情况下存储所有柱子的索引。

扩展题目

类似的单调栈经典题目:接雨水
听说字节跳动公司随便拉一个扫地大爷都会做这道题。
这道题有多种做法,这里讲解一下单调栈做法。

题目描述

给定 n 个非负整数表示每个宽度为 1 的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。

解题思路

我们用单调栈的方式解决此问题,通过维护柱子索引的栈来计算每个低洼处可能接到的雨水量。


为什么选择单调栈?

  • 单调栈能有效处理「左边最高」和「右边最高」的问题。
  • 栈中的柱子会按照高度递减存储(单调递减栈)。
  • 每次遇到一个比栈顶柱子高的柱子时,可以计算当前的低洼处能接的水量。

单调栈解决方法

核心思想
  • 遍历柱子:

    1. 当前柱子高度 h[i] 小于等于栈顶柱子高度时,直接入栈。

    2. 当前柱子高度 h[i] 大于栈顶柱子高度时,弹出栈顶柱子并计算雨水量:

      • 低洼处: 栈顶柱子高度。
      • 左右边界: 栈顶柱子的下一个元素是左边界,当前柱子是右边界。
      • 宽度: 右边界与左边界之间的距离。

代码实现

public static int trap(int[] height) {
    Stack<Integer> stack = new Stack<>();
    int water = 0;

    for (int i = 0; i < height.length; i++) {
        // 当前柱子高度大于栈顶柱子时,计算雨水量
        while (!stack.isEmpty() && height[i] > height[stack.peek()]) {
            int bottom = stack.pop(); // 弹出栈顶柱子(低洼处)

            if (stack.isEmpty()) {
                break; // 如果栈空,则没有左边界,跳过
            }

            int left = stack.peek(); // 左边界索引
            int width = i - left - 1; // 宽度:右边界 - 左边界 - 1
            int boundedHeight = Math.min(height[left], height[i]) - height[bottom]; // 高度差
            water += width * boundedHeight; // 雨水量 = 宽度 × 高度差
        }
        stack.push(i); // 将当前柱子的索引入栈
    }

    return water;
}

代码详解

  1. 遇到低洼:

    • 当前柱子高度 h[i] 小于等于栈顶柱子时,将当前柱子的索引压入栈。
    • 栈内维护一个单调递减的柱子索引序列。
  2. 遇到边界:

    • 当前柱子高度 h[i] 大于栈顶柱子时,弹出栈顶柱子,并尝试计算当前低洼处的雨水量:

      • 弹出的栈顶柱子是低洼处的底部。

      • 栈顶的下一个元素是左边界,当前柱子是右边界。

      • 雨水面积计算:

        • 宽度:i - left - 1。
        • 高度:min(h[left], h[i]) - h[bottom]。
        • 总雨水:宽度 × 高度。
  3. 当前柱子入栈:

    • 每次弹出完栈顶后,当前柱子索引 i 都需要压入栈,作为后续雨水的左边界。

返回累加的雨水量。


复杂度分析

  1. 时间复杂度: O(n)

    • 每个柱子最多进栈一次、出栈一次,因此整体复杂度为线性。
  2. 空间复杂度: O(n)

    • 使用了栈来存储柱子的索引。

你学废了吗?