0Pricing
DSA Interview Prep · Lezione

Ottimizzazione dello spazio per la DP 2D

Riduca lo spazio di LCS e della distanza di modifica da O(mn) a O(min(m,n)) mantenendo soltanto la riga corrente e quella precedente della tabella DP

Ottimizzazione dello spazio per la DP 2D è una lezione DSA Interview Prep gratuita su CoddyKit. Questa è la lezione 4 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.

Perché lo spazio è importante nel DP 2D

Una tabella DP 2D per stringhe di lunghezza 1000 richiede 1000×1000 = 1.000.000 di celle, ovvero circa 8 MB per interi a 64 bit. Per sequenze più lunghe, come nell’allineamento del DNA o nel diff di testi di grandi dimensioni, questo diventa impraticabile. L’osservazione fondamentale è che la maggior parte delle ricorrenze DP 2D considera solo la riga corrente e quella precedente, quindi è possibile comprimere l’intera tabella in uno o due array 1D. Questo è il principio alla base dell’ottimizzazione dello spazio nel DP 2D.

# Full 2D DP: O(mn) space
# LCS for 1000-char strings
m, n = 1000, 1000
dp_2d_size = m * n * 8  # bytes (64-bit ints)
print(f'2D table: {dp_2d_size:,} bytes = {dp_2d_size//1024} KB')

# 1D rolling array: O(n) space
dp_1d_size = n * 8
print(f'1D array: {dp_1d_size:,} bytes = {dp_1d_size} bytes')
print(f'Space saving: {dp_2d_size // dp_1d_size}x')

Schema dell’array scorrevole

Lo schema dell’array scorrevole sostituisce la tabella 2D completa con un array 1D che rappresenta la riga precedente. Quando calcola la riga i, aggiorni ogni cella j usando il valore corrente dp[j], che contiene ancora il valore dp[i-1][j] della riga precedente, e il valore appena aggiornato dp[j-1], che corrisponde a dp[i][j-1]. Una variabile diagonal conserva dp[i-1][j-1] prima che venga sovrascritto. Questo schema si applica alla LCS, alla distanza di modifica e alla maggior parte dei problemi di DP 2D.

# Rolling array template for 2D DP
# Before update: dp[j] holds dp[i-1][j] (previous row)
# After update: dp[j] holds dp[i][j] (current row)

def rolling_array_template(grid):
    m, n = len(grid), len(grid[0])
    dp = [0] * (n + 1)  # represents one row
    for i in range(1, m + 1):
        diag = 0  # stores dp[i-1][j-1] before overwrite
        for j in range(1, n + 1):
            temp = dp[j]  # save dp[i-1][j] before overwriting
            # compute dp[i][j] using dp[j] (above) and dp[j-1] (left) and diag
            dp[j] = diag + dp[j] + dp[j-1]  # placeholder logic
            diag = temp
    return dp[n]

LCS con spazio O(min m,n)

Per la LCS, si assicuri che text1 sia la stringa più corta, così n sarà piccolo. Allochi un array 1D di dimensione n+1. Proceda riga per riga. In ogni cella: salvi temp = dp[j], che corrisponde a dp[i-1][j]. Poi: se i caratteri corrispondono, dp[j] = diag + 1; altrimenti dp[j] = max(dp[j], dp[j-1]). Infine, imposti diag = temp. Dopo aver elaborato tutte le righe, dp[n] contiene la lunghezza della LCS.

def lcs_space_opt(text1, text2):
    # Ensure text2 is the shorter one
    if len(text1) < len(text2):
        text1, text2 = text2, text1
    m, n = len(text1), len(text2)
    dp = [0] * (n + 1)
    for i in range(1, m + 1):
        diag = 0
        for j in range(1, n + 1):
            temp = dp[j]  # dp[i-1][j]
            if text1[i-1] == text2[j-1]:
                dp[j] = diag + 1
            else:
                dp[j] = max(dp[j], dp[j-1])
            diag = temp
    return dp[n]

