Title
给定 n 个非负整数,用来表示柱状图中各个柱子的高度。每个柱子彼此相邻,且宽度为 1 。
求在该柱状图中,能够勾勒出来的矩形的最大面积。
以上是柱状图的示例,其中每个柱子的宽度为 1,给定的高度为 [2,1,5,6,2,3]。
图中阴影部分为所能勾勒出的最大矩形面积,其面积为 10 个单位。
示例:
输入: [2,1,5,6,2,3]
输出: 10
Solve
Violence
暴力的方法就比较好写了,直接枚举矩形的宽和高,其中宽表示矩形贴着柱状图底边的宽度,高表示矩形在柱状图上的高度。
双重循环枚举矩形的左右边界以固定宽度w,高度为所有包含在内柱子的最小高度h,对应的面积为w * h:
def largestRectangleArea_Violence_Width(self, heights: List[int]) -> int:
ans, length = 0, len(heights)
for i in range(length):
minHeight = max(heights)
for j in range(i, length):
minHeight = min(minHeight, heights[j])
ans = max((j - i + 1) * minHeight, ans)
return ans
还有一种暴力枚举的方法,我们使用两层循环枚举宽,其实可以通过一层循环枚举高,将其固定为矩形的高度h,然后从这根柱子开始向两侧延伸,直到遇到高度小于h的柱子,就确定了矩形的左右边界,即宽w,对应的面积就是w * h:
def largestRectangleArea_Violence_Height(self, heights: List[int]) -> int:
ans, length = 0, len(heights)
for middle in range(length):
height = heights[middle]
left, right = middle, middle
while left - 1 > -1 and heights[left - 1] >= height:
left -= 1
while right + 1 < length and heights[right + 1] >= height:
right += 1
ans = max(ans, (right - left + 1) * height)
return ans
两种暴力枚举方法的时间复杂度均为O(N2),会超出时间显示,枚举宽度的方法使用了两层循环,没有什么优化的空间了,所以只能优化枚举高度的一层循环。
单调栈
如果有两根柱子j0和j1,其中j0在j1的左侧,并且j0的高度大于等于j1,那么在后面的柱子i向左找小于其高度柱子时,j1会挡住j0。
这样以来,可以对数组从左向右进行遍历,同时维护一个可能答案的数据结构,其中按照从小到大的顺序存放一些j值,如果我们存放了j0,j1,…,js,一定有heights[j0]<heights[j1]<…<heights[js]。
当我们枚举到第i根柱子时,数据结构中存放了j0,j1,…,js,如果第i根柱子左侧且最近的小于其高度的柱子为ji,那么必然有:
h e i g h t [ j 0 ] < h e i g h t [ j 1 ] < ⋯ < h e i g h t [ j i ] < h e i g h t [ i ] ≤ h e i g h t [ j i + 1 ] < ⋯ < h e i g h t [ j s ] height[j_0]<height[j_1]<⋯<height[j_i]<height[i]≤height[j_{i+1}]<⋯<height[j_s] height[j0]<height[j1]<⋯<height[ji]<height[i]≤height[ji+1]<⋯<height[js]
当我们枚举到i+1时,原来的i也变成了j值,因此i会被放入数据结构。由于所有在数据结构中的j值均小于i,那么所有高度大于等于height[i]的j都不会作为答案,需要从数据结构中移除,恰好就是ji+1,…,js。
这样我们在枚举到第 i 根柱子的时候,就可以先把所有高度大于等于 height[i] 的 j 值全部移除,剩下的 j 值中高度最高的即为答案。
在这之后,我们将 i 放入数据结构中,开始接下来的枚举。此时,我们需要使用的数据结构也就呼之欲出了,它就是栈。
- 栈中存放了 j 值。从栈底到栈顶,j 的值严格单调递增,同时对应的高度值也严格单调递增;
- 当我们枚举到第 i 根柱子时,我们从栈顶不断地移除 height[j]≥height[i] 的 j 值。在移除完毕后,栈顶的 j 值就一定满足 height[j]<height[i],此时 j 就是 i 左侧且最近的小于其高度的柱子。
- 如果我们移除了栈中所有的 j 值,那就说明 i 左侧所有柱子的高度都大于 height[i],可以认为 i 左侧且最近的小于其高度的柱子在位置 j=-1,一根「虚拟」的、高度无限低的柱子。
- 再将 ii 放入栈顶
栈中存放的元素具有单调性,这就是经典的数据结构「单调栈」了。
例子:[6,7,5,2,4,5,9,3]
初始时的栈为空。
6:因为栈为空,所以 6 左侧的柱子是「哨兵」,位置为 -1。随后我们将 6 入栈。
- 栈:[6(0)]。(这里括号内的数字表示柱子在原数组中的位置)
7:由于 6<7,因此不会移除栈顶元素,所以 6 左侧的柱子是 7,位置为 0。随后我们将 7 入栈。
- 栈:[6(0), 7(1)]
5:由于 7≥5,因此移除栈顶元素 7。同样地,6≥5,再移除栈顶元素 6。此时栈为空,所以 5 左侧的柱子是「哨兵」,位置为 −1。随后我们将 5 入栈。
- 栈:[5(2)]
2:移除栈顶元素 5,得到 2 左侧的柱子是「哨兵」,位置为 −1。将 2 入栈。
- 栈:[2(3)]
4,5 和 9:不会移除任何栈顶元素,得到它们左侧的柱子分别是 2,4 和 5,位置分别为 3,4 和 5。将它们入栈。
- 栈:[2(3), 4(4), 5(5), 9(6)]
3:依次移除栈顶元素 9,5 和 4,得到 3 左侧的柱子是 2,位置为 3。将 3 入栈。
- 栈:[2(3), 3(7)]
这样以来,我们得到它们左侧的柱子编号分别为 [-1, 0, -1, -1, 3, 4, 5, 3]。用相同的方法,我们从右向左进行遍历,也可以得到它们右侧的柱子编号分别为 [2, 2, 3, 8, 7, 7, 7, 8],这里我们将位置 8 看作「哨兵」。
每一个位置只会入栈一次(在枚举到它时),并且最多出栈一次。因此当我们从左向右/总右向左遍历数组时,对栈的操作的次数就为 O(N)。所以单调栈的总时间复杂度为 O(N)。
Code
def largestRectangleArea_MonotoneStack(self, heights: List[int]) -> int:
length = len(heights)
left, right = [0] * length, [0] * length
monotoneStack = list()
for i in range(length):
while monotoneStack and heights[monotoneStack[-1]] >= heights[i]:
monotoneStack.pop()
left[i] = monotoneStack[-1] if monotoneStack else -1
monotoneStack.append(i)
monotoneStack = list()
for i in range(length - 1, -1, -1):
while monotoneStack and heights[monotoneStack[-1]] >= heights[i]:
monotoneStack.pop()
right[i] = monotoneStack[-1] if monotoneStack else length
monotoneStack.append(i)
ans = max((right[i] - left[i] - 1) * heights[i] for i in range(length)) if length > 0 else 0
return ans