0Pricing
DSA Interview Prep · Lezione

DP top-down con memoisation

Aggiunga un dizionario memo a una soluzione ricorsiva per eliminare le chiamate duplicate e usi @lru_cache per applicare la memoisation con il minimo codice

DP top-down con memoisation è 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.

DP top-down: l'idea della memoizzazione

La DP top-down parte dalla soluzione ricorsiva originale e aggiunge la memoizzazione: una cache che memorizza il risultato di ogni sottoproblema la prima volta che viene calcolato. Nelle chiamate successive con gli stessi argomenti, il risultato memorizzato viene restituito immediatamente senza ripetere la ricorsione. In questo modo una ricorsione ingenua O(2^n) diventa O(n) con modifiche minime al codice, spesso bastano solo 2-3 righe aggiunte a una soluzione ricorsiva esistente.

# Top-down approach:
# 1. Write the recursive solution (natural but slow)
# 2. Add a memo dict to cache results
# 3. Before recursing, check if the result is cached
# 4. Before returning, store the result in the cache

# This is also called 'memoization' (US spelling)
# 'memoize' means 'to remember', not 'memorize'

# The cache key is the function arguments
# For fib: key is n
# For 2D DP: key is (i, j)
# For 3D DP: key is (i, j, k)
print('Top-down = recursion + memo cache')

Fibonacci con memoizzazione

L'aggiunta di un dizionario memo alla ricorsione ingenua di Fibonacci riduce il tempo da O(2^n) a O(n). La prima chiamata a fib(k) calcola e memorizza il risultato. Tutte le chiamate successive per lo stesso k restituiscono immediatamente il valore memorizzato. La complessità spaziale è O(n) per il dizionario memo, più O(n) per lo stack delle chiamate. Confronti il numero di chiamate: senza memo, fib(30) ne esegue circa 2 milioni; con memo, esattamente 30.

def fib_memo(n, memo=None):
    if memo is None:
        memo = {}
    if n in memo:
        return memo[n]  # return cached result
    if n <= 1:
        return n
    memo[n] = fib_memo(n-1, memo) + fib_memo(n-2, memo)
    return memo[n]

# Verify speed improvement:
print(fib_memo(30))   # fast!
print(fib_memo(50))   # still fast
print(fib_memo(100))  # no problem

# Without memo, fib_naive(50) would take minutes
# With memo: each of the 50 sub-problems computed once

Utilizzo di @functools.lru_cache

