Die anderen Antworten hier haben die O(n)-Zeit- und O(n)-Raum-Lösung mit zwei Stapeln gut dargestellt. Es gibt noch eine andere Perspektive auf dieses Problem, die unabhängig davon eine O(n)-Zeit- und O(n)-Raum-Lösung für das Problem bietet und vielleicht ein wenig mehr Einsicht darüber gibt, warum die stapelbasierte Lösung funktioniert.
Der Grundgedanke ist die Verwendung einer Datenstruktur namens Kartesischer Baum . Ein kartesischer Baum ist eine binäre Baumstruktur (allerdings keine binäre Suche Baum), der um ein Eingabefeld herum aufgebaut ist. Insbesondere wird die Wurzel des kartesischen Baums über dem minimalen Element des Arrays aufgebaut, und die linken und rechten Teilbäume werden rekursiv aus den Unterarrays links und rechts des minimalen Werts konstruiert.
Hier ein Beispiel für ein Array und seinen kartesischen Baum:
+----------------------- 23 ------+
| |
+------------- 26 --+ +-- 79
| | |
31 --+ 53 --+ 84
| |
41 --+ 58 -------+
| |
59 +-- 93
|
97
+----+----+----+----+----+----+----+----+----+----+----+
| 31 | 41 | 59 | 26 | 53 | 58 | 97 | 93 | 23 | 84 | 79 |
+----+----+----+----+----+----+----+----+----+----+----+
Der Grund, warum kartesische Bäume bei diesem Problem nützlich sind, ist, dass die vorliegende Frage eine wirklich schöne rekursive Struktur hat. Beginnen Sie damit, das unterste Rechteck im Histogramm zu betrachten. Es gibt drei Möglichkeiten, wo das maximale Rechteck am Ende platziert werden könnte:
-
Er könnte direkt unter dem Mindestwert im Histogramm liegen. In diesem Fall sollten wir ihn so breit wie das gesamte Array machen, um ihn so groß wie möglich zu machen.
-
Er könnte ganz links vom Mindestwert liegen. In diesem Fall wollen wir rekursiv die Antwort aus dem Subarray rein links vom Minimalwert gebildet haben.
-
Er könnte ganz rechts vom Mindestwert liegen. In diesem Fall wollen wir rekursiv die Antwort aus dem Subarray rein rechts vom Minimalwert gebildet haben.
Beachten Sie, dass diese rekursive Struktur - den Minimalwert finden, etwas mit den Unterfeldern links und rechts von diesem Wert tun - perfekt der rekursiven Struktur eines kartesischen Baums entspricht. Wenn wir zu Beginn einen kartesischen Baum für das gesamte Array erstellen können, können wir das Problem lösen, indem wir den kartesischen Baum von der Wurzel abwärts rekursiv durchlaufen. An jedem Punkt berechnen wir rekursiv das optimale Rechteck in den linken und rechten Subarrays, zusammen mit dem Rechteck, das man erhält, wenn man es direkt unter den Minimalwert einpasst, und geben dann die beste Option zurück, die wir finden.
In Pseudocode sieht das folgendermaßen aus:
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)
}
}
Sobald wir den kartesischen Baum haben, benötigt dieser Algorithmus O(n) Zeit, da wir jeden Knoten genau einmal besuchen und O(1) Arbeit pro Knoten erledigen.
Es stellt sich heraus, dass es einen einfachen Algorithmus mit linearer Zeit für den Aufbau kartesischer Bäume gibt. Die "natürliche" Art und Weise, einen solchen Baum zu erstellen, wäre, das Array zu scannen, den Minimalwert zu finden und dann rekursiv einen kartesischen Baum aus den linken und rechten Subarrays zu erstellen. Das Problem ist, dass die Suche nach dem Minimalwert sehr teuer ist und viel Zeit in Anspruch nehmen kann (n 2 ).
Der "schnelle" Weg, einen kartesischen Baum zu erstellen, besteht darin, das Array von links nach rechts zu durchsuchen und ein Element nach dem anderen hinzuzufügen. Dieser Algorithmus basiert auf den folgenden Beobachtungen über kartesische Bäume:
-
Erstens gehorchen kartesische Bäume der Haufeneigenschaft: Jedes Element ist kleiner oder gleich seinen Kindern. Der Grund dafür ist, dass die Wurzel des kartesischen Baums der kleinste Wert im gesamten Array ist, und seine Kinder sind die kleinsten Elemente in ihre Subarrays, usw.
-
Zweitens: Wenn Sie einen kartesischen Baum in Reihenfolge durchlaufen, erhalten Sie die Elemente des Arrays in der Reihenfolge zurück, in der sie erscheinen. Um zu sehen, warum das so ist, beachten Sie, dass Sie bei einer inorder Traversierung eines kartesischen Baumes zuerst alles links vom Minimalwert besuchen, dann den Minimalwert, dann alles rechts vom Minimalwert. Diese Besuche werden rekursiv auf die gleiche Weise durchgeführt, so dass am Ende alles in der richtigen Reihenfolge besucht wird.
Diese beiden Regeln geben uns eine Menge Informationen darüber, was passiert, wenn wir mit einem kartesischen Baum der ersten k Elemente des Arrays beginnen und einen kartesischen Baum für die ersten k+1 Elemente bilden wollen. Das neue Element muss auf dem rechten Rückgrat des kartesischen Baums landen - dem Teil des Baums, der gebildet wird, indem man bei der Wurzel beginnt und nur Schritte nach rechts macht -, weil sonst etwas in einer inorder traversal nachkommen würde. Und innerhalb dieses rechten Rückgrats muss er so platziert werden, dass er größer ist als alles, was über ihm liegt, da wir die Eigenschaft des Haufens beachten müssen.
Um einen neuen Knoten in den kartesischen Baum einzufügen, beginnt man am äußersten rechten Knoten des Baums und wandert nach oben, bis man entweder die Wurzel des Baums erreicht oder einen Knoten mit einem kleineren Wert findet. Der neue Wert hat dann als linkes Kind den letzten Knoten, über den er nach oben gewandert ist.
Hier ist ein Beispiel für diesen Algorithmus auf einem kleinen Array:
+---+---+---+---+
| 2 | 4 | 3 | 1 |
+---+---+---+---+
2 wird die Wurzel.
2 --+
|
4
4 ist größer als 2, wir können uns nicht nach oben bewegen. Nach rechts anhängen.
+---+---+---+---+
| 2 | 4 | 3 | 1 |
+---+---+---+---+
2 ------+
|
--- 3
|
4
3 ist kleiner als 4, klettere darüber. Kann nicht weiter über 2 klettern, da sie kleiner als 3 ist. Der überkletterte Teilbaum, der in 4 verwurzelt ist, geht nach links zum neuen Wert 3, und 3 wird jetzt zum ganz rechten Knoten.
+---+---+---+---+
| 2 | 4 | 3 | 1 |
+---+---+---+---+
+---------- 1
|
2 ------+
|
--- 3
|
4
1 klettert über die Wurzel 2, der gesamte Baum mit der Wurzel 2 wird nach links von 1 verschoben, und 1 ist nun die neue Wurzel - und auch der Wert ganz rechts.
+---+---+---+---+
| 2 | 4 | 3 | 1 |
+---+---+---+---+
Auch wenn es nicht so aussieht, als würde dies in linearer Zeit ablaufen - würde man nicht möglicherweise den ganzen Weg zur Wurzel des Baumes immer und immer wieder zurücklegen? - kann man mit einem cleveren Argument zeigen, dass dies in linearer Zeit abläuft. Wenn Sie während einer Einfügung über einen Knoten in der rechten Wirbelsäule klettern, wird dieser Knoten schließlich aus der rechten Wirbelsäule verschoben und kann daher bei einer zukünftigen Einfügung nicht erneut gescannt werden. Daher wird jeder Knoten höchstens einmal gescannt, so dass die Gesamtarbeit linear ist.
Und jetzt kommt der Clou: Die Standardmethode, mit der Sie diesen Ansatz umsetzen, besteht darin, einen Stapel mit den Werten zu führen, die den Knoten auf der rechten Wirbelsäule entsprechen. Wenn man über einen Knoten geht, wird ein Knoten vom Stapel genommen. Daher sieht der Code zum Aufbau eines kartesischen Baums etwa so aus:
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.
}
Die Stapelmanipulationen hier mögen wirklich misslungen erscheinen, und das liegt daran, dass Dies sind genau die Stapeloperationen, die Sie auch in den hier gezeigten Antworten durchführen würden. Man kann sich das, was diese Ansätze bewirken, wie folgt vorstellen implizit den kartesischen Baum aufbauen und dabei den oben gezeigten rekursiven Algorithmus ausführen.
Der Vorteil der Kenntnis kartesischer Bäume liegt meiner Meinung nach darin, dass sie einen sehr guten konzeptionellen Rahmen für die korrekte Funktionsweise dieses Algorithmus bieten. Wenn man weiß, dass man einen rekursiven Spaziergang durch einen kartesischen Baum macht, ist es einfacher zu erkennen, dass man garantiert das größte Rechteck finden wird. Außerdem ist das Wissen um die Existenz des kartesischen Baums ein nützliches Hilfsmittel für die Lösung anderer Probleme. Kartesische Bäume tauchen bei der Entwicklung von schnellen Datenstrukturen für die Bereich-Minimum-Abfrage-Problem und werden verwendet, um die Suffix-Arrays in Suffixbäume .
Hier ist ein Java-Code, der diese Idee umsetzt, mit freundlicher Genehmigung von @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);
}
}