Memoisation: memorizzare nella cache i risultati ricorsivi
Applichi @functools.lru_cache e dizionari memo manuali a Fibonacci e climbing-stairs per eliminare i ricalcoli esponenziali
Memoisation: memorizzare nella cache i risultati ricorsivi è 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.
Il problema della ricorsione ridondante
La versione ricorsiva ingenua di Fibonacci calcola più volte gli stessi valori. fib(5) chiama fib(4) e fib(3); fib(4) chiama fib(3) e fib(2) — quindi fib(3) viene calcolata due volte. Questa ridondanza cresce esponenzialmente: fib(40) esegue più di un miliardo di chiamate di funzione. La memoizzazione risolve il problema memorizzando ogni risultato la prima volta che viene calcolato, così le chiamate successive lo recuperano in O(1) invece di ricalcolarlo.
# Count calls without memoisation
call_count = [0]
def fib_plain(n):
call_count[0] += 1
if n <= 1: return n
return fib_plain(n-1) + fib_plain(n-2)
fib_plain(20)
print(f'fib(20) without memo: {call_count[0]:,} calls')
# ~21,891 calls for n=20; ~1 billion for n=40Memoizzazione manuale con un Dict
Aggiunga un dizionario memo come parametro (o utilizzi una closure). Prima del calcolo, verifichi se la risposta è già presente in memo. In caso affermativo, la restituisca immediatamente. In caso contrario, la calcoli, la memorizzi in memo e la restituisca. Ora ogni sottoproblema distinto viene calcolato esattamente una volta, trasformando O(2^n) in O(n) in termini di tempo e O(n) in termini di spazio per il dizionario memo, oltre a O(n) di spazio nello stack.
def fib_memo(n, memo={}):
if n in memo:
return memo[n]
if n <= 1:
return n
memo[n] = fib_memo(n-1, memo) + fib_memo(n-2, memo)
return memo[n]
print(fib_memo(10)) # 55
print(fib_memo(50)) # 12586269025
print(fib_memo(100)) # huge number — still fast!Decoratore functools.lru_cache
Python fornisce @functools.lru_cache(maxsize=None) (disponibile anche come @functools.cache in Python 3.9+) per automatizzare la memoizzazione. Aggiungendo questo decoratore sopra una funzione, tutte le chiamate vengono memorizzate in cache in base ai relativi argomenti. maxsize=None indica una dimensione illimitata della cache: ogni combinazione univoca di argomenti viene memorizzata. In questo modo qualsiasi funzione ricorsiva diventa una versione con memoizzazione con una sola riga di codice.
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)) # 354224848179261915075
print(fib.cache_info()) # CacheInfo(hits=..., misses=..., maxsize=None, currsize=...)Climbing Stairs (LeetCode 70)
LeetCode 70 «Climbing Stairs»: è possibile salire 1 o 2 gradini alla volta. In quanti modi si può raggiungere il gradino n? In realtà, questo è un problema di Fibonacci: ways(n) = ways(n-1) + ways(n-2). Casi base: ways(0) = 1 (un modo per rimanere al piano terra), ways(1) = 1. Con la memoizzazione, la complessità è O(n) in termini di tempo e O(n) in termini di spazio.
import functools
@functools.lru_cache(maxsize=None)
def climbStairs(n):
if n <= 1:
return 1
return climbStairs(n-1) + climbStairs(n-2)
for i in range(1, 8):
print(f'climbStairs({i}) = {climbStairs(i)}')
# 1,2,3,5,8,13,21Coin Change (LeetCode 322)
LeetCode 322 «Coin Change»: date le denominazioni e un importo obiettivo, trovi il numero minimo di monete. Ricorsione memoizzata top-down: dp(amount) = 1 + min(dp(amount - coin)) per ogni moneta valida. Il caso base è dp(0) = 0. Memorizzi nella cache ogni sottoimporto. Se un sottoimporto è impossibile, restituisca infinito. La memoizzazione trasforma la forza bruta esponenziale in una complessità temporale pari a O(amount × len(coins)).
import functools
def coinChange(coins, amount):
@functools.lru_cache(maxsize=None)
def dp(rem):
if rem == 0:
return 0
if rem < 0:
return float('inf')
return 1 + min(dp(rem - c) for c in coins)
result = dp(amount)
return result if result != float('inf') else -1
print(coinChange([1, 5, 11], 15)) # 3 (5+5+5)
print(coinChange([1, 2, 5], 11)) # 3 (5+5+1)
print(coinChange([2], 3)) # -1Word Break (LeetCode 139) con memoizzazione
LeetCode 139 «Word Break»: determini se una stringa può essere segmentata in parole del dizionario. Ricorsione top-down: can_break(s, start) prova ogni prefisso s[start:end]; se il prefisso è nel dizionario e can_break(s, end) restituisce true, restituisca true. Senza memoizzazione la complessità è O(2^n); con la memoizzazione (memorizzando nella cache ogni indice di inizio) diventa O(n² × L), dove L è la lunghezza massima di una parola.
import functools
def wordBreak(s, wordDict):
word_set = set(wordDict)
@functools.lru_cache(maxsize=None)
def can_break(start):
if start == len(s):
return True
for end in range(start + 1, len(s) + 1):
if s[start:end] in word_set and can_break(end):
return True
return False
return can_break(0)
print(wordBreak('leetcode', ['leet', 'code'])) # True
print(wordBreak('applepenapple', ['apple','pen'])) # True
print(wordBreak('catsandog', ['cats','dog','sand','and','cat'])) # FalseMemoizzazione vs tabulazione
La memoizzazione (top-down) parte dal problema originale e memorizza nella cache le risposte man mano che vengono scoperte ricorsivamente. Risolve solo i sottoproblemi effettivamente necessari. La tabulazione (bottom-up) riempie in anticipo una tabella, dai sottoproblemi più piccoli a quelli più grandi, risolvendo tutti i sottoproblemi indistintamente. La memoizzazione è più facile da ricavare da una soluzione ricorsiva; la tabulazione evita i limiti della profondità della ricorsione e l'overhead delle chiamate di funzione.
# Memoisation (top-down)
import functools
@functools.lru_cache(maxsize=None)
def fib_td(n):
if n <= 1: return n
return fib_td(n-1) + fib_td(n-2)
# Tabulation (bottom-up)
def fib_bu(n):
if n <= 1: return n
dp = [0] * (n + 1)
dp[1] = 1
for i in range(2, n + 1):
dp[i] = dp[i-1] + dp[i-2]
return dp[n]
print(fib_td(20), fib_bu(20)) # 6765 6765
# Both O(n) time; fib_bu avoids recursion limitOttimizzazione dello spazio: variabili a scorrimento
Molti problemi di DP che la ricorsione con memoizzazione risolve usando O(n) di spazio possono essere ulteriormente ottimizzati fino a O(1) di spazio quando servono solo un numero fisso di risposte dei sottoproblemi precedenti. Per Fibonacci, contano solo gli ultimi due valori. Lo stesso vale per Climbing Stairs. Due variabili a scorrimento sostituiscono l'intero dizionario memo o la tabella.
# Fibonacci with O(1) space
def fib_o1(n):
if n <= 1:
return n
prev2, prev1 = 0, 1
for _ in range(2, n + 1):
prev2, prev1 = prev1, prev2 + prev1
return prev1
for i in range(8):
print(f'fib({i})={fib_o1(i)}', end=' ')
print()
# Climbing stairs O(1) space
def climbStairs_o1(n):
if n <= 1: return 1
a, b = 1, 1
for _ in range(2, n + 1):
a, b = b, a + b
return b
print(climbStairs_o1(10)) # 89lru_cache vs closure vs dizionario globale
Esistono tre modi per implementare manualmente la memoizzazione. Un dizionario globale è semplice, ma inquina l'ambito del modulo. Una closure incapsula la cache all'interno della funzione, impedendone la dispersione, ma richiede un wrapper. @lru_cache è la soluzione più pulita: un solo decoratore sostituisce tutto il codice ripetitivo. In un colloquio, inizi con @lru_cache, a meno che l'intervistatore non chieda esplicitamente un'implementazione manuale.
import functools
# 1. Global dict (messy)
memo_global = {}
def fib_global(n):
if n in memo_global: return memo_global[n]
if n <= 1: return n
memo_global[n] = fib_global(n-1) + fib_global(n-2)
return memo_global[n]
# 2. Closure (cleaner scope)
def make_fib():
cache = {}
def fib(n):
if n in cache: return cache[n]
if n <= 1: return n
cache[n] = fib(n-1) + fib(n-2)
return cache[n]
return fib
fib_closure = make_fib()
# 3. lru_cache (best)
@functools.lru_cache(maxsize=None)
def fib_cached(n):
if n <= 1: return n
return fib_cached(n-1) + fib_cached(n-2)
print(fib_global(30), fib_closure(30), fib_cached(30)) # all 832040Quando la memoizzazione non è utile
La memoizzazione accelera solo i problemi con sottoproblemi sovrapposti, cioè i casi in cui lo stesso sottoproblema viene calcolato più volte. Se ogni sottoproblema è univoco (come nel semplice attraversamento di un albero, in cui ogni nodo viene visitato esattamente una volta), la memoizzazione aggiunge overhead senza offrire vantaggi. Inoltre, la memoizzazione non può risolvere i problemi in cui l'albero ricorsivo è esponenziale nel numero di sottoproblemi distinti anziché nel riutilizzo: per questi problemi serve un algoritmo completamente diverso.
# Memoisation DOES help: overlapping sub-problems (Fibonacci)
# fib(n) reuses fib(n-2), fib(n-3), etc.
# Memoisation does NOT help: distinct sub-problems (permutations)
# Each unique (remaining_elements, target) pair is truly distinct
# The exponential complexity comes from the state space itself
print('Memoisation: useful when SAME sub-problem recurs multiple times')
print('Not useful: when every sub-problem is unique to one recursive path')Riepilogo: checklist della memoizzazione
Applichi la memoizzazione quando: dispone di una soluzione ricorsiva corretta ma lenta a causa dei ricalcoli ridondanti, la funzione ha un numero ridotto di combinazioni distinte di argomenti e il valore restituito dipende solo dagli argomenti (funzione pura, senza effetti collaterali né stato globale). Verifichi lo spazio degli stati dei sottoproblemi: se esistono al massimo O(n) o O(n²) stati distinti, la memoizzazione converte il tempo esponenziale in tempo polinomiale.
Verifica rapida
Metta alla prova la propria comprensione dei concetti di Data Structures & Algorithms — Coding Interview Prep trattati in questa lezione.
Riepilogo della lezione
In questa lezione ha imparato che: la memoizzazione memorizza i risultati dei sottoproblemi per evitare i ricalcoli, convertendo la ricorsione esponenziale in tempo polinomiale, @functools.lru_cache è lo strumento idiomatico di Python e richiede una sola riga e la memoizzazione (top-down) e la tabulazione (bottom-up) sono i due stili della DP: la memoizzazione è più facile da ricavare, mentre la tabulazione evita i problemi legati alla profondità dello stack. Congratulazioni: ha completato i moduli sulla ricorsione e sulle hash map!
Domande Frequenti
La lezione «Memoisation: memorizzare nella cache i risultati ricorsivi» è gratuita?
Sì — il testo completo di «Memoisation: memorizzare nella cache i risultati ricorsivi» è 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 «Memoisation: memorizzare nella cache i risultati ricorsivi»?
Applichi @functools.lru_cache e dizionari memo manuali a Fibonacci e climbing-stairs per eliminare i ricalcoli esponenziali 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 «Memoisation: memorizzare nella cache i risultati ricorsivi»?
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
- Schema della ricorsione: caso base, fiducia, costruzione
- Visualizzare lo stack delle chiamate
- Compromessi tra ricorsivo e iterativo
- Memoisation: memorizzare nella cache i risultati ricorsivi