print(lcs_space_opt('ABCBDAB', 'BDCABA'))  # 4
print(lcs_space_opt('AGGTAB', 'GXTXAYB')) # 4

Distanza di modifica con spazio O(n)

La distanza di modifica usa lo stesso schema scorrevole. L’array 1D iniziale rappresenta la riga 0: dp[j] = j, cioè l’inserimento di j caratteri. Per ogni riga i, imposti dp[0] = i, cioè la cancellazione di i caratteri, e salvi diag = dp[0] prima dell’aggiornamento. Nel ciclo interno, salvi temp = dp[j], calcoli il nuovo valore a partire da inserimento (dp[j-1]+1), cancellazione (dp[j]+1) e sostituzione (diag + cost), quindi imposti diag = temp.

def edit_dist_opt(s, t):
    m, n = len(s), len(t)
    dp = list(range(n + 1))   # row 0: dp[0][j] = j
    for i in range(1, m + 1):
        diag = dp[0]           # dp[i-1][0] before dp[0] update
        dp[0] = i              # dp[i][0] = i
        for j in range(1, n + 1):
            temp = dp[j]       # dp[i-1][j]
            cost = 0 if s[i-1] == t[j-1] else 1
            dp[j] = min(
                dp[j-1] + 1,  # insert
                dp[j] + 1,    # delete
                diag + cost   # replace or match
            )
            diag = temp
    return dp[n]

print(edit_dist_opt('horse', 'ros'))  # 3
print(edit_dist_opt('intention', 'execution'))  # 5

Somma minima del percorso con spazio O(n)

Per il problema della somma minima del percorso su una griglia, l'array 1D a scorrimento inizia con le somme prefisse della prima riga (esiste un solo modo per raggiungere ogni cella della prima riga). Per ogni riga successiva, lo si aggiorna da sinistra a destra: dp[j] prima dell'aggiornamento è il valore della riga sopra (dp[i-1][j]), mentre dp[j-1], appena aggiornato, proviene da sinistra. Qui non serve la diagonale, perché la somma minima del percorso non richiede la cella diagonale.

def min_path_sum_opt(grid):
    m, n = len(grid), len(grid[0])
    dp = [float('inf')] * n
    dp[0] = 0
    for i in range(m):
        # Update first column (only from above)
        dp[0] += grid[i][0]
        for j in range(1, n):
            # min of above (dp[j] = old) and left (dp[j-1] = updated)
            dp[j] = grid[i][j] + min(dp[j], dp[j-1])
    return dp[n-1]

grid = [[1,3,1],[1,5,1],[4,2,1]]
print(min_path_sum_opt(grid))  # 7

Quando è necessario accedere alla diagonale

Non tutti i problemi di DP 2D possono essere compressi con un semplice array a scorrimento, perché alcuni richiedono l'elemento diagonale dp[i-1][j-1] dopo che dp[j] è stato sovrascritto. La soluzione è sempre la stessa: salvare temp = dp[j] prima di aggiornarlo e usarlo come diag per il calcolo della colonna successiva. Questo accorgimento di una cella in anticipo gestisce in modo pulito tutte le ricorrenze a tre direzioni (LCS, distanza di modifica).

# Recap: the diagonal save pattern
# Without it: dp[j-1] updated (left) and dp[j] about to be overwritten
# With it:

def show_diagonal_pattern(s1, s2):
    n = len(s2)
    dp = [0] * (n + 1)
    for ch1 in s1:
        diag = 0  # was dp[i-1][0] = 0 for LCS
        for j, ch2 in enumerate(s2, 1):
            temp = dp[j]  # SAVE before overwrite
            if ch1 == ch2:
                dp[j] = diag + 1  # use saved diagonal
            else:
                dp[j] = max(dp[j], dp[j-1])
            diag = temp  # advance diagonal
    return dp[n]

print(show_diagonal_pattern('ABCBDAB', 'BDCABA'))  # 4