Il decoratore Python @functools.lru_cache(maxsize=None) (o l'alias @cache in Python 3.9+) applica automaticamente la memoizzazione a una funzione in base ai suoi argomenti. È il modo più semplice per aggiungere la DP top-down durante un colloquio: scriva la soluzione ricorsiva e applichi il decoratore. Il decoratore memorizza tutti i risultati in un dizionario indicizzato dagli argomenti della funzione, che devono essere hashable (niente liste: usi invece le tuple).

import functools

@functools.lru_cache(maxsize=None)
def fib(n):
    if n <= 1:
        return n
    return fib(n-1) + fib(n-2)

print(fib(50))   # 12586269025
print(fib(100))  # works instantly

# Clear cache between tests if needed:
fib.cache_clear()

# Python 3.9+ shorthand:
# from functools import cache
# @cache
# def fib(n): ...

print(fib.cache_info())  # shows hits, misses, maxsize, currsize

Coin Change top-down

Coin Change (LeetCode #322): date le denominazioni delle monete e un importo obiettivo, trovi il numero minimo di monete necessarie. La formulazione ricorsiva consiste nel prendere ogni moneta e risolvere il problema per l'importo rimanente, quindi scegliere il minimo. Applichi la memoizzazione all'importo per evitare di ricalcolare i risultati. Caso base: amount=0 richiede 0 monete; un importo impossibile restituisce infinito (o -1 al termine della ricorsione).

import functools

def coin_change_top_down(coins, amount):
    @functools.lru_cache(maxsize=None)
    def dp(remaining):
        if remaining == 0:
            return 0  # no coins needed
        if remaining < 0:
            return float('inf')  # impossible
        # Try each coin and take the minimum
        return 1 + min(dp(remaining - c) for c in coins)

    result = dp(amount)
    return result if result != float('inf') else -1

print(coin_change_top_down([1, 5, 6, 9], 11))  # 2: (5+6) or (2*5+1?no: 9+2?no) 5+6=11 YES
print(coin_change_top_down([2], 3))             # -1: impossible
print(coin_change_top_down([1, 2, 5], 11))      # 3: 5+5+1

Climbing Stairs top-down con K passi

Generalizzi climbing stairs consentendo da 1 a k passi. Lo stato è il gradino corrente e dal gradino i si possono raggiungere i gradini i+1, i+2, ..., i+k. La ricorrenza è: dp(i) = sum of dp(i-j) for j in 1..k if i-j >= 0. La memoizzazione rende il problema O(n*k) invece di O(k^n). Questa generalizzazione compare in problemi come «costo minimo per raggiungere l'ultimo gradino» e «conteggio dei modi per riempire una griglia».

import functools

def climb_k_steps(n, k):
    @functools.lru_cache(maxsize=None)
    def dp(i):
        if i == 0:
            return 1  # base: one way to stay at ground
        if i < 0:
            return 0  # impossible
        # From stair i, you could have come from i-1, i-2, ..., i-k
        return sum(dp(i - j) for j in range(1, k+1) if i - j >= 0)

    return dp(n)

# k=2 (original): should match fib-like sequence
print([climb_k_steps(n, 2) for n in range(7)])  # [1,1,2,3,5,8,13]
# k=3: more options
print([climb_k_steps(n, 3) for n in range(7)])  # [1,1,2,4,7,13,24]

LCS top-down: memoizzazione 2D

La sottosequenza comune più lunga (LCS) richiede uno stato 2D: dp(i, j) = lunghezza della LCS di s1[:i] e s2[:j]. Se s1[i-1] == s2[j-1], i caratteri corrispondono: dp(i,j) = 1 + dp(i-1, j-1). Altrimenti: dp(i,j) = max(dp(i-1,j), dp(i,j-1)) — si salta un carattere da una delle due stringhe. La memoizzazione in base a (i, j) dà O(mn) invece di O(2^(m+n)).

import functools

def lcs_top_down(s1, s2):
    m, n = len(s1), len(s2)

    @functools.lru_cache(maxsize=None)
    def dp(i, j):
        if i == 0 or j == 0:
            return 0  # empty prefix has LCS of 0
        if s1[i-1] == s2[j-1]:
            return 1 + dp(i-1, j-1)  # characters match
        return max(dp(i-1, j), dp(i, j-1))  # skip one

    return dp(m, n)

print(lcs_top_down('abcde', 'ace'))   # 3: 'ace'
print(lcs_top_down('abc', 'abc'))     # 3: 'abc'
print(lcs_top_down('abc', 'def'))     # 0: no common chars

Dizionario memo o lru_cache: quale scegliere

Usi @lru_cache quando gli argomenti della funzione sono tipi primitivi hashable (int, str, tuple). Usi un dizionario memo manuale quando deve passare uno stato mutabile (liste, dizionari) convertendolo in tuple, quando deve tenere traccia delle chiavi già calcolate o quando si trova in un metodo di classe in cui self non dovrebbe essere memorizzato nella cache. Il dizionario memo manuale è più esplicito ed evita problemi sottili con le closure nelle funzioni helper ricorsive.

# @lru_cache: clean, automatic, O(1) overhead
# Use when: arguments are simple (int, str, tuple)
import functools
@functools.lru_cache(maxsize=None)
def simple_dp(n):
    if n <= 1: return n
    return simple_dp(n-1) + simple_dp(n-2)

# Manual memo dict: explicit, flexible
# Use when: complex state, need to inspect memo, class methods
def manual_memo_dp(s1, s2):
    memo = {}
    def dp(i, j):
        if (i,j) in memo: return memo[(i,j)]
        if i == 0 or j == 0:
            return 0
        if s1[i-1] == s2[j-1]:
            memo[(i,j)] = 1 + dp(i-1, j-1)
        else:
            memo[(i,j)] = max(dp(i-1,j), dp(i,j-1))
        return memo[(i,j)]
    return dp(len(s1), len(s2))

print(manual_memo_dp('abcde', 'ace'))  # 3

Target Sum top-down

Target Sum (LeetCode #494): assegni + o - a ogni numero e conti le assegnazioni che producono una somma obiettivo. Stato: dp(index, current_sum). A ogni indice, provi ad aggiungere (+) e sottrarre (-) il numero corrente. La memoizzazione in base a (index, current_sum) trasforma la forza bruta O(2^n) in O(n * sum_range). L'intervallo delle somme è limitato dalla somma totale di tutti i numeri, per un totale di O(n * S) stati.

import functools

def find_target_sum_ways(nums, target):
    @functools.lru_cache(maxsize=None)
    def dp(index, current_sum):
        if index == len(nums):
            return 1 if current_sum == target else 0
        # Try adding the number
        add = dp(index + 1, current_sum + nums[index])
        # Try subtracting the number
        subtract = dp(index + 1, current_sum - nums[index])
        return add + subtract

    return dp(0, 0)

print(find_target_sum_ways([1,1,1,1,1], 3))  # 5
print(find_target_sum_ways([1], 1))            # 1
print(find_target_sum_ways([1], -1))           # 1

Top-down e bottom-up: vantaggi e svantaggi

Top-down (memoizzazione): vantaggi: è naturale da scrivere, perché parte dalla soluzione ricorsiva; calcola solo i sottoproblemi effettivamente necessari (valutazione lazy); è facile aggiungere la cache in modo incrementale. Bottom-up (tabulazione): vantaggi: nessun sovraccarico dello stack delle chiamate (nessun limite della ricorsione Python), accesso alla memoria più favorevole alla cache e ottimizzazione dello spazio più semplice. Entrambi hanno la stessa complessità asintotica. Nei colloqui, inizi da top-down per verificare la correttezza, poi la converta in bottom-up se le viene richiesto di usare meno spazio.

# Top-down advantages:
# + Natural: write recursive, add @cache
# + Lazy: only computes needed sub-problems
# + Easy to reason about correctness
# - Uses call stack (recursion limit in Python)
# - Higher constant factor (function call overhead)

# Bottom-up advantages:
# + No recursion limit
# + Better cache performance (sequential memory)
# + Easier to space-optimise (rolling array)
# - Must compute all sub-problems in order
# - Less intuitive for complex 2D/3D problems

# Interview strategy:
# Start with top-down to verify recurrence,
# convert to bottom-up only if asked.
print('Top-down: easy to write | Bottom-up: efficient for large n')

Word Break con DP top-down

Word Break (LeetCode #139) chiede se una stringa s può essere segmentata in parole appartenenti a un dizionario. Stato: dp(i) = indica se s[i:] può essere segmentata. A partire dall'indice i, provi tutte le parole: se s[i:i+len(w)] == w, richiami la funzione sul suffisso rimanente. La memoizzazione sull'indice iniziale trasforma la forza bruta O(2^n) in O(n^2) (o O(n * max_word_len)) con il controllo di appartenenza all'insieme.

import functools

def word_break(s, word_dict):
    word_set = set(word_dict)

    @functools.lru_cache(maxsize=None)
    def dp(start):
        if start == len(s):
            return True  # successfully segmented entire string
        for end in range(start + 1, len(s) + 1):
            if s[start:end] in word_set and dp(end):
                return True
        return False

    return dp(0)

print(word_break('leetcode', ['leet', 'code']))       # True
print(word_break('applepenapple', ['apple', 'pen'])) # True
print(word_break('catsandog', ['cats', 'dog', 'and', 'cat', 'san', 'andog'])) # False

Limite di ricorsione e itertools

Il limite di ricorsione predefinito di Python è 1000 (impostato da sys.getrecursionlimit()). Per problemi DP su input grandi (n = 10.000+), la memoizzazione top-down raggiungerà questo limite. Opzioni: aumenti il limite con sys.setrecursionlimit(100000) oppure converta il problema in DP bottom-up. Nella programmazione competitiva aumentare il limite è comune; nel codice di produzione, preferisca sempre soluzioni bottom-up o iterative per garantire maggiore affidabilità.

import sys

print('Default recursion limit:', sys.getrecursionlimit())  # 1000

# For large DP problems, increase if needed:
# sys.setrecursionlimit(100000)

# Better: convert to bottom-up DP for large n
def fib_bottom_up(n):
    if n <= 1: return n
    a, b = 0, 1
    for _ in range(2, n+1):
        a, b = b, a + b
    return b

# No recursion limit issue:
print(fib_bottom_up(10000))  # works fine, no recursion

Verifica rapida

Verifichi la comprensione dei concetti di Data Structures & Algorithms — Coding Interview Prep presentati in questa lezione.

Riepilogo della lezione

In questa lezione ha appreso: la DP top-down con un dizionario memo e il decoratore @lru_cache, soluzioni con memoizzazione per Fibonacci, Coin Change, LCS, Target Sum e Word Break e quando scegliere l'approccio top-down rispetto a quello bottom-up. Nel prossimo passaggio implementerà la DP bottom-up con la tabulazione e l'ottimizzazione dello spazio.

Domande Frequenti

La lezione «DP top-down con memoisation» è gratuita?

Sì — il testo completo di «DP top-down con memoisation» è 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 «DP top-down con memoisation»?

Aggiunga un dizionario memo a una soluzione ricorsiva per eliminare le chiamate duplicate e usi @lru_cache per applicare la memoisation con il minimo codice 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 «DP top-down con memoisation»?

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. Riconoscere la DP: sottoproblemi sovrapposti
  2. DP top-down con memoisation
  3. DP bottom-up con tabulation
  4. Coin change e scala a costo minimo
← Torna a DSA Interview Prep