0Pricing
Coding Interview Prep · Lezione

Knapsack 0/1 e ottimizzazione dello spazio

Derivi la ricorrenza dello 0/1 knapsack, riempia la tabella 2D e la riduca a un array 1D iterando sulla capacità in ordine inverso.

Knapsack 0/1 e ottimizzazione dello spazio è una lezione Coding Interview Prep gratuita su CoddyKit. Questa è la lezione 1 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 Coding Interview Prep, e i tuoi progressi si sincronizzano tra il web e l'app CoddyKit. Il corso Coding Interview Prep include 4 lezioni in totale.

Il problema dello zaino 0/1

Il problema dello zaino 0/1: dati n elementi, ciascuno con un peso w[i] e un valore v[i], e uno zaino con capacità W, scelga gli elementi in modo da massimizzare il valore totale senza superare la capacità. Ogni elemento viene preso esattamente una volta (0 = escludere, 1 = includere). Questo è il modello fondamentale di un'ampia famiglia di problemi di DP da colloquio, tra cui partition-equal-subset-sum e target-sum.

Stato e ricorrenza della DP

Definisca dp[i][c] come il valore massimo ottenibile usando i primi i elementi con capacità c. Per l'elemento i ci sono due possibilità: saltarlo (dp[i-1][c]) oppure includerlo se w[i] <= c (dp[i-1][c-w[i]] + v[i]). La ricorrenza è: dp[i][c] = max(dp[i-1][c], dp[i-1][c-w[i]] + v[i]) quando w[i] <= c; altrimenti dp[i][c] = dp[i-1][c]. Caso base: dp[0][c] = 0 per ogni c.

Implementazione della tabella DP 2D

La tabella 2D contiene (n+1) x (W+1) celle e viene compilata riga per riga per ciascun elemento. Dopo aver compilato tutte le righe, dp[n][W] contiene il valore massimo. L'esecuzione richiede O(n × W) di tempo e O(n × W) di spazio: si tratta di una complessità pseudopolinomiale, efficiente quando W è piccolo.

def knapsack_2d(weights, values, W):
    n = len(weights)
    dp = [[0]*(W+1) for _ in range(n+1)]
    
    for i in range(1, n+1):
        w, v = weights[i-1], values[i-1]
        for c in range(W+1):
            dp[i][c] = dp[i-1][c]  # skip item i
            if c >= w:
                dp[i][c] = max(dp[i][c], dp[i-1][c-w] + v)
    
    return dp[n][W]

weights = [2, 3, 4, 5]
values  = [3, 4, 5, 6]
print(knapsack_2d(weights, values, 8))  # 10

Perché iterare la capacità al contrario nella DP 1D

L'osservazione fondamentale è che la riga i dipende solo dalla riga i-1. È quindi possibile usare un unico array 1D e aggiornarlo in loco. Tuttavia, se si itera sulla capacità c da sinistra a destra (da piccola a grande), l'elemento i potrebbe essere contato due volte: si potrebbe usare il valore aggiornato per c-w[i], che include già l'elemento i. Iterare da destra a sinistra (da grande a piccola) garantisce che ogni elemento venga usato al massimo una volta durante l'aggiornamento di una riga.

# Forward iteration (WRONG for 0/1 knapsack - counts items multiple times)
# for c in range(W+1):
#     dp[c] = max(dp[c], dp[c-w] + v)   <-- dp[c-w] may already use item i

# Backward iteration (CORRECT for 0/1 knapsack)
# for c in range(W, w-1, -1):
#     dp[c] = max(dp[c], dp[c-w] + v)   <-- dp[c-w] still from previous row

Implementazione ottimizzata nello spazio con DP 1D

Mantenendo un solo array e iterando sulla capacità da W fino a w[i], si ottiene lo stesso risultato della tabella 2D con O(W) di spazio. La complessità temporale rimane O(n × W). Questa ottimizzazione dello spazio è fondamentale da ricordare: nei colloqui viene chiesto spesso di ridurre lo zaino 2D a una soluzione 1D.

def knapsack_1d(weights, values, W):
    dp = [0] * (W + 1)
    
    for i in range(len(weights)):
        w, v = weights[i], values[i]
        for c in range(W, w - 1, -1):  # iterate RIGHT TO LEFT
            dp[c] = max(dp[c], dp[c - w] + v)
    
    return dp[W]

weights = [2, 3, 4, 5]
values  = [3, 4, 5, 6]
print(knapsack_1d(weights, values, 8))  # 10

Ricostruzione degli elementi selezionati

Per trovare quali elementi sono stati selezionati, è necessaria la tabella 2D completa. Dopo averla compilata, parta da dp[n][W] e proceda a ritroso: se dp[i][c] != dp[i-1][c], l'elemento i è stato incluso; ne sottragga il peso da c e passi alla riga i-1. Continui fino a i = 0. L'ottimizzazione 1D elimina la possibilità di ricostruire la soluzione.

def knapsack_with_items(weights, values, W):
    n = len(weights)
    dp = [[0]*(W+1) for _ in range(n+1)]
    for i in range(1, n+1):
        w, v = weights[i-1], values[i-1]
        for c in range(W+1):
            dp[i][c] = dp[i-1][c]
            if c >= w:
                dp[i][c] = max(dp[i][c], dp[i-1][c-w] + v)
    
    # Reconstruct
    selected, c = [], W
    for i in range(n, 0, -1):
        if dp[i][c] != dp[i-1][c]:
            selected.append(i-1)
            c -= weights[i-1]
    return dp[n][W], selected[::-1]