Ottimizzazione dello spazio dello zaino 2D

Anche il problema dello zaino 0/1 beneficia dell'ottimizzazione dello spazio. La tabella 2D completa ha dimensioni (n_items+1) × (capacity+1). L'array a scorrimento la riduce a O(capacity). La differenza fondamentale rispetto a LCS e alla distanza di modifica è la seguente: iterare la dimensione della capacità in ordine inverso (dal valore più alto a quello più basso). In questo modo ogni elemento viene conteggiato al massimo una volta: un'iterazione in avanti consentirebbe di selezionare uno stesso elemento più volte.

def knapsack_01(weights, values, capacity):
    dp = [0] * (capacity + 1)
    for w, v in zip(weights, values):
        # Reverse order: prevents using the same item twice
        for c in range(capacity, w - 1, -1):
            dp[c] = max(dp[c], dp[c - w] + v)
    return dp[capacity]

weights = [1, 3, 4, 5]
values  = [1, 4, 5, 7]
cap = 7
print(knapsack_01(weights, values, cap))  # 9 (items 3+4: weight 3+4=7, value 4+5=9)

Iterazione in avanti e all'indietro

È fondamentale sapere in quale direzione iterare il ciclo interno: all'indietro per lo zaino 0/1 (ogni elemento viene usato al massimo una volta: consultare gli stati precedenti impedisce di riutilizzarlo); in avanti per lo zaino illimitato (ogni elemento può essere riutilizzato: consultare gli stati già aggiornati consente utilizzi multipli). Sbagliare direzione modifica silenziosamente un problema 0/1 trasformandolo in uno illimitato, o viceversa. Confermi sempre il vincolo prima di scegliere la direzione.

# 0/1 Knapsack: each item used AT MOST ONCE → iterate reverse
def knapsack_01_demo(weights, values, cap):
    dp = [0] * (cap + 1)
    for w, v in zip(weights, values):
        for c in range(cap, w-1, -1):  # REVERSE
            dp[c] = max(dp[c], dp[c-w] + v)
    return dp[cap]

# Unbounded Knapsack: items can be reused → iterate forward
def knapsack_unbounded(weights, values, cap):
    dp = [0] * (cap + 1)
    for c in range(1, cap + 1):
        for w, v in zip(weights, values):
            if c >= w:
                dp[c] = max(dp[c], dp[c-w] + v)  # FORWARD
    return dp[cap]

print(knapsack_01_demo([2,3],[3,4],5))     # 7
print(knapsack_unbounded([2,3],[3,4],5))   # 8 (use weight-2 twice: 3+3=6? or 4+... )

Percorsi unici con spazio O(n)

Per il problema dei percorsi unici, l'intera tabella può essere sostituita da una sola riga. Inizializzi tutte le celle a 1 (la prima riga). Per ogni riga successiva, aggiorni da sinistra a destra: dp[j] += dp[j-1]. Non serve la diagonale, perché la ricorrenza usa solo la cella sopra (dp[j], il valore corrente prima dell'aggiornamento) e la cella a sinistra (dp[j-1], già aggiornata). Questa è la compressione più semplice da 2D→1D.

def unique_paths_opt(m, n):
    dp = [1] * n  # first row: all 1s
    for i in range(1, m):
        for j in range(1, n):
            dp[j] += dp[j-1]  # above (dp[j]) + left (dp[j-1])
    return dp[n-1]

# With obstacles
def unique_paths_obstacles_opt(grid):
    m, n = len(grid), len(grid[0])
    dp = [0] * n
    dp[0] = 1
    for i in range(m):
        if grid[i][0] == 1: dp[0] = 0  # blocked column
        for j in range(1, n):
            if grid[i][j] == 1: dp[j] = 0  # blocked
            else: dp[j] += dp[j-1]
    return dp[n-1]

print(unique_paths_opt(3, 7))  # 28
print(unique_paths_obstacles_opt([[0,0,0],[0,1,0],[0,0,0]]))  # 2

