Largest Rectangle in Histogram
Un istogramma è una fila di barre affiancate senza spazi, ciascuna larga un’unità: heights[i] è l’altezza della barra i. Un rettangolo al suo interno copre una serie di barre adiacenti e non può essere più alto della barra più bassa della serie.
Restituisci l’area massima che un rettangolo di questo tipo può avere.
Funzione
- heightsinteger-array
- l'altezza di ciascuna barra, da sinistra a destra
- Restituisceinteger
- l'area del rettangolo più grande che entra nell'istogramma
Vincoli
1 ≤ heights.length ≤ 2 × 1040 ≤ heights[i] ≤ 105- Ogni barra è larga un'unità, quindi un rettangolo che copre le barre da
iajè largoj-i+1unità.
Esempi
- Input
- heights = [2, 5, 6, 3, 4, 1]
- Output
- 12
- Spiegazione
- Le quattro barre 5, 6, 3 e 4 sono tutte alte almeno 3, quindi un rettangolo di altezza 3 le copre: 3 × 4 = 12. Le due barre più alte, 5 e 6, danno solo 5 × 2 = 10.
- Input
- heights = [1, 8, 1, 1]
- Output
- 8
- Spiegazione
- La barra di 8 da sola fa 8 × 1 = 8. Qualsiasi rettangolo più largo include una barra di 1, quindi misura al massimo 1 × 4 = 4.
- Input
- heights = [3, 3, 3, 3]
- Output
- 12
- Spiegazione
- Tutte e quattro le barre sono alte 3, quindi l’intero istogramma è un rettangolo: 3 × 4 = 12.
+17 test nascosti all’invio
Per approfondire
Supponiamo che ogni barra abbia una propria larghezza, indicata in un secondo array. Cosa cambia nella soluzione con stack in un solo passaggio?
Suggerimenti
Aprili uno alla volta. Ognuno rivela un po’ di più.
Il rettangolo più grande tocca la parte superiore di almeno una barra sotto di esso: se non lo facesse, potresti renderlo più alto. Quindi prova ogni barra come barra che determina l’altezza. Quanto può essere largo un rettangolo esattamente di quell’altezza?
Un rettangolo alto quanto la barra
isi estende a sinistra e a destra finché non incontra una barra strettamente più bassa su ciascun lato. Se conosci la barra più vicina e più bassa su ciascun lato di ogni barra, ogni barra fornisce un'area candidata, e ce ne sono solon.Mantieni uno stack di indici le cui altezze aumentano dal basso verso l’alto. Quando arriva una barra che non è più alta di quella in cima, la barra in cima non può arrivare più a destra: rimuovila e il suo rettangolo copre le barre strettamente comprese tra la nuova cima dello stack e la barra corrente. Una barra di altezza 0 dopo la fine rimuove tutto ciò che resta.
Soluzione
Un rettangolo può iniziare e terminare in corrispondenza di qualsiasi barra, e la sua altezza dipende dalla barra più bassa che copre, quindi provare ogni sequenza di barre richiede circa n²/2 passaggi. La soluzione consiste nel capovolgere la domanda: il rettangolo migliore è alto esattamente quanto una delle barre, quindi ogni barra deve solo sapere fin dove può estendersi prima che una barra più bassa la fermi. Uno stack monotono individua questi punti di arresto per ogni barra, prima in due passate e poi in una.
Prova ogni esecuzione con un minimo progressivo
Corretto, ma non termina sui test più grandi
Intuizione
Un rettangolo copre una sequenza di barre adiacenti da start a end e la sua altezza è limitata dalla barra più bassa della sequenza. Prova quindi ogni sequenza. Fissa start, poi aumenta end di una barra alla volta e tieni traccia dell’altezza minima incontrata fino a quel momento. Il rettangolo migliore su quella sequenza ha area lowest × (end-start+1).
In [2, 5, 6, 3, 4, 1], parti dal 5. Le sequenze danno 5 × 1 = 5, poi 5 × 2 = 10 con il 6, poi 3 × 3 = 9 quando si aggiunge il 3, 3 × 4 = 12 con il 4 e 1 × 5 = 5 con l’1. La risposta è 12. Aggiornare lowest man mano che la sequenza cresce mantiene ogni passaggio in O(1), quindi non devi mai ripassare la sequenza per trovare il suo minimo.
È corretto perché ogni rettangolo si trova sopra una qualche sequenza e, per una sequenza fissata, il rettangolo più alto che ci sta è alto esattamente quanto la barra più bassa. È lento perché ci sono n(n+1)/2 sequenze: circa 2 × 10^8 per 2 × 10^4 barre, e quel numero non dipende affatto dalle altezze. La maggior parte di queste sequenze viene interrotta da una barra bassa molto prima di raggiungere la fine, ma l’algoritmo esaustivo continua comunque ad ampliarle.
Algoritmo
- Imposta
bestsu 0. - Per ogni
start, impostalowestsuheights[start]. - Per ogni
enddastartfino all'ultima barra, abbassalowestaheights[end]se quella barra è più corta. - Aggiorna
bestconlowest × (end-start+1). - Restituisci
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 bestLa barra più corta più vicina su ciascun lato
Intuizione
Inverti la ricerca. Nel rettangolo migliore, almeno una barra sottostante è esattamente alta quanto il rettangolo; altrimenti potresti alzare il rettangolo. Quindi la risposta è il massimo, considerando ogni barra i, tra i rettangoli alti esattamente heights[i] e larghi il più possibile. Si estendono finché incontrano una barra strettamente più bassa su ciascun lato. Chiama i loro indici left[i] e right[i], usando -1 e n quando non ce n'è nessuna. Il rettangolo copre le barre strettamente comprese tra loro: larghezza right[i]-left[i]-1. Si tratta di n candidati invece di n²/2.
Per trovare left[i] per ogni barra, procedi da sinistra a destra con uno stack di indici le cui altezze crescono rigorosamente dal basso verso l'alto. Quando arriva la barra i, rimuovi dallo stack tutti gli indici le cui barre sono alte almeno quanto heights[i]. Quelle barre non potranno mai essere la barra più vicina e più bassa né per i né per qualsiasi barra successiva, perché i è più vicina e non è più alta. Qualunque indice rimanga in cima è quello della barra più vicina e più bassa a sinistra. Poi inserisci i nello stack. La stessa scansione da destra a sinistra fornisce right[i].
Per [2, 5, 6, 3, 4, 1] le scansioni restituiscono left = [-1, 0, 1, 0, 3, -1] e right = [5, 3, 3, 5, 5, 6]. La barra di altezza 3 all'indice 3 è fermata dal 2 all'indice 0 e dall'1 all'indice 5, quindi il suo rettangolo è 3 × (5-0-1) = 12. Il 6 è stretto tra i suoi vicini e produce soltanto 6 × 1.
Ogni indice viene inserito una volta e rimosso al massimo una volta in ciascuna scansione, quindi entrambe le scansioni sono O(n), anche se una barra può rimuoverne molte. Il costo è costituito da due array aggiuntivi.
Algoritmo
- Procedi da sinistra a destra con una pila vuota. Per ogni
i, rimuovi elementi dalla pila finché la barra in cima è alta almeno quantoheights[i]; impostaleft[i]sul valore in cima, oppure su -1 se la pila è vuota; inserisciinella pila. - Procedi allo stesso modo da destra a sinistra per riempire
right[i], usandonse la pila è vuota. - Per ogni
i, calcolaheights[i] × (right[i]-left[i]-1). - Restituisci la più grande di queste aree.
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 bestUn passaggio con uno stack monotono
Intuizione
La scansione da sinistra a destra vede già ogni limite destro; lo scarta. Quando la barra i rimuove la barra t, heights[i] non è più alta di heights[t], quindi i è il punto in cui il rettangolo di t termina a destra. L’indice che resta sotto t nella pila è il punto in cui termina a sinistra. Quindi misura il rettangolo nel momento in cui rimuovi la barra: heights[t] × (i - below - 1), dove below è la nuova cima della pila, oppure -1 se la pila è vuota.
L’invariante: le altezze nella pila crescono strettamente dal basso verso l’alto, e l’indice sotto ciascun elemento è la barra più vicina alla sua sinistra che è più bassa. Ogni barra compresa tra le due è stata rimossa durante il processo, dalla barra stessa oppure da una barra che l’elemento ha rimosso in seguito, quindi nessuna di esse è più bassa dell’elemento. Le barre che non vengono mai rimosse arrivano fino alla fine, quindi dopo l’ultima barra elabori un’altra barra di altezza 0. È più bassa di tutte e svuota la pila.
Seguiamo [2, 5, 6, 3, 4, 1]. Inserisci 2, 5 e 6: la pila contiene gli indici [0, 1, 2]. Il 3 all’indice 3 rimuove il 6 (area 6 × (3-1-1) = 6) e il 5 (area 5 × (3-0-1) = 10), poi si ferma al 2 e viene inserito. Inserisci il 4. L’1 all’indice 5 rimuove il 4 (area 4), poi il 3, il cui rettangolo va dall’indice 1 all’indice 4: 3 × (5-0-1) = 12. Rimuove anche il 2 (2 × 5 = 10; la pila è vuota, quindi la larghezza è 5). Lo 0 conclusivo rimuove l’1 (1 × 6 = 6). Il massimo è 12.
Rimuovere con >= significa che una barra di uguale altezza può fermare una barra prima del previsto. È sicuro: la barra di uguale altezza prende il suo posto nella pila, eredita lo stesso limite sinistro e, quando viene rimossa in seguito, il suo rettangolo copre l’intera sequenza. In [3, 3, 3, 3] i primi tre 3 registrano larghezze 1, 2 e 3, e l’ultimo viene rimosso dallo 0 conclusivo con larghezza 4, ottenendo 12.
Algoritmo
- Inizia con una pila di indici vuota e
best = 0. - Per
ida 0 an, imposta l’altezza corrente suheights[i], oppure su 0 quandoi = n. - Mentre la barra in cima alla pila è alta almeno quanto l’altezza corrente, estraila come
t; la larghezza èi - below - 1, dovebelowè il nuovo elemento in cima o -1; aggiornabestconheights[t] × width. - Inserisci
inella pila. - Restituisci
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
Trappole e casi limite
Il ciclo con lo stack è breve e quasi tutti i bug riguardano la larghezza o le barre rimaste alla fine.
- Dimenticare le barre ancora sullo stack. In un istogramma crescente come
[1, 2, 3, 4, 5]non viene mai estratto nulla durante il ciclo e, senza la barra di chiusura di altezza 0, il risultato è 0 invece di 9. - Calcolare la larghezza a partire dall'indice della barra estratta. Il suo rettangolo inizia subito dopo la barra sottostante nello stack, non dalla barra stessa: in
[2, 5, 6, 3, 4, 1]il 3 all'indice 3 si estende dagli indici 1 a 4. Usandoi - tsi ottiene 2 invece di 4. - Usare la larghezza sbagliata quando lo stack è vuoto dopo un'estrazione. La barra estratta è la più bassa incontrata finora, quindi il suo rettangolo arriva fino all'indice 0 e la larghezza è
i. In[2, 1, 2]l'1 si estende su tutte e tre le barre, per un'area di 3. - Fermarsi davanti a barre uguali su entrambi i lati nella versione a due passaggi. In questo caso, in
[3, 3, 3, 3]ogni barra ha una larghezza di 1 e il risultato è 3 invece di 12. Estrai con>=in modo che i limiti siano costituiti da barre strettamente più basse. - Supporre che vinca la barra più alta o l'intervallo più largo. In
[2, 5, 6, 3, 4, 1]né il 6 né l'intera larghezza di 6 barre danno la risposta: la risposta si ottiene con un'altezza intermedia su una larghezza intermedia. - Overflow. In questo caso un'area può raggiungere
10^5 × 2 × 10^4 = 2 × 10^9, che rientra ancora in un intero con segno a 32 bit; con limiti più grandi, esegui la moltiplicazione a 64 bit.
Domande frequenti4
Qual è la complessità temporale del problema del rettangolo più grande nell'istogramma?
La soluzione con stack monotono richiede O(n) tempo e O(n) spazio aggiuntivo. Ogni indice viene inserito una volta e rimosso una volta, e ogni rimozione richiede una quantità costante di lavoro. Provare ogni sequenza di barre richiede O(n²) tempo, circa 2 × 10^8 passaggi per 2 × 10^4 barre.
Perché il rettangolo di una barra viene misurato quando viene estratta?
Una barra viene rimossa dalla pila dalla prima barra alla sua destra che non è più alta, quindi il suo rettangolo termina lì a destra. L’indice sotto di essa nella pila è quello della barra più vicina, più bassa, alla sua sinistra, quindi è lì che termina a sinistra. Nel momento in cui viene rimossa, entrambi gli estremi sono noti e l’area è height × (i - below - 1).
Il problema del rettangolo più grande nell’istogramma può essere risolto con la tecnica divide et impera?
Sì. La barra più bassa dell'intero intervallo si trova sotto il rettangolo migliore, che è quindi lowest × width, oppure divide l'intervallo in una parte sinistra e una destra, da risolvere separatamente. Con una scansione lineare per trovare il minimo, la complessità è O(n log n) con input casuali, ma O(n²) con input ordinati; un albero dei segmenti per i minimi negli intervalli rende la complessità sempre O(n log n). La pila è più semplice e veloce.
In che modo Largest Rectangle in Histogram viene usato per trovare il rettangolo massimo in una griglia 0/1?
Esamina la griglia riga per riga e tieni, per ogni colonna, il conteggio degli 1 consecutivi che terminano alla riga corrente; uno 0 azzera il conteggio. I conteggi di ogni riga formano un istogramma e il rettangolo più grande di 1 che termina su quella riga è il rettangolo più grande di quell'istogramma. Eseguire lo stack una volta per riga risolve la griglia in tempo O(rows × cols).
Problemi simili
Problemi che usano le stesse idee. Risolverne due o tre è ciò che fissa uno schema.
Python
def largestRectangleArea(heights):
# Scrivi il codice quiCaso 1
Caso 2
Caso 3
Input
heights = [2, 5, 6, 3, 4, 1]
Atteso
12