每日一题:LeetCode 84. 柱状图中最大的矩形

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

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

以上是柱状图的示例,其中每个柱子的宽度为 1,给定的高度为 [2,1,5,6,2,3]

图中阴影部分为所能勾勒出的最大矩形面积,其面积为 10 个单位。

示例:

输入: [2,1,5,6,2,3]
输出: 10

 

解题思路

取所有已知高度矩形的最大面积,例如[2,1,5,6,2,3]

得到所有1为高度,宽度为width_1(未知)的最大值

得到所有2为高度,宽度为width_2(未知)的最大值

得到所有3为高度,宽度为width_3(未知)的最大值

得到所有5为高度,宽度为width_5(未知)的最大值

得到所有6为高度,宽度为width_6(未知)的最大值

然后将所有面积最大值进行对比,就可能得到最终的最大值了。

如何将思路实际编码呢?

class Solution {
    public int largestRectangleArea(int[] heights) {
        //暴力解法
        //根据枚举中出现的最小高度的所有可能的情况
        int res = 0;
        for (int i = 0; i < heights.length; i++) {
            int minHeight = Integer.MAX_VALUE;
            for (int j = i; j < heights.length; j++) {
                minHeight = Math.min(minHeight,heights[j]);
                res = Math.max(res,(j - i + 1)*minHeight);
            }
        }
        return res;
    }
}

存在不必要的重复解,存在很明显的无效解,还有优化的空间。直接计算当前位置的左右边界,确定left和right。

class Solution {
    public int largestRectangleArea(int[] heights) {
        int len = heights.length;
        int res = 0;
        for (int i = 0; i < len; i++) {
            int height = heights[i];
            int left = i;
            int right = i;
            //计算完第一个元素之和,从第二个元素开始判断
            while (left - 1 >= 0 && heights[left - 1] >= height) {
                left--;
            }
            while (right + 1 < len && heights[right + 1] >= height) {
                right++;
            }
            res = Math.max(res, (right - left + 1) * height);
        }
        return res;
    }
}

虽然去掉了不必要的重复解,但是时间复杂度o(n^2),会导致超时存在。因为存在不必要的逻辑判断,思考是否可以去掉?

根据上面的分析,我们知道对于第i根柱子所围成的最大矩形是

s=(right-left+1)*height[i]

有两种思考途径:

1 减少比较次数,将每次比较的最大值存储起来,将问题变成动态规划的求dp值

2 找最近重复子问题,分析问题的重复性,发现其实是可以用单调栈去解决的

动态规划求dp值不容易想到,先从最近重复子问题出发。

 

找最近重复子问题

我们就拿示例的数组 [2, 1, 5, 6, 2, 3] 为例:

1、一开始看到的柱形高度为 2 ,这个时候以这个 2 为高度的最大面积的矩形还不能确定,我们需要继续向右遍历,如下图。

image.png

然后看到到高度为 1 的柱形,这个时候以这个柱形为高度的矩形的最大面积还是不知道的。但是它之前的以 2 为高度的最大面积的矩形是可以确定的,这是因为这个 1 比 2 小 ,因为这个 1 卡在了这里 2 不能再向右边扩展了,如下图。

image.png

那么也就是说,只要右边的柱子的高度一直比当前柱子大,那么当前柱子的宽度就是不能确定的,一旦遇到比当前柱子小的,当前柱子的宽度就确定了。

那么其实是可以维护一个单调栈,栈内元素是单调递增的,一旦发现当前元素比栈顶元素要小,就可以确定栈顶元素的宽度了,然后将能确定宽度的栈内元素依次比较和出栈并计算面积。

image.png

image.png

image.png

image.png

image.png

image.png

image.png

image.png

image.png

此处还涉及一个小技巧 - 哨兵节点

public class Solution {

    public int largestRectangleArea(int[] heights) {
        int len = heights.length;
        if (len == 0) {
            return 0;
        }

        if (len == 1) {
            return heights[0];
        }

        int res = 0;

        int[] newHeights = new int[len + 2];
        newHeights[0] = 0;
        System.arraycopy(heights, 0, newHeights, 1, len);
        newHeights[len + 1] = 0;
        len += 2;
        heights = newHeights;

        Deque<Integer> stack = new ArrayDeque<>(len);
        // 先放入哨兵,在循环里就不用做非空判断
        stack.addLast(0);
        
        for (int i = 1; i < len; i++) {
            while (heights[i] < heights[stack.peekLast()]) {
                int curHeight = heights[stack.pollLast()];
                int curWidth = i - stack.peekLast() - 1;
                res = Math.max(res, curHeight * curWidth);
            }
            stack.addLast(i);
        }
        return res;
    }
}

参考:https://leetcode-cn.com/problems/largest-rectangle-in-histogram/solution/bao-li-jie-fa-zhan-by-liweiwei1419/

 

动态规划分析

求动态规划的几个步骤:

1 由于需要用到dp[ ]数组,那么我要先确定dp[i] 代表的含义是什么?

定义两个dp数组,left[i]表示左边比i小的位置索引,right[i]表示右边比i小的位置索引

2 如何确定转移方程?

只要元素一直满足这个条件 heights[i - 1] >= heights[i],left[i] 就能赋值到左边最小的索引

只要元素一直满足这个条件heights[i+1] >= heights[i],right[i] 就能赋值到右边最小的索引

3 如何确定初始值和边界条件?

左边比i小的初值定义为-1,右边比i小的初值定义为数组大小height.length,哨兵技巧

4 如何确定计算顺序?

一般是从小到大去计算的,因为此题f(1),f(2)已经是已知的了。如果是从大到小计算的话,那么要保证之前的值已经计算出来了

 

class Solution {
    public int largestRectangleArea(int[] heights) {
        int len = heights.length;
        //定义两个dp数组,left[i]表示左边比i小的位置索引,right[i]表示右边比i小的位置索引
        int[] left = new int[heights.length];
        int[] right = new int[heights.length];
        //左边比i小的初值定义为-1。右边比i小的初值定义为数组大小height.length,方便计算
        left[0] = -1;
        right[len - 1] = len;
        //计算每一个位置上的left[i]   即左边比i小的位置索引
        for (int i = 1; i < len; i++) {
            int p = i - 1;
            while (p >=0 && heights[p] >= heights[i]){
                p = left[p];
            }
            //获取到比当前高度小的位置的索引
            left[i] = p;
        }
        //计算每一个位置上的right[i]   即右边比i小的位置索引
        for (int i = len - 2; i >=0; i--) {
            int p = i+1;
            while (p < len && heights[p] >= heights[i]){
                p = right[p];
            }
            right[i] = p;
        }
        int area = 0;
        for (int i = 0; i < len; i++) {
            area = Math.max(area,(right[i] - left[i] - 1) * heights[i]);
        }
        return area;
    }
}

参考:参考:https://leetcode-cn.com/problems/largest-rectangle-in-histogram/solution/javade-5chong-jie-fa-xiao-lu-zui-gao-de-ji-bai-lia/

 

同类题目还包括:

42. 接雨水
739. 每日温度
496. 下一个更大元素 I
316. 去除重复字母
901. 股票价格跨度
402. 移掉K位数字    
581. 最短无序连续子数组    

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值