Buffer a due righe per ricorrenze complesse

Quando la ricorrenza richiede celle di due o più righe precedenti (ad esempio, alcune varianti della DP su intervalli o riduzioni della DP 3D), si usa un buffer a due righe: mantenga gli array prev e curr e li scambi dopo ogni riga. In questo modo si ottiene uno spazio O(2n) = O(n). Per le ricorrenze che guardano indietro di k righe, mantenga k array come buffer circolare. Questo generalizza il modello dell'array a scorrimento a una riga.

def lcs_two_row_buffer(s1, s2):
    m, n = len(s1), len(s2)
    prev = [0] * (n + 1)  # dp[i-1]
    curr = [0] * (n + 1)  # dp[i]
    for i in range(1, m + 1):
        curr[0] = 0
        for j in range(1, n + 1):
            if s1[i-1] == s2[j-1]:
                curr[j] = prev[j-1] + 1
            else:
                curr[j] = max(prev[j], curr[j-1])
        prev, curr = curr, prev  # swap (curr becomes prev)
    return prev[n]  # after swap, prev holds the last computed row

print(lcs_two_row_buffer('ABCBDAB', 'BDCABA'))  # 4

Quando l'ottimizzazione dello spazio non è possibile

L'ottimizzazione dello spazio non è sempre possibile. Se è necessario ricostruire la soluzione ottimale (non soltanto il suo valore), in genere serve l'intera tabella per il backtracking. Le alternative includono: (1) memorizzare una tabella separata delle decisioni delle stesse dimensioni; (2) usare l'algoritmo di Hirschberg, che calcola LCS in tempo O(mn) e spazio O(min(m,n)), inclusa la ricostruzione, dividendo ricorsivamente il problema al punto medio; (3) accettare uno spazio O(mn) quando è necessaria la ricostruzione.

# When reconstruction needed: must keep full table or use Hirschberg
# Hirschberg's idea: compute LCS length in O(n) space at midpoint of s1,
# recurse on left and right halves. O(mn) time, O(n) space + reconstruction.

# For interview: mention the trade-off
# 'I can reduce to O(n) space if only the value is needed.
#  To also reconstruct the sequence, I need the full O(mn) table
#  or a more complex divide-and-conquer approach.'

print('Space opt: O(n) for length only')
print('Full table: O(mn) needed for reconstruction')

Verifica rapida

Metta alla prova la Sua comprensione dei concetti di Data Structures & Algorithms — Coding Interview Prep trattati in questa lezione.

Riepilogo della lezione

In questa lezione ha imparato che: le tabelle DP 2D possono essere compresse in uno spazio O(n) usando un singolo array 1D a scorrimento quando serve solo la riga precedente, il modello della variabile diagonale (salvare temp prima di sovrascrivere) gestisce le ricorrenze che richiedono dp[i-1][j-1] e lo zaino 0/1 itera la capacità al contrario, mentre lo zaino illimitato itera in avanti. Ora studieremo il modello del backtracking: Scegli, Esplora, Annulla la scelta, alla base degli algoritmi di ricerca esaustiva.

Domande Frequenti

La lezione «Ottimizzazione dello spazio per la DP 2D» è gratuita?

Sì — il testo completo di «Ottimizzazione dello spazio per la DP 2D» è 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 «Ottimizzazione dello spazio per la DP 2D»?

Riduca lo spazio di LCS e della distanza di modifica da O(mn) a O(min(m,n)) mantenendo soltanto la riga corrente e quella precedente della tabella DP 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 4 di 4.

Quanto tempo richiede la lezione «Ottimizzazione dello spazio per la DP 2D»?

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

  1. Percorsi unici e somma minima dei percorsi nelle griglie
  2. Sottosequenza comune più lunga
  3. Distanza di modifica (Levenshtein)
  4. Ottimizzazione dello spazio per la DP 2D
← Torna a DSA Interview Prep