题目地址:
https://leetcode.com/problems/largest-rectangle-in-histogram/
以每个位置的高度给定表示一个直方图的数组 A A A,要求返回其所含的最大矩阵的面积。这个矩阵必须完全含在直方图内(边界重合是可以的),并且要求底边与下沿边重合。
法1:单调栈。 A [ i ] A[i] A[i]为高的最大矩形的面积取决于其两边离它最近的严格比它小的数的位置在哪里。这启发我们用单调栈来做。我们维护一个单调增的栈,由于需要用到下标,我们栈里存储的是下标,但是排序是按照数组 A A A来排序的。当一个下标出栈的时候,这个下标对应的数 x x x的左边第一个比它小的数就是栈顶,右边第一个比它小的数就是当前遍历的数,这样就能求出以 x x x为高的最大矩形的面积了。由于有可能出栈的时候栈空了,我们先将 − 1 -1 −1进栈(相当于 A [ − 1 ] = 0 A[-1]=0 A[−1]=0,也就是下标 − 1 -1 −1的地方有一个高度为 0 0 0的柱子),然后再遍历数组。这样计算矩形面积的时候公式是统一的。当数组遍历完毕后,如果栈内还有元素,说明这些数的右边没有比它严格小的数,而只有左边有,此时 A [ l ] = 0 A[l]=0 A[l]=0, l l l为 A A A的长度,也就是想象在 l l l的地方有一个高度为 0 0 0的柱子,按照相同的手法求一遍栈内元素为高度的最大矩形面积即可。代码如下:
import java.util.Deque;
import java.util.LinkedList;
public class Solution {
public int largestRectangleArea(int[] heights) {
if (heights == null || heights.length == 0) {
return 0;
}
int res = 0;
// 维护单调递增的栈
Deque<Integer> stack = new LinkedList<>();
stack.push(-1);
for (int i = 0; i < heights.length; i++) {
while (stack.peek() != -1 && heights[stack.peek()] > heights[i]) {
int top = stack.pop();
// i是top右边第一个比它矮的下标,栈顶是top左边第一个比它矮的下标
res = Math.max(res, (i - stack.peek() - 1) * heights[top]);
}
stack.push(i);
}
while (stack.peek() != -1) {
int top = stack.pop();
res = Math.max(res, (heights.length - 1 - stack.peek()) * heights[top]);
}
return res;
}
}
时空复杂度 O ( n ) O(n) O(n)。
注解:
这里的单调严格不严格是不打紧的,都能保证答案正确,主要原因是,如果某个高度连续出现了若干次,当最后一次它出栈的时候,以它为高的最大矩形面积就被确定了,会更新res。
法2:笛卡儿树。由于这一题本质上是要快速的求出某个
A
[
i
]
A[i]
A[i]左右两边连续的大于等于它的数有多少个,这个个数就是以
A
[
i
]
A[i]
A[i]为高度的最大矩形的底边长。而笛卡儿树恰好可以解决这个问题。试想有这样的一棵树,其树根为全数组的最小值,其左孩子是其左边的所有数字的最小值,其右孩子是其右边子树的最小值,并且左右子树满足相同的性质。这样将所有的数建成一棵树,容易看出其有堆的性质,这样每个节点
x
x
x的左子树的大小即为连续的一段比
x
x
x大的数的个数,每个节点
x
x
x的右子树的大小即为连续的一段比
x
x
x大的数的个数。见下图:
若
f
(
x
)
f(x)
f(x)表示
x
x
x为根的子树的节点个数,那么以
x
x
x为高的最大面积其实就是
(
f
(
x
.
l
)
+
f
(
x
.
r
)
+
1
)
∗
x
(f(x.l)+f(x.r)+1)*x
(f(x.l)+f(x.r)+1)∗x,也就是其左子树节点个数加右子树节点个数加
1
1
1,再乘以树根的数值。一遍DFS即可求得。至于笛卡儿树的建树,可以用单调栈在
O
(
n
)
O(n)
O(n)时间内求得(参考https://blog.csdn.net/qq_46105170/article/details/108526277)。所以整个时间复杂度仍然是
O
(
n
)
O(n)
O(n)的。代码如下:
import java.util.Deque;
import java.util.LinkedList;
public class Solution {
class Node {
int val;
Node left, right;
public Node(int val) {
this.val = val;
}
}
private int res;
public int largestRectangleArea(int[] heights) {
Node root = buildCartesianTree(heights);
dfs(root);
return res;
}
// 对笛卡儿树进行DFS,按照公式求以当前节点为高度的最大矩形面积,同时更新res
private int dfs(Node cur) {
if (cur == null) {
return 0;
}
int l = dfs(cur.left), r = dfs(cur.right);
res = Math.max(res, (r + l + 1) * cur.val);
return 1 + l + r;
}
private Node buildCartesianTree(int[] nums) {
Deque<Node> stack = new LinkedList<>();
// 其实就是在维护笛卡儿树的右链
for (int i = 0; i < nums.length; i++) {
Node last = null;
while (!stack.isEmpty() && stack.peek().val > nums[i]) {
last = stack.pop();
}
Node cur = new Node(nums[i]);
cur.left = last;
if (!stack.isEmpty()) {
stack.peek().right = cur;
}
stack.push(cur);
}
// 树根就在栈底
return stack.peekLast();
}
}
时空复杂度 O ( n ) O(n) O(n)。
注解:
关于笛卡儿树的性质有以下这么几个:
1、它有堆性质,即每个节点是其为根的子树的最小值;
2、它的左子树根是其左边连续一段比它大的数中的最后一个,右子树根是其右边连续一段比它大的数中的最后一个;
3、它的每个节点的左子树的节点个数,是其左边连续的一段大于它的数的个数。它的每个节点的右子树的节点个数,是其右边连续的一段大于它的数的个数。