算法的重要性,我就不多说了吧,想去大厂,就必须要经过基础知识和业务逻辑面试+算法面试。所以,为了提高大家的算法能力,这个号后续每天带大家做一道算法题,题目就从LeetCode上面选 !
今天和大家聊的问题叫做 柱状图中最大的矩形,我们先来看题面:
https://leetcode-cn.com/problems/largest-rectangle-in-histogram/
Given n non-negative integers representing the histogram's bar height where the width of each bar is 1, find the area of largest rectangle in the histogram.
题意
给定 n 个非负整数,用来表示柱状图中各个柱子的高度。每个柱子彼此相邻,且宽度为 1 。求在该柱状图中,能够勾勒出来的矩形的最大面积。
输入: [2,1,5,6,2,3]
输出: 10
解题
区间求最值
拿到手应该能感受到这题的难度,我们一上来的确没有什么太好的思路,题目也比较明确,没有太多可以分析的入手点。所以我们可以先来思考一下最简单的解法。最简单的解法就是找出能够围成的所有矩形,然后比较它们之间的面积,得出其中的最大面积。我们很容易可以想到可以遍历矩形的起始位置,这样就得到了矩形的宽。至于矩形的长也很简单,就是选定的这个区间段里的最低高度。





逆向思维
上面的一种思路虽然不太可行,但是它提供了一种正向思路。我们搜索所有的区间,然后通过区间里的木条确定区围成矩形的高度,就得到了矩形的面积。既然这条路走不通,我们能不能反向思考呢?我们假设我们找到了答案,它是区间[a, b]段的木条围成的矩形,它的高度是h。那么根据木桶效应,a到b区间段的木条当中一定有一根的长度是h。比如下图当中[5, 6, 2, 3]如果要围成矩形,那么高度只能是2。


class Solution:
def largestRectangleArea(self, heights: List[int]) -> int:
n = len(heights)
# 左侧边界初始化为0
left_side = [0 for i in range(n)]
# 右侧边界初始化为n-1
right_side = [n-1 for _ in range(n)]
stack_left = []
stack_right = []
for i in range(n):
h = heights[i]
# 弹出栈中所有比当前元素小的值
# 注意,栈内存储的是下标
while len(stack_right) > 0 and h < heights[stack_right[-1]]:
tail = stack_right[-1]
stack_right.pop()
right_side[tail] = i - 1
# 当前元素入栈
stack_right.append(i)
# 把坐标翻转,等价于逆向遍历
i_ = n - 1 - i
h = heights[i_]
# 维护单调栈的逻辑同上
while len(stack_left) > 0 and h < heights[stack_left[-1]]:
tail = stack_left[-1]
stack_left.pop()
left_side[tail] = i_ + 1
# 当前元素入栈
stack_left.append(i_)
ret = 0
for i in range(n):
# 矩形面积等于右侧边界-左侧边界+1 x 高度
cur = (right_side[i] - left_side[i] + 1) * heights[i]
ret = max(ret, cur)
return ret
总结
想要把这道题做出来,单单理清楚题意和单单会单调栈都是没有用的。既需要理清楚题意,从最简单的解法出发推导出优化的方法,也需要深刻理解单调栈这个数据结构,才可以灵活应用。另外,在代码当中需要特别注意边界的情况。比如初始化时左右边界的设定,以及可能会出现连续相等元素的情况,这些都需要纳入考虑。代码虽然看起来简单,但是隐藏了很多细节,所以只看代码是没用的,最好还是能亲自实现一下。好了,今天的文章就到这里。