Largest Rectangle in Histogram
Histogram to rząd przylegających do siebie słupków bez przerw, każdy o szerokości jednej jednostki: heights[i] to wysokość słupka i. Prostokąt w histogramie obejmuje ciąg sąsiadujących słupków, a jego wysokość nie może być większa niż wysokość najniższego słupka w tym ciągu.
Zwróć największe pole, jakie może mieć taki prostokąt.
Funkcja
- heightsinteger-array
- wysokość każdego słupka, od lewej do prawej
- Zwracainteger
- pole największego prostokąta, który mieści się w histogramie
Ograniczenia
1 ≤ heights.length ≤ 2 × 1040 ≤ heights[i] ≤ 105- Każdy słupek ma szerokość jednej jednostki, więc prostokąt obejmujący słupki od
idojma szerokośćj-i+1jednostek.
Przykłady
- Wejście
- heights = [2, 5, 6, 3, 4, 1]
- Wyjście
- 12
- Wyjaśnienie
- Wszystkie cztery słupki o wysokości 5, 6, 3 i 4 mają wysokość co najmniej 3, więc prostokąt o wysokości 3 obejmuje je wszystkie: 3 × 4 = 12. Dwa najwyższe słupki, 5 i 6, dają tylko 5 × 2 = 10.
- Wejście
- heights = [1, 8, 1, 1]
- Wyjście
- 8
- Wyjaśnienie
- Sam pasek o szerokości 8 daje 8 × 1 = 8. Każdy szerszy prostokąt zawiera pasek o szerokości 1, więc jego pole wynosi co najwyżej 1 × 4 = 4.
- Wejście
- heights = [3, 3, 3, 3]
- Wyjście
- 12
- Wyjaśnienie
- Wszystkie cztery słupki mają wysokość 3, więc cały histogram to jeden prostokąt: 3 × 4 = 12.
+17 ukrytych testów przy wysłaniu
Pytanie dodatkowe
Załóżmy, że każdy słupek ma własną szerokość, podaną w drugiej tablicy. Co się zmienia w rozwiązaniu jednokrotnego przejścia ze stosem?
Podpowiedzi
Otwieraj je po kolei. Każda zdradza trochę więcej.
Największy prostokąt dotyka górnej krawędzi co najmniej jednego znajdującego się pod nim słupka: gdyby tak nie było, można by go powiększyć. Spróbuj więc potraktować każdy słupek jako ten, który wyznacza wysokość. Jak szeroki może być prostokąt o dokładnie takiej wysokości?
Prostokąt o wysokości słupka
irozciąga się w lewo i w prawo, aż napotka po obu stronach ściśle niższy słupek. Jeśli znasz najbliższy niższy słupek po obu stronach każdego słupka, każdy słupek daje jeden obszar kandydujący, a jest ich tylkon.Przechowuj stos indeksów, których wysokości rosną od dołu do góry. Gdy pojawi się słupek, który nie jest wyższy od słupka na szczycie stosu, ten ostatni słupek nie może sięgnąć dalej w prawo: zdejmij go ze stosu, a jego prostokąt obejmuje słupki znajdujące się ściśle między nowym szczytem stosu a bieżącym słupkiem. Słupek o wysokości 0, dodany po końcu, zdejmie ze stosu wszystkie pozostałe elementy.
Rozwiązanie
Prostokąt może zaczynać się i kończyć przy dowolnym słupku, a jego wysokość zależy od najniższego słupka, który obejmuje, więc sprawdzanie każdego zakresu słupków wymaga około n²/2 kroków. Rozwiązaniem jest odwrócenie pytania: najlepszy prostokąt ma dokładnie taką wysokość jak jeden ze słupków, więc każdy słupek musi tylko ustalić, jak daleko może sięgać, zanim zatrzyma go niższy słupek. Stos monotoniczny znajduje te punkty zatrzymania dla każdego słupka — najpierw w dwóch przebiegach, a potem w jednym.
Wypróbuj każde uruchomienie, śledząc minimum
Poprawne, ale nie kończy się na największych testach
Intuicja
Prostokąt obejmuje ciąg sąsiadujących słupków od start do end, a jego wysokość jest ograniczona przez najniższy słupek w tym ciągu. Sprawdź więc każdy ciąg. Ustal start, a następnie wydłużaj ciąg, zwiększając end o jeden słupek i zapamiętując najniższą dotąd wysokość. Najlepszy prostokąt dla tego ciągu ma pole lowest × (end-start+1).
W ciągu [2, 5, 6, 3, 4, 1] zacznij od 5. Dla kolejnych ciągów otrzymujemy 5 × 1 = 5, następnie 5 × 2 = 10 po dodaniu 6, potem 3 × 3 = 9 po dołączeniu 3, 3 × 4 = 12 po dodaniu 4 i 1 × 5 = 5 po dodaniu 1. Odpowiedzią jest 12. Aktualizowanie lowest podczas wydłużania ciągu sprawia, że każdy krok zajmuje O(1), więc nie musisz ponownie przeszukiwać ciągu, by znaleźć jego minimum.
To rozwiązanie jest poprawne, ponieważ każdy prostokąt obejmuje jakiś ciąg, a dla ustalonego ciągu najwyższy pasujący prostokąt ma dokładnie wysokość najniższego słupka. Jest powolne, ponieważ istnieje n(n+1)/2 ciągów: około 2 × 10^8 dla 2 × 10^4 słupków, a ich liczba w ogóle nie zależy od wysokości. Większość tych ciągów zostaje ograniczona przez niski słupek na długo przed końcem, ale metoda brute force i tak nadal je wydłuża.
Algorytm
- Ustaw
bestna 0. - Dla każdego
startustawlowestnaheights[start]. - Dla każdego
endodstartdo ostatniego słupka zmniejszlowestdoheights[end], jeśli ten słupek jest niższy. - Zaktualizuj
best, używająclowest × (end-start+1). - Zwróć
best.
def largestRectangleArea(heights):
n = len(heights)
best = 0
for start in range(n):
lowest = heights[start]
for end in range(start, n):
# The rectangle over start..end is as tall as the lowest bar in it.
lowest = min(lowest, heights[end])
best = max(best, lowest * (end - start + 1))
return bestNajbliższy krótszy słupek po każdej stronie
Intuicja
Odwróćmy sposób szukania. W najlepszym prostokącie co najmniej jeden słupek pod nim ma dokładnie taką samą wysokość jak prostokąt; w przeciwnym razie można by podnieść prostokąt. Odpowiedzią jest więc najlepszy spośród wszystkich słupków i prostokąt o wysokości dokładnie heights[i], rozciągnięty na maksymalną szerokość. Rozciąga się on do napotkania po każdej stronie słupka ściśle niższego. Nazwijmy ich indeksy left[i] i right[i], używając -1 i n, gdy nie ma takiego słupka. Prostokąt obejmuje słupki leżące ściśle między nimi: szerokość right[i]-left[i]-1. Daje to n kandydatów zamiast n²/2.
Aby znaleźć left[i] dla każdego słupka, przejdź od lewej do prawej, używając stosu indeksów, których wysokości rosną ściśle od dołu do góry. Gdy pojawi się słupek i, zdejmuj ze stosu każdy indeks, którego słupek jest co najmniej tak wysoki jak heights[i]. Te słupki nigdy nie mogą być najbliższymi niższymi słupkami dla i ani dla żadnego kolejnego słupka, ponieważ i jest bliżej i nie jest wyższy. To, co pozostanie na szczycie stosu, jest najbliższym niższym słupkiem po lewej. Następnie dodaj i na stos. Ten sam przebieg od prawej do lewej wyznacza right[i].
Dla [2, 5, 6, 3, 4, 1] przebiegi dają left = [-1, 0, 1, 0, 3, -1] i right = [5, 3, 3, 5, 5, 6]. Słupek o wysokości 3 na indeksie 3 jest ograniczony przez słupek o wysokości 2 na indeksie 0 i słupek o wysokości 1 na indeksie 5, więc jego prostokąt ma pole 3 × (5-0-1) = 12. Słupek o wysokości 6 jest ograniczony przez sąsiadów i daje tylko 6 × 1.
Każdy indeks jest dodawany na stos raz i zdejmowany z niego co najwyżej raz w każdym przebiegu, więc oba przebiegi mają złożoność O(n), mimo że jeden słupek może spowodować zdjęcie wielu elementów ze stosu. Kosztem są dwie dodatkowe tablice.
Algorytm
- Przejdź od lewej do prawej z pustym stosem. Dla każdego
izdejmuj elementy ze stosu, dopóki słupek na wierzchu jest co najmniej tak wysoki jakheights[i]; ustawleft[i]na wartość ze szczytu stosu lub na -1, jeśli stos jest pusty; dodajina stos. - Przejdź od prawej do lewej w ten sam sposób, aby wypełnić
right[i], używającndla pustego stosu. - Dla każdego
iobliczheights[i] × (right[i]-left[i]-1). - Zwróć największe z tych pól.
def largestRectangleArea(heights):
n = len(heights)
left = [-1] * n # index of the nearest shorter bar on the left, or -1
right = [n] * n # index of the nearest shorter bar on the right, or n
stack = [] # indices whose heights rise strictly from bottom to top
for i in range(n):
while stack and heights[stack[-1]] >= heights[i]:
stack.pop()
if stack:
left[i] = stack[-1]
stack.append(i)
stack = []
for i in range(n - 1, -1, -1):
while stack and heights[stack[-1]] >= heights[i]:
stack.pop()
if stack:
right[i] = stack[-1]
stack.append(i)
best = 0
for i in range(n):
# Bar i is the lowest bar of everything strictly between left[i] and right[i].
best = max(best, heights[i] * (right[i] - left[i] - 1))
return bestJedno przejście z użyciem stosu monotonicznego
Intuicja
Przebieg od lewej do prawej już napotyka każdą prawą granicę; odrzuca ją. Gdy słupek i zdejmuje ze stosu słupek t, heights[i] nie jest większe niż heights[t], więc i to miejsce, w którym prostokąt słupka t kończy się z prawej strony. Indeks znajdujący się na stosie bezpośrednio pod t to miejsce, w którym kończy się z lewej strony. Zatem zmierz prostokąt w chwili zdejmowania słupka ze stosu: heights[t] × (i - below - 1), gdzie below to nowy szczyt stosu albo -1, jeśli stos jest teraz pusty.
Niezmiennik: wysokości na stosie rosną ściśle od dołu do góry, a indeks pod każdym wpisem wskazuje najbliższy słupek po jego lewej stronie, który jest od niego niższy. Każdy słupek między nimi został zdjęty ze stosu po drodze, albo przez ten wpis, albo przez słupek, który ten wpis zdjął później, więc żaden z nich nie jest niższy od tego wpisu. Słupki, które nigdy nie zostaną zdjęte ze stosu, sięgają aż do końca, więc po przetworzeniu ostatniego słupka przetwórz jeszcze jeden słupek o wysokości 0. Jest niższy od wszystkich i opróżnia stos.
Prześledźmy [2, 5, 6, 3, 4, 1]. Wstaw 2, 5 i 6: stos zawiera indeksy [0, 1, 2]. Wartość 3 na indeksie 3 zdejmuje ze stosu 6 (pole 6 × (3-1-1) = 6) i 5 (pole 5 × (3-0-1) = 10), po czym zatrzymuje się na 2 i zostaje dodana na stos. Dodaj 4. Wartość 1 na indeksie 5 zdejmuje ze stosu 4 (pole 4), a następnie 3, którego prostokąt rozciąga się od indeksu 1 do 4: 3 × (5-0-1) = 12. Zdejmuje też 2 ze stosu (2 × 5 = 10; stos jest pusty, więc szerokość wynosi 5). Końcowe 0 zdejmuje ze stosu 1 (1 × 6 = 6). Największe pole wynosi 12.
Zdejmowanie ze stosu przy warunku >= oznacza, że słupek o tej samej wysokości może przedwcześnie zatrzymać inny słupek. To bezpieczne: słupek o tej samej wysokości zajmuje jego miejsce na stosie, dziedziczy tę samą lewą granicę, a gdy zostanie później zdjęty ze stosu, jego prostokąt obejmie cały ciąg. Dla [3, 3, 3, 3] pierwsze trzy słupki o wysokości 3 wyznaczają szerokości 1, 2 i 3, a ostatni zostaje zdjęty ze stosu przez końcowe 0 przy szerokości 4, co daje 12.
Algorytm
- Rozpocznij od pustego stosu indeksów i
best = 0. - Dla
iod 0 donprzyjmij, że bieżąca wysokość wynosiheights[i]lub 0, gdyi = n. - Gdy słupek na szczycie stosu jest co najmniej tak wysoki jak bieżąca wysokość, zdejmij go jako
t; szerokość wynosii - below - 1, gdziebelowto nowy szczyt stosu lub -1; zaktualizujbestwartościąheights[t] × width. - Umieść
ina stosie. - Zwróć
best.
def largestRectangleArea(heights):
n = len(heights)
stack = [] # indices whose heights rise strictly from bottom to top
best = 0
for i in range(n + 1):
current = heights[i] if i < n else 0 # a bar of 0 past the end empties the stack
while stack and heights[stack[-1]] >= current:
# Bar i stops the popped bar on the right; the bar below it stops it on the left.
height = heights[stack.pop()]
left = stack[-1] if stack else -1
best = max(best, height * (i - left - 1))
stack.append(i)
return best
Pułapki i przypadki brzegowe
Pętla stosu jest krótka, a niemal każdy błąd dotyczy szerokości albo słupków pozostawionych na końcu.
- Zapominanie o słupkach, które wciąż są na stosie. W rosnącym histogramie, takim jak
[1, 2, 3, 4, 5], podczas pętli nic nie zostaje zdjęte ze stosu, a bez końcowego słupka o wysokości 0 zwracasz 0 zamiast 9. - Obliczanie szerokości od indeksu zdjętego słupka. Jego prostokąt zaczyna się zaraz za słupkiem znajdującym się pod nim na stosie, a nie przy nim samym: w
[2, 5, 6, 3, 4, 1]słupek o wysokości 3 na indeksie 3 rozciąga się od indeksu 1 do 4. Użyciei - tdaje 2 zamiast 4. - Używanie nieprawidłowej szerokości, gdy po zdjęciu elementu stos jest pusty. Zdjęty słupek jest dotąd najniższy, więc jego prostokąt sięga aż do indeksu 0, a szerokość wynosi
i. W[2, 1, 2]słupek o wysokości 1 obejmuje wszystkie trzy słupki, dając pole równe 3. - Zatrzymywanie się na słupkach o równej wysokości po obu stronach w wersji dwuprzebiegowej. Wtedy w
[3, 3, 3, 3]każdy słupek ma szerokość 1, więc zwracasz 3 zamiast 12. Zdejmuj elementy ze stosu przy>=, aby granice wyznaczały słupki ściśle niższe. - Zakładanie, że najwyższy słupek lub najszerszy zakres daje najlepszy wynik. W
[2, 5, 6, 3, 4, 1]ani słupek o wysokości 6, ani pełna szerokość 6 słupków nie daje odpowiedzi; wynik daje średnia wysokość obejmująca średnią szerokość. - Przepełnienie. Pole może tu osiągnąć wartość
10^5 × 2 × 10^4 = 2 × 10^9, która nadal mieści się w 32-bitowej liczbie całkowitej ze znakiem; przy większych ograniczeniach wykonuj mnożenie w 64 bitach.
Najczęstsze pytania4
Jaka jest złożoność czasowa problemu największego prostokąta w histogramie?
Rozwiązanie ze stosem monotonicznym działa w czasie O(n) i wymaga O(n) dodatkowej pamięci. Każdy indeks jest dodawany na stos raz i zdejmowany ze stosu raz, a każde zdjęcie ze stosu wymaga stałej liczby operacji. Sprawdzenie każdego ciągu słupków zajmuje O(n²) czasu, czyli około 2 × 10^8 kroków dla 2 × 10^4 słupków.
Dlaczego prostokąt paska jest mierzony, gdy zostaje on wyświetlony?
Słupek jest zdejmowany ze stosu przez pierwszy słupek po jego prawej stronie, który nie jest wyższy, więc w tym miejscu kończy się jego prostokąt po prawej. Indeks pod nim na stosie wskazuje najbliższy niższy słupek po jego lewej stronie, więc w tym miejscu kończy się on po lewej. W chwili zdjęcia ze stosu oba końce są znane, a pole wynosi height × (i - below - 1).
Czy problem największego prostokąta w histogramie można rozwiązać metodą dziel i zwyciężaj?
Tak. Najniższy słupek w całym zakresie albo znajduje się pod najlepszym prostokątem, którego pole wynosi wtedy lowest × width, albo dzieli zakres na lewą i prawą część, które rozwiązujesz osobno. Przy liniowym przeszukiwaniu minimum złożoność wynosi O(n log n) dla losowych danych wejściowych, ale O(n²) dla posortowanych; drzewo przedziałowe do znajdowania minimów w zakresach zapewnia złożoność O(n log n) zawsze. Stos jest prostszy i szybszy.
Jak wykorzystuje się największy prostokąt w histogramie do znalezienia największego prostokąta w siatce 0/1?
Przechodź przez siatkę wiersz po wierszu i przechowuj dla każdej kolumny liczbę jedynek w ciągu kończącym się w bieżącym wierszu; zero resetuje tę liczbę. Liczby dla każdego wiersza tworzą histogram, a największy prostokąt jedynek kończący się w tym wierszu jest największym prostokątem w tym histogramie. Jednokrotne uruchomienie stosu dla każdego wiersza pozwala rozwiązać problem siatki w czasie O(rows × cols).
Podobne zadania
Zadania oparte na tych samych pomysłach. Rozwiązanie dwóch lub trzech utrwala schemat.
Python
def largestRectangleArea(heights):
# Napisz kod tutajPrzypadek 1
Przypadek 2
Przypadek 3
Wejście
heights = [2, 5, 6, 3, 4, 1]
Oczekiwane
12