Другие ответы здесь проделали большую работу, представив решение O (n) -время, O (n) -пространстве с использованием двух стеков. Есть еще одна точка зрения на эту проблему, которая независимо обеспечивает решение проблемы в O (n) -времени, O (n) -пространстве и может дать немного больше понимания того, почему работает решение на основе стека.
Ключевой идеей является использование структуры данных, называемой декартовым деревом . Декартово дерево - это двоичная древовидная структура (но не бинарное дерево search ), построенная вокруг входного массива. В частности, корень декартового дерева строится над минимальным элементом массива, а левое и правое поддеревья рекурсивно строятся из подмассивов слева и справа от минимального значения.
Например, вот примерный массив и его декартово дерево:
+----------------------- 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) работу на узел.
Оказывается, существует простой линейный алгоритм построения декартовых деревьев. «Естественный» способ, которым вы, вероятно, подумаете построить его, будет сканировать массив, найти минимальное значение, а затем рекурсивно построить декартово дерево из левого и правого подмассивов. Проблема в том, что процесс поиска минимального значения действительно дорог, и это может занять время & theta; (n 2 ).
«Быстрый» способ построить декартово дерево - это сканировать массив слева направо, добавляя по одному элементу за раз. Этот алгоритм основан на следующих наблюдениях о декартовых деревьях:
Во-первых, декартовы деревья подчиняются свойству кучи: каждый элемент меньше или равен своим дочерним элементам. Причина этого заключается в том, что корень декартового дерева является наименьшим значением в общем массиве, а его дочерние элементы - наименьшими элементами в их подмассивах и т. Д.
Во-вторых, если вы выполняете обход по порядку декартового дерева, вы возвращаете элементы массива в порядке их появления. Чтобы понять, почему это так, обратите внимание, что если вы выполняете обход по декартовому дереву по порядку, сначала вы просматриваете все слева от минимального значения, затем минимальное значение, затем все справа от минимального значения. Эти посещения рекурсивно выполняются таким же образом, поэтому все заканчивается посещением по порядку.
Эти два правила дают нам много информации о том, что произойдет, если мы начнем с декартового дерева первых k элементов массива и захотим сформировать декартово дерево для первых k + 1 элементов.Этот новый элемент должен в конечном итоге оказаться в правом позвоночнике декартового дерева - части дерева, образованной путем начала у корня и принятия шагов только вправо - потому что в противном случае что-то последует за ним в обходе по порядку.И внутри этого правого отдела позвоночника его нужно разместить таким образом, чтобы он был больше, чем все, что находится над ним, поскольку нам нужно подчиняться свойству кучи.
Способ, которым вы фактически добавляете новый узел кДекартово дерево должно начинаться с самого правого узла дерева и идти вверх до тех пор, пока вы не коснетесь корня дерева или не найдете узел с меньшим значением.Затем вы делаете новое значение своим левым потомком, последним узлом, на котором оно поднялось.
Вот след этого алгоритма на небольшом массиве:
+---+---+---+---+
| 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);
}
}
Манипуляции со стеком здесьможет показаться очень знакомым, и это потому, что это именно те операции стека, которые вы выполняете в ответах, показанных в другом месте здесь. На самом деле, вы можете думать о том, что эти подходы делают как неявно 1091* построение декартового дерева и запуск рекурсивного алгоритма, показанного выше, в процессе этого.
Преимущество знания о декартовых деревьях, я думаю, состоит в том, что оно обеспечивает действительно хорошую концептуальную основу для понимания, почему этот алгоритмработает правильно.Если вы знаете, что вы выполняете рекурсивный обход декартового дерева, легче увидеть, что вы гарантированно найдете самый большой прямоугольник.Кроме того, знание того, что декартово дерево существует, дает вам полезный инструмент для решения других проблем.Декартовы деревья обнаруживаются при проектировании быстрых структур данных для задачи минимального запроса в диапазоне и используются для преобразования массивов суффиксов в деревья суффиксов .