此处的其他答案在使用两个堆栈表示O(n)时间,O(n)空间解决方案方面做得很好。关于此问题还有另一种观点,可以独立地为该问题提供O(n)时间,O(n)空间解决方案,并且可以提供更多有关基于堆栈的解决方案为何起作用的见解。
关键思想是使用称为笛卡尔树的数据结构。笛卡尔树是二叉树结构(尽管不是二叉树搜索围绕输入数组构建树)。具体来说,笛卡尔树的根是在数组的最小元素之上构建的,左子树和右子树是从子数组到最小值的左和右递归构造的。
例如,这是一个示例数组及其笛卡尔树:
+----------------------- 23 ------+
| |
+------------- 26 --+ +-- 79
| | |
31 --+ 53 --+ 84
| |
41 --+ 58 -------+
| |
59 +-- 93
|
97
+----+----+----+----+----+----+----+----+----+----+----+
| 31 | 41 | 59 | 26 | 53 | 58 | 97 | 93 | 23 | 84 | 79 |
+----+----+----+----+----+----+----+----+----+----+----+
笛卡尔树在此问题中有用的原因是手头的问题具有非常好的递归结构。首先查看直方图中的最低矩形。对于可以放置最大矩形的位置,有三个选项:
它可以在直方图的最小值下方通过。在这种情况下,要使其尽可能大,我们希望使其与整个阵列一样宽。
它可能完全在最小值的左侧。在这种情况下,我们递归地希望从子数组形成的答案纯粹在最小值的左侧。
它可能完全在最小值的右边。在这种情况下,我们递归地希望从子数组形成的答案纯粹在最小值的右边。
请注意,此递归结构-找到最小值,对该值的左右两边的子数组进行操作-完全匹配笛卡尔树的递归结构。实际上,如果我们可以在开始时为整个数组创建笛卡尔树,则可以通过从根向下递归遍历笛卡尔树来解决此问题。在每个点上,我们递归地计算左右子数组中的最佳矩形,以及通过将最小值拟合右而得到的矩形,然后返回找到的最佳选项。
在伪代码中,如下所示:
function largestRectangleUnder(int low, int high, Node root) {
/* Base case: If the range is empty, the biggest rectangle we
* can fit is the empty rectangle.
*/
if (low == high) return 0;
/* Assume the Cartesian tree nodes are annotated with their
* positions in the original array.
*/
return max {
(high - low) * root.value, // Widest rectangle under the minimum
largestRectangleUnder(low, root.index, root.left),
largestRectnagleUnder(root.index + 1, high, root.right)
}
}
一旦有了笛卡尔树,该算法将花费时间O(n),因为我们只对每个节点进行一次访问,并为每个节点执行O(1)工作。
事实证明,有一个简单的线性时间算法可以构建笛卡尔树。您可能会想构建一个“自然”的方法是扫描阵列,找到最小值,然后从左右两个子阵列递归地构建笛卡尔树。问题在于,找到最小值的过程确实非常昂贵,这可能需要花费时间Θ(n 2)。
建立笛卡尔树的“快速”方法是从左到右扫描数组,一次添加一个元素。该算法基于关于笛卡尔树的以下观察结果:
首先,笛卡尔树遵循heap属性:每个元素都小于或等于其子元素。这样做的原因是,笛卡尔树的根是整个阵列中的最小值,其子女中最小的元素及其子阵等
其次,如果对笛卡尔树进行有序遍历,则将按其出现的顺序取回数组的元素。要了解为什么会这样,请注意,如果对笛卡尔树进行有序遍历,则首先要访问最小值左侧的所有内容,然后是最小值,然后是最小值右侧的所有内容。这些访问以相同的方式递归进行,因此所有内容最终都会按顺序访问。
这两个规则为我们提供了很多信息,如果我们从数组的前k个元素的笛卡尔树开始并且想要为前k + 1个元素形成笛卡尔树,将会发生什么。这个新元素必须终止于笛卡尔树的右脊上-该树的一部分是从根开始并仅向右迈出的步伐形成的-因为否则,将有序地遍历它。并且,在该正确的书脊中,必须以使其比其上方的所有内容都大的方式放置它,因为我们需要遵守heap属性。
实际将新节点添加到笛卡尔树的方式是从树中最右边的节点开始并向上走,直到您击中树的根或找到具有较小值的节点为止。然后,使新值的最后一个节点作为其左子节点。
这是在一个小数组上的该算法的痕迹:
+---+---+---+---+
| 2 | 4 | 3 | 1 |
+---+---+---+---+
2成为根。
2 --+
|
4
4大于2,我们不能向上移动。追加到右边。
+---+---+---+---+
| 2 | 4 | 3 | 1 |
+---+---+---+---+
2 ------+
|
--- 3
|
4
3小于4,爬过去。不能再超过2,因为它小于3。爬到以4为根的子树上到达新值3的左侧,现在3成为最右边的节点。
+---+---+---+---+
| 2 | 4 | 3 | 1 |
+---+---+---+---+
+---------- 1
|
2 ------+
|
--- 3
|
4
1从根2上方爬过,将以2为根的整个树移到1的左侧,现在1是新的根-也是最右边的值。
+---+---+---+---+
| 2 | 4 | 3 | 1 |
+---+---+---+---+
尽管这似乎不是线性运行的,但您难道不会最终一遍又一遍地爬到树的根部吗?-您可以使用一个聪明的参数证明它可以线性运行。如果您在插入过程中爬上右脊椎中的一个节点,那么该节点最终会移出右脊椎,因此在以后的插入中将无法重新扫描。因此,每个节点最多只能扫描一次,因此完成的总工作是线性的。
现在,更重要的是-实际实施此方法的标准方法是维护与右主干上的节点相对应的值的堆栈。“越过”节点上方的动作对应于将节点弹出堆栈。因此,用于构建笛卡尔树的代码如下所示:
Stack s;
for (each array element x) {
pop s until it's empty or s.top > x
push x onto the stack.
do some sort of pointer rewiring based on what you just did.
}
这里的堆栈操作可能看起来确实很熟悉,这是因为这些是您在此处其他地方显示的答案中将要执行的确切堆栈操作。实际上,您可以将这些方法视为隐式构建笛卡尔树并在执行过程中运行上面显示的递归算法。
我认为了解笛卡尔树的好处是,它提供了一个非常好的概念框架,可以了解该算法为何正常工作。如果您知道自己正在执行的是笛卡尔树的递归遍历,那么可以很容易地看到可以保证找到最大的矩形。另外,知道笛卡尔树的存在为您提供了解决其他问题的有用工具。笛卡尔树出现在快速数据结构的设计中,用于范围最小查询问题,并用于将后缀数组转换为后缀树。
这是实现此想法的一些Java代码,由@Azeem提供!
import java.util.Stack;
public class CartesianTreeMakerUtil {
private static class Node {
int val;
Node left;
Node right;
}
public static Node cartesianTreeFor(int[] nums) {
Node root = null;
Stack<Node> s = new Stack<>();
for(int curr : nums) {
Node lastJumpedOver = null;
while(!s.empty() && s.peek().val > curr) {
lastJumpedOver = s.pop();
}
Node currNode = this.new Node();
currNode.val = curr;
if(s.isEmpty()) {
root = currNode;
}
else {
s.peek().right = currNode;
}
currNode.left = lastJumpedOver;
s.push(currNode);
}
return root;
}
public static void printInOrder(Node root) {
if(root == null) return;
if(root.left != null ) {
printInOrder(root.left);
}
System.out.println(root.val);
if(root.right != null) {
printInOrder(root.right);
}
}
public static void main(String[] args) {
int[] nums = new int[args.length];
for (int i = 0; i < args.length; i++) {
nums[i] = Integer.parseInt(args[i]);
}
Node root = cartesianTreeFor(nums);
tester.printInOrder(root);
}
}