Rettangolo più grande nell'istogramma
Usi uno stack monotono per tenere traccia dei limiti sinistri e calcoli in un'unica scansione l'area massima del rettangolo contenuto in un istogramma.
Rettangolo più grande nell'istogramma è una lezione DSA Interview Prep gratuita su CoddyKit. Questa è la lezione 2 di 4. Puoi leggere la lezione completa qui gratuitamente — poi esercitati direttamente nel browser con un editor di codice integrato e un tutor IA disponibile 24/7. Fa parte del percorso di apprendimento DSA Interview Prep, e i tuoi progressi si sincronizzano tra il web e l'app CoddyKit. Il corso DSA Interview Prep include 4 lezioni in totale.
Problema: rettangolo più grande nell'istogramma
Il problema Largest Rectangle in Histogram (LeetCode 84) fornisce un array di interi non negativi che rappresenta le altezze delle barre di un istogramma, in cui ogni barra ha larghezza 1. Occorre trovare l'area del rettangolo più grande che può essere formato all'interno dell'istogramma. Il rettangolo deve coprire barre contigue e la sua altezza è limitata dalla barra più corta che copre.
Un approccio a forza bruta consiste nel calcolare, per ogni coppia (i, j), l'altezza minima nell'intervallo [i, j] e moltiplicarla per (j - i + 1). Questo richiede O(n³), oppure O(n²) usando minimi precalcolati: è troppo lento. La soluzione con stack monotono ha complessità O(n).
# Example: heights = [2, 1, 5, 6, 2, 3]
# Rectangles:
# width=1, height=6 at index 3 => area=6
# width=2, height=5 at indices 2-3 => area=10 (maximum!)
# width=6, height=1 across all => area=6
# width=3, height=2 at indices 2-4 => area=6
heights = [2, 1, 5, 6, 2, 3]
print('Heights:', heights)
print('Expected max area: 10 (bars of height 5 and 6, width 2)')
# Brute force for small inputs:
def brute_force(heights):
n = len(heights)
max_area = 0
for i in range(n):
min_h = heights[i]
for j in range(i, n):
min_h = min(min_h, heights[j])
max_area = max(max_area, min_h * (j - i + 1))
return max_area
print('Brute force answer:', brute_force(heights)) # 10Idea chiave: cosa limita il rettangolo di ogni barra?
Per ogni barra i di altezza h, il rettangolo più grande in cui può costituire il minimo si estende verso sinistra fino alla prima barra più bassa di h e verso destra fino alla prima barra più bassa di h. La larghezza è right_boundary - left_boundary - 1 e l'area è h × width.
Possiamo riformulare il problema: per ogni barra, troviamo il suo elemento minore precedente (PSE) e il suo elemento minore successivo (NSE). Sono esattamente gli elementi calcolati da uno stack monotono crescente. Nel momento in cui rimuoviamo la barra i (perché è stata trovata una barra più bassa), la barra corrente è il suo NSE e la cima dello stack dopo la rimozione è il suo PSE.
heights = [2, 1, 5, 6, 2, 3]
n = len(heights)
# Find PSE and NSE for each bar
pse = [-1] * n # index of previous smaller element
nse = [n] * n # index of next smaller element (default: beyond array)
# PSE
stack = []
for i in range(n):
while stack and heights[stack[-1]] >= heights[i]:
stack.pop()
pse[i] = stack[-1] if stack else -1
stack.append(i)
# NSE
stack = []
for i in range(n - 1, -1, -1):
while stack and heights[stack[-1]] >= heights[i]:
stack.pop()
nse[i] = stack[-1] if stack else n
stack.append(i)
max_area = 0
for i in range(n):
width = nse[i] - pse[i] - 1
area = heights[i] * width
print(f'Bar {i} (h={heights[i]}): PSE={pse[i]}, NSE={nse[i]}, width={width}, area={area}')
max_area = max(max_area, area)
print('Max area:', max_area)Soluzione in un'unica passata con uno stack monotono
L'approccio in due passate descritto sopra funziona, ma può essere combinato in un'unica passata. Si elaborano le barre da sinistra a destra con uno stack monotono crescente. Quando la barra i è più bassa della cima dello stack, si rimuove la cima: l'altezza della barra rimossa è l'altezza di un rettangolo, il suo limite destro è i e il suo limite sinistro è la nuova cima dello stack + 1.
Un trucco standard consiste nell'aggiungere una sentinella 0 alla fine di heights. In questo modo tutte le barre vengono rimosse dallo stack alla fine, anche se non compare naturalmente alcuna barra più bassa. Senza la sentinella, è necessario eseguire una fase di pulizia dopo il ciclo per gli elementi ancora presenti nello stack.
def largest_rectangle(heights):
stack = [] # monotonic increasing: indices of bars
max_area = 0
heights = heights + [0] # sentinel: forces all bars to be popped
for i, h in enumerate(heights):
while stack and heights[stack[-1]] > h:
height = heights[stack.pop()] # height of the rectangle
width = i if not stack else i - stack[-1] - 1 # left boundary
max_area = max(max_area, height * width)
stack.append(i)
return max_area
print(largest_rectangle([2, 1, 5, 6, 2, 3])) # 10
print(largest_rectangle([2, 4])) # 4
print(largest_rectangle([1, 1])) # 2
print(largest_rectangle([0, 9])) # 9
print(largest_rectangle([6, 7, 5, 2, 4, 5, 9, 3])) # 16Tracciare l'algoritmo in un'unica passata
Tracciamo [2, 1, 5, 6, 2, 3, 0] (con sentinella) passo dopo passo:
- i=0, h=2: inseriamo 0. Stack: [0]
- i=1, h=1: rimuoviamo 0 (h=2, width=1, area=2). Stack vuoto, inseriamo 1. Stack: [1]
- i=2, h=5: 5>1, inseriamo 2. Stack: [1,2]
- i=3, h=6: 6>5, inseriamo 3. Stack: [1,2,3]
- i=4, h=2: rimuoviamo 3 (h=6,width=4-2-1=1,area=6), rimuoviamo 2 (h=5,width=4-1-1=2,area=10★), 2>1: ci fermiamo. Inseriamo 4. Stack: [1,4]
- i=5, h=3: 3>2, inseriamo 5. Stack: [1,4,5]
- i=6, sentinella h=0: rimuoviamo tutto, calcolando le aree...
def largest_rectangle_trace(heights):
stack = []
max_area = 0
hs = heights + [0]
for i, h in enumerate(hs):
while stack and hs[stack[-1]] > h:
top = stack.pop()
w = i if not stack else i - stack[-1] - 1
area = hs[top] * w
print(f' Pop bar {top} (h={hs[top]}): width={w}, area={area}', end='')
if area > max_area:
max_area = area
print(' *** NEW MAX ***', end='')
print()
print(f'i={i} h={h}: push {i}, stack={[hs[s] for s in stack + [i]]}')
stack.append(i)
print(f'Max area: {max_area}')
return max_area
largest_rectangle_trace([2, 1, 5, 6, 2, 3])Calcolo della larghezza: perché i - stack[-1] - 1?
Quando rimuoviamo la barra j dallo stack, sappiamo che il limite destro del rettangolo di j è i, cioè la prima barra più bassa di j a destra. Il limite sinistro è la barra immediatamente sotto j nello stack dopo la rimozione, che chiamiamo k. La larghezza è quindi i - k - 1 (le barre da k+1 a i-1, estremi inclusi).
Se lo stack è vuoto dopo la rimozione, il rettangolo di j si estende fino al bordo sinistro (indice 0). La larghezza è semplicemente i (gli indici da 0 a i-1, tutti alti almeno quanto heights[j]). Questo è il caso speciale width = i if not stack else i - stack[-1] - 1.
# Illustrating left/right boundary logic
heights = [1, 3, 5, 2]
# After processing with stack:
# When we pop bar 2 (h=5) at i=3 (h=2):
# stack after pop = [0, 1] => left boundary = 1+1=2, right=3-1=2 => width=1
# When we pop bar 1 (h=3) at i=3 (h=2):
# stack after pop = [0] => left boundary = 0+1=1, right=3-1=2 => width=2
# etc.
def compute_boundaries(heights):
hs = heights + [0]
stack = []
for i, h in enumerate(hs):
while stack and hs[stack[-1]] > h:
top = stack.pop()
if stack:
left = stack[-1] + 1
width = i - stack[-1] - 1
else:
left = 0
width = i
print(f'Bar {top} (h={hs[top]}): extends from {left} to {i-1}, width={width}')
stack.append(i)
compute_boundaries([2, 1, 5, 6, 2, 3])Rettangolo massimo in una matrice binaria
Maximal Rectangle (LeetCode 85) estende il problema dell'istogramma a una matrice binaria 2D. Per ogni riga, si calcola l'altezza degli 1 consecutivi sopra ogni cella. Si crea così un istogramma per quella riga. Si applica quindi l'algoritmo del rettangolo più grande nell'istogramma all'istogramma di ogni riga. Il massimo complessivo tra tutte le righe è la risposta.
In questo modo un problema 2D viene ridotto a n problemi 1D ripetuti sugli istogrammi. La complessità temporale è O(m × n) per una matrice con m righe e n colonne: una passata sull'istogramma per ogni riga, con costo O(n) per passata.
def maximal_rectangle(matrix):
if not matrix or not matrix[0]:
return 0
n = len(matrix[0])
heights = [0] * n
max_area = 0
def hist_max_area(h):
stack, area = [], 0
for i, hh in enumerate(h + [0]):
while stack and h[stack[-1]] > hh:
top = stack.pop()
w = i if not stack else i - stack[-1] - 1
area = max(area, h[top] * w)
stack.append(i)
return area
for row in matrix:
for j in range(n):
heights[j] = heights[j] + 1 if row[j] == '1' else 0
max_area = max(max_area, hist_max_area(heights[:]))
return max_area
matrix = [['1','0','1','0','0'],
['1','0','1','1','1'],
['1','1','1','1','1'],
['1','0','0','1','0']]
print(maximal_rectangle(matrix)) # 6Casi limite nei problemi sugli istogrammi
Casi limite importanti da gestire:
- Altezza sempre uguale: l'intero array forma un unico rettangolo; risposta = n × altezza
- Ordine monotono crescente: non avviene alcuna rimozione fino alla sentinella; l'area dell'ultima barra è il massimo
- Una sola barra: risposta = height[0]
- Barre con altezza 0: agiscono da sentinelle naturali e dividono l'istogramma in segmenti indipendenti
La sentinella (aggiunta come 0) alla fine gestisce il caso di ordine monotono crescente, forzando la rimozione di tutte le barre rimanenti alla fine. Senza di essa, è necessario un ciclo di pulizia separato dopo l'iterazione principale.
def largest_rectangle(heights):
stack = []
max_area = 0
heights = heights + [0]
for i, h in enumerate(heights):
while stack and heights[stack[-1]] > h:
top = stack.pop()
w = i if not stack else i - stack[-1] - 1
max_area = max(max_area, heights[top] * w)
stack.append(i)
return max_area
# Edge cases
print(largest_rectangle([5, 5, 5, 5])) # 20 (all same)
print(largest_rectangle([1, 2, 3, 4, 5])) # 9 (increasing: 3*3)
print(largest_rectangle([5, 4, 3, 2, 1])) # 9 (decreasing: 3*3)
print(largest_rectangle([5])) # 5 (single bar)
print(largest_rectangle([0, 0, 0])) # 0 (all zero)
print(largest_rectangle([3, 0, 3])) # 3 (zero splits)Alternativa: divide et impera
Il problema dell'istogramma può essere risolto anche con divide et impera: si divide in corrispondenza della barra di altezza minima, si risolve ricorsivamente ciascuna metà e si confronta il risultato con il rettangolo che copre l'intera larghezza usando l'altezza minima. Si ottiene O(n log n) in media, ma O(n²) nel caso peggiore per gli input ordinati.
L'approccio con stack monotono è strettamente migliore, con O(n) nel caso peggiore. Tuttavia, comprendere l'approccio divide et impera approfondisce l'intuizione sul problema e spiega perché la barra di altezza minima in ogni segmento è sempre il fattore limitante per i rettangoli che coprono l'intera larghezza.
def largest_rectangle_dc(heights, lo=0, hi=None):
if hi is None:
hi = len(heights) - 1
if lo > hi:
return 0
# Find the index of the minimum height in [lo, hi]
min_idx = lo
for i in range(lo, hi + 1):
if heights[i] < heights[min_idx]:
min_idx = i
# Three options:
# 1. Max rect entirely in left half
# 2. Max rect entirely in right half
# 3. Max rect spanning entire [lo, hi] with height = min
full_width_area = heights[min_idx] * (hi - lo + 1)
left_area = largest_rectangle_dc(heights, lo, min_idx - 1)
right_area = largest_rectangle_dc(heights, min_idx + 1, hi)
return max(full_width_area, left_area, right_area)
print(largest_rectangle_dc([2, 1, 5, 6, 2, 3])) # 10Pattern dell'istogramma: conteggio dei sottoarray
Un problema correlato che utilizza la stessa tecnica dello stack consiste nel contare il numero di sottoarray in un istogramma in cui l'elemento minimo è uguale a un determinato target. Si risolve calcolando PSE e NSE per ogni barra e utilizzando la formula (i - pse[i]) × (nse[i] - i), che conta i sotto-istogrammi in cui la barra i è il minimo.
Questa tecnica del “conteggio a sinistra × conteggio a destra” compare in diversi problemi di LeetCode: sum of subarray minimums (907), count of substrings with all unique characters e problemi basati sulla tecnica dei contributi. Lo stack monotono calcola PSE e NSE in O(n), consentendo di calcolare il contributo di ogni elemento in O(1).
def sum_of_subarray_minimums(arr):
n = len(arr)
pse = [-1] * n # previous strictly smaller element
nse = [n] * n # next smaller or equal element
stack = []
for i in range(n):
while stack and arr[stack[-1]] >= arr[i]:
stack.pop()
pse[i] = stack[-1] if stack else -1
stack.append(i)
stack = []
for i in range(n - 1, -1, -1):
while stack and arr[stack[-1]] > arr[i]:
stack.pop()
nse[i] = stack[-1] if stack else n
stack.append(i)
MOD = 10**9 + 7
total = 0
for i in range(n):
left_count = i - pse[i] # subarrays where i is leftmost min
right_count = nse[i] - i # subarrays where i is the min
total += arr[i] * left_count * right_count
return total % MOD
print(sum_of_subarray_minimums([3, 1, 2, 4])) # 17
print(sum_of_subarray_minimums([11, 81, 94, 43, 3])) # 444Consigli pratici per i colloqui
Quando durante un colloquio si trova davanti a un problema sugli istogrammi, segua questa checklist:
- Chiarisca: le altezze possono essere 0? Qual è l'output: area, indici o conteggio?
- Parta dalla forza bruta e indichi la complessità O(n²) o O(n³)
- Specifichi che il contributo di ogni barra dipende dalla sua estensione a sinistra e a destra fino alla barra più corta più vicina
- Introduca PSE/NSE → stack monotono → soluzione O(n)
- Gestisca il trucco della sentinella (aggiungere 0) per semplificare il codice
- Tracci un piccolo esempio sulla lavagna
Una richiesta di approfondimento comune consiste nell'estendere la soluzione alla versione 2D (rettangolo massimo). Dimostri di saperla ridurre a n problemi di istogrammi, ciascuno in O(n), per un totale di O(m×n).
# Final clean solution for interview
def largest_rectangle_in_histogram(heights):
stack = []
max_area = 0
for i, h in enumerate(heights + [0]): # sentinel forces final pops
while stack and heights[stack[-1]] > h:
height = heights[stack.pop()]
width = i if not stack else i - stack[-1] - 1
max_area = max(max_area, height * width)
stack.append(i)
return max_area
# Verify all test cases from earlier
test_cases = [
([2, 1, 5, 6, 2, 3], 10),
([6, 7, 5, 2, 4, 5, 9, 3], 16),
([1], 1),
([2, 0, 2], 2),
([], 0),
]
for heights, expected in test_cases:
if not heights:
result = 0
else:
result = largest_rectangle_in_histogram(heights)
status = 'PASS' if result == expected else 'FAIL'
print(f'{status}: {heights} => {result} (expected {expected})')Somma degli intervalli dei sottoarray e varianti simili
La tecnica PSE/NSE si generalizza a diversi problemi di LeetCode. Sum of Subarray Ranges (2104) richiede la somma di (max - min) su tutti i sottoarray. Questa equivale alla (somma dei massimi dei sottoarray) meno (la somma dei minimi dei sottoarray), calcolati entrambi con uno stack monotono in O(n). Number of Visible People in a Queue (1944) utilizza uno stack decrescente, in cui ogni estrazione conta una persona visibile. Riconoscere questa famiglia di problemi deriva dall'osservare la frase 'per ogni elemento, fin dove può esercitare il proprio dominio?' — la risposta è sempre PSE/NSE con uno stack monotono.
def sum_subarray_ranges(nums):
n = len(nums)
# Sum of subarray max - sum of subarray min
def contrib(arr, is_max):
# Count contribution of each element as max (or min)
n = len(arr)
left = [0]*n; right = [0]*n
stack = []
for i in range(n):
while stack and (arr[stack[-1]] < arr[i] if is_max else arr[stack[-1]] > arr[i]):
stack.pop()
left[i] = i - (stack[-1] if stack else -1)
stack.append(i)
stack = []
for i in range(n-1, -1, -1):
while stack and (arr[stack[-1]] <= arr[i] if is_max else arr[stack[-1]] >= arr[i]):
stack.pop()
right[i] = (stack[-1] if stack else n) - i
stack.append(i)
return sum(arr[i] * left[i] * right[i] for i in range(n))
return contrib(nums, True) - contrib(nums, False)
print(sum_subarray_ranges([1, 2, 3])) # 4
print(sum_subarray_ranges([1, 3, 3])) # 4
print(sum_subarray_ranges([4, -2, -3, 4, 1])) # 59Verifica rapida
Verifichi la comprensione dei concetti di Data Structures & Algorithms — Coding Interview Prep trattati in questa lezione.
Riepilogo della lezione
In questa lezione ha imparato che: per ogni barra, il rettangolo più grande che la contiene ha i confini definiti dalla barra più corta più vicina su ciascun lato (PSE e NSE), uno stack crescente monotono calcola tutti i confini PSE/NSE in un'unica scansione O(n), trovandoli entrambi quando le barre vengono estratte e aggiungere una sentinella 0 assicura che tutte le barre vengano estratte dallo stack, semplificando il codice in un unico ciclo. Ora applicheremo la deque monotona per risolvere il massimo su una finestra scorrevole in O(n).
Domande Frequenti
La lezione «Rettangolo più grande nell'istogramma» è gratuita?
Sì — il testo completo di «Rettangolo più grande nell'istogramma» è gratuito qui sul web. Per esercitarvi in modo interattivo (un editor di codice integrato e un tutor IA 24/7) e sbloccare il resto del corso DSA Interview Prep, passa a CoddyKit PRO. Il corso DSA Interview Prep include 4 lezioni in totale.
Cosa imparerò in «Rettangolo più grande nell'istogramma»?
Usi uno stack monotono per tenere traccia dei limiti sinistri e calcoli in un'unica scansione l'area massima del rettangolo contenuto in un istogramma. Eserciti DSA Interview Prep con codice pratico che esegui direttamente nel browser, e un tutor IA 24/7 risponde alle tue domande mentre lavori sulla lezione.
Ho bisogno di esperienza per iniziare DSA Interview Prep?
Non è richiesta alcuna esperienza precedente. DSA Interview Prep su CoddyKit è strutturato per principianti e studenti avanzati, quindi puoi iniziare da qui o dall'inizio e procedere al tuo ritmo. Questa è la lezione 2 di 4.
Quanto tempo richiede la lezione «Rettangolo più grande nell'istogramma»?
La maggior parte delle lezioni CoddyKit richiede circa 5–10 minuti. Ogni lezione è breve e interattiva, quindi fai progressi costanti e riprendi esattamente da dove hai lasciato su web e app.
Posso scrivere ed eseguire codice in questa lezione DSA Interview Prep?
Sì. Ogni lezione DSA Interview Prep include un editor di codice integrato, quindi scrivi ed esegui codice reale direttamente nel tuo browser e ricevi feedback istantaneo dall'IA — nessuna configurazione locale necessaria.
Tutte le lezioni di questo corso
- Stack monotono: crescente e decrescente
- Rettangolo più grande nell'istogramma
- Massimo in una finestra scorrevole con deque monotona
- Trapping Rain Water: stack e due puntatori