print(knapsack_with_items([2,3,4,5],[3,4,5,6],8))

Esempio pratico: massimizzare il valore totale

Consideri gli elementi: weights=[2,3,4,5], values=[3,4,5,6], W=8. La soluzione ottimale consiste nello scegliere gli elementi di peso 3 (valore 4) e di peso 5 (valore 6): peso totale 8 e valore 10. In alternativa, si possono scegliere gli elementi di peso 2 e 5, ottenendo un valore totale di 9, oppure quelli di peso 2 e 3, ottenendo un valore di 7. La DP trova correttamente il massimo, cioè 10. Si noti che l'approccio greedy (scegliere il rapporto valore/peso più alto) sceglierebbe per primo l'elemento con rapporto 1.5 (peso 2, valore 3), ma non è sempre ottimale.

Zaino frazionario e zaino 0/1

Nello zaino frazionario è possibile prendere frazioni degli elementi. Questo problema si risolve con un approccio greedy, ordinando gli elementi in base al rapporto valore/peso. Nello zaino 0/1, gli elementi sono indivisibili: l'approccio greedy fallisce ed è necessaria la DP. Gli intervistatori usano questa distinzione per verificare che si sappia quando è applicabile un approccio greedy. Se le viene chiesta la variante frazionaria, menzioni subito l'approccio greedy con ordinamento; per la variante 0/1, scelga la DP.

# Fractional knapsack: greedy by value/weight ratio
def fractional_knapsack(weights, values, W):
    items = sorted(zip(values, weights), key=lambda x: x[0]/x[1], reverse=True)
    total = 0
    for v, w in items:
        if W >= w:
            total += v; W -= w
        else:
            total += v * (W / w); break
    return total

print(fractional_knapsack([2,3,4,5],[3,4,5,6],8))

Complessità temporale pseudopolinomiale

Lo zaino 0/1 è NP-completo, eppure lo si risolve in O(nW) di tempo. La contraddizione si risolve considerando che O(nW) è pseudopolinomiale: W è un valore, non la dimensione dell'input. La rappresentazione binaria di W richiede O(log W) bit, quindi la complessità effettiva è O(n × 2^(log W)), esponenziale rispetto alla dimensione dell'input. Quando W è piccolo (ad esempio 10⁴), la DP è pratica; quando W può arrivare a 10⁹, sono necessari approcci diversi.

Domanda successiva dell'intervistatore: capacità elevata

Se l'intervistatore impone che W sia molto grande (ad esempio 10⁹), ma n sia piccolo, la DP standard non è adatta. Le alternative includono: (1) meet-in-the-middle in O(2^(n/2) × n) di tempo, (2) un'approssimazione greedy per la variante frazionaria oppure (3) branch-and-bound. Per la maggior parte dei problemi da colloquio con W <= 10⁵, la DP 1D con iterazione all'indietro è la risposta attesa.

Meet-in-the-middle per capacità elevate

Quando W è molto grande ma n è piccolo (ad esempio n=40), la DP standard O(nW) non è praticabile, mentre la forza bruta 2^n è troppo lenta. Il meet-in-the-middle divide gli elementi in due metà, enumera tutti i 2^(n/2) sottoinsiemi di ciascuna metà e li abbina in modo ottimale. Si ordina una metà in base al peso, quindi, per ogni sottoinsieme dell'altra metà, si usa la ricerca binaria per trovare l'abbinamento migliore entro il limite di capacità. L'esecuzione richiede O(2^(n/2) × n), rendendo l'approccio pratico fino a n=40.

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: la DP dello zaino 0/1 ha lo stato dp[i][c], che rappresenta il valore massimo con i elementi e capacità c, la ricorrenza sceglie se saltare o includere ciascun elemento e l'ottimizzazione dello spazio 1D itera sulla capacità da destra a sinistra per evitare di contare due volte gli elementi. Ora esploreremo lo zaino illimitato, in cui gli elementi possono essere riutilizzati, e lo applicheremo a Coin Change II.

Domande Frequenti

La lezione «Knapsack 0/1 e ottimizzazione dello spazio» è gratuita?

Sì — il testo completo di «Knapsack 0/1 e ottimizzazione dello spazio» è 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 Coding Interview Prep, passa a CoddyKit PRO. Il corso Coding Interview Prep include 4 lezioni in totale.

Cosa imparerò in «Knapsack 0/1 e ottimizzazione dello spazio»?

Derivi la ricorrenza dello 0/1 knapsack, riempia la tabella 2D e la riduca a un array 1D iterando sulla capacità in ordine inverso. Eserciti Coding 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 Coding Interview Prep?

Non è richiesta alcuna esperienza precedente. Coding 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 1 di 4.

Quanto tempo richiede la lezione «Knapsack 0/1 e ottimizzazione dello spazio»?

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 Coding Interview Prep?

Sì. Ogni lezione Coding 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. Knapsack 0/1 e ottimizzazione dello spazio
  2. Knapsack illimitato e Coin Change II
  3. Somma di sottoinsiemi con partizione equa
  4. Somma obiettivo con segni positivi e negativi
← Torna a Coding Interview Prep