Salta al contenuto
Note per Studenti Memoizzazione di ricorrenze triangolari - conteggio esatto e diagonali costanti

Memoizzazione di ricorrenze triangolari - conteggio esatto e diagonali costanti

In questa pagina 4

Esercitazione sulla memoizzazione (Programmazione dinamica - Fibonacci, memoizzazione e schema del paradigmala programmazione dinamica (DP) evolve il divide and conquer: invece di risolvere di nuovo le stesse sottoistanze, le salva in una tabella. Fibonacci ricorsivo costa F_n - 1 addizioni (esponenziale), quello con tabella n - 1. Tre condizioni: sottostruttura, sottoistanze ripetute, spazio dei sottoproblemi ristretto. Bottom-up o memoizzazione (ricorsione + tabella globale). Per i problemi di ottimizzazione lo schema è: sottostruttura ottima, ricorrenza sui costi, informazione addizionale, calcolo della tabella, ricostruzione.Programmazione dinamica - Fibonacci, memoizzazione e schema del paradigma →) con una ricorrenza definita sul triangolo 1≤i≤j≤n1 \le i \le j \le n, come quella della sottosequenza palindroma (Allineamento di stringhe e sottosequenza palindroma più lungadue problemi di DP su stringhe con la stessa struttura della LCS. Allineamento ottimo di due stringhe di DNA con matrice di score S e penalità di gap g: C(i,j) = max{C(i-1,j-1) + s(x_i,y_j), C(i-1,j) + g, C(i,j-1) + g}, C(i,0) = i g, C(0,j) = j g; Theta(mn) tempo, spazio Theta(min{m,n}) con un solo vettore se serve solo il costo. Sottosequenza palindroma più lunga (LPS) sullo spazio delle sottostringhe X_(i..j): p(i,j) = 2 + p(i+1,j-1) se x_i = x_j, altrimenti max{p(i+1,j), p(i,j-1)}; scansione per diagonali, n(n-1)/2 confronti.Allineamento di stringhe e sottosequenza palindroma più lunga →), ma con un prodotto invece di un massimo. Lo scopo è doppio: contare esattamente le operazioni di un codice memoizzato analizzando l'albero delle chiamate, e capire come una proprietà della ricorrenza permetta di ridurre di un ordine di grandezza il numero di sottoproblemi.

La ricorrenza

Sia n>0n > 0. Per 1≤i≤j≤n1 \le i \le j \le n:

M(i,j)={1i=j2j=i+1M(i+1,j−1)⋅M(i+1,j)⋅M(i,j−1)j>i+1M(i, j) = \begin{cases} 1 & i = j \\ 2 & j = i + 1 \\ M(i+1, j-1) \cdot M(i+1, j) \cdot M(i, j-1) & j > i + 1 \end{cases}

Si vuole M(1,n)M(1, n) con il numero esatto T(n)T(n) di moltiplicazioni tra interi. La cella (i,j)(i, j) dipende da tre celle di lunghezza minore (la sottostringa Xi..jX_{i..j} ha lunghezza ℓ=j−i+1\ell = j - i + 1; le dipendenze hanno lunghezza ℓ−2\ell - 2, ℓ−1\ell - 1, ℓ−1\ell - 1): le stesse della LPS.

Il codice memoizzato standard

Il valore 00 è un default ammissibile, perché M(i,j)>0M(i, j) > 0 per ogni i≤ji \le j (è un prodotto di numeri positivi, a partire da 11 e 22).

INIT_M(n)
  if n = 1 then return 1               { M(1,1) = 1 }
  if n = 2 then return 2               { M(1,2) = 2 }
  for i <- 1 to n-1 do
    M[i, i] <- 1
    M[i, i+1] <- 2
  M[n, n] <- 1
  for i <- 1 to n-2 do
    for j <- i+2 to n do M[i, j] <- 0   { da calcolare }
  return REC_M(1, n)

REC_M(i, j)
  if M[i, j] = 0 then
    M[i, j] <- REC_M(i+1, j-1) * REC_M(i+1, j) * REC_M(i, j-1)
  return M[i, j]

Correttezza: discende dall'inizializzazione dei casi base e dalla scelta della sentinella.

Costo. INIT_M non esegue moltiplicazioni. Nell'albero delle chiamate di REC_M(1, n):

  • le coppie con j≥i+2j \ge i + 2 (cioè 1≤i≤n−21 \le i \le n - 2 e i+2≤j≤ni + 2 \le j \le n) sono nodi interni, ciascuno una volta sola: tutte vengono raggiunte dalla radice (si arriva a (i,j)(i, j) togliendo caratteri dagli estremi), e alle chiamate successive la cella è già piena. Ogni nodo interno esegue due moltiplicazioni (tre fattori);
  • tutte le altre chiamate sono foglie a lavoro nullo (casi base e letture di tabella).

Quindi

T(n)=∑i=1n−2∑j=i+2n2=2∑i=1n−2(n−i−1)=2∑k=1n−2k=2⋅(n−2)(n−1)2=(n−1)(n−2).T(n) = \sum_{i=1}^{n-2} \sum_{j=i+2}^{n} 2 = 2\sum_{i=1}^{n-2} (n - i - 1) = 2 \sum_{k=1}^{n-2} k = 2 \cdot \frac{(n-2)(n-1)}{2} = (n-1)(n-2).

(Nella seconda uguaglianza, per ii fissato il numero di valori di jj è n−(i+2)+1=n−i−1n - (i+2) + 1 = n - i - 1; nella terza si pone k=n−i−1k = n - i - 1, che va da n−2n - 2 a 11.) Per n=6n = 6: 2020 moltiplicazioni, 1010 nodi interni (1,3),(1,4),(1,5),(1,6),(2,4),(2,5),(2,6),(3,5),(3,6),(4,6)(1,3), (1,4), (1,5), (1,6), (2,4), (2,5), (2,6), (3,5), (3,6), (4,6) (controllato eseguendo il codice per n≤15n \le 15). È un algoritmo Θ(n2)\Theta(n^2).

Un algoritmo migliore: le diagonali sono costanti

Basta scrivere la tabella per n=6n = 6 (riga ii, colonna jj):

j=1j=1 j=2j=2 j=3j=3 j=4j=4 j=5j=5 j=6j=6
i=1i=1 1 2 4 32 4096 536870912
i=2i=2 1 2 4 32 4096
i=3i=3 1 2 4 32
i=4i=4 1 2 4
i=5i=5 1 2
i=6i=6 1

Ogni diagonale (ℓ=j−i+1\ell = j - i + 1 costante) ha lo stesso valore.

Proposizione. Per ogni 1≤ℓ≤n1 \le \ell \le n, tutti gli elementi M(i,j)M(i, j) con j−i+1=ℓj - i + 1 = \ell valgono uno stesso numero V(ℓ)V(\ell), dove V(ℓ)={1ℓ=12ℓ=2V(ℓ−1)2⋅V(ℓ−2)ℓ≥3V(\ell) = \begin{cases} 1 & \ell = 1 \\ 2 & \ell = 2 \\ V(\ell - 1)^2 \cdot V(\ell - 2) & \ell \ge 3 \end{cases}

Dimostrazione per induzione su ℓ\ell. Base: ℓ=1\ell = 1: M(i,i)=1=V(1)M(i, i) = 1 = V(1) per tutti gli ii; ℓ=2\ell = 2: M(i,i+1)=2=V(2)M(i, i+1) = 2 = V(2) per tutti gli ii. Passo: sia ℓ≥3\ell \ge 3 e valga la tesi per le diagonali di indice minore di ℓ\ell. Un elemento M(i,j)M(i, j) sulla diagonale ℓ\ell è il prodotto di M(i+1,j−1)M(i+1, j-1) (diagonale ℓ−2\ell - 2, valore V(ℓ−2)V(\ell - 2) per l'ipotesi induttiva) e di M(i+1,j)M(i+1, j) e M(i,j−1)M(i, j-1) (entrambi sulla diagonale ℓ−1\ell - 1, valore V(ℓ−1)V(\ell - 1)). Quindi vale V(ℓ−1)⋅V(ℓ−1)⋅V(ℓ−2)V(\ell - 1) \cdot V(\ell - 1) \cdot V(\ell - 2), indipendentemente da ii. □\square

(Nelle dispense, nel passo induttivo, il secondo fattore è scritto M(i−1,j)M(i-1, j) al posto di M(i+1,j)M(i+1, j): è un refuso.)

La risposta cercata è M(1,n)=V(n)M(1, n) = V(n): non serve più una tabella bidimensionale di Θ(n2)\Theta(n^2) elementi, ma un vettore V[1..n]V[1..n].

INIT_V(n)
  if n = 1 then return 1
  if n = 2 then return 2
  for l <- 3 to n do V[l] <- 0
  V[1] <- 1; V[2] <- 2
  return REC_V(n)

REC_V(l)
  if V[l] = 0 then V[l] <- (REC_V(l-1))^2 * REC_V(l-2)
  return V[l]

Per n≥3n \ge 3 l'albero di REC_V(n) ha esattamente un nodo interno per ogni ℓ\ell con 3≤ℓ≤n3 \le \ell \le n (le chiamate successive trovano il valore in tabella), ciascuno con due moltiplicazioni (il quadrato e il prodotto per V(ℓ−2)V(\ell - 2)):

T(n)=2 (n−2).T(n) = 2\,(n - 2).

È un algoritmo lineare invece che quadratico: per n=6n = 6 sono 88 moltiplicazioni invece di 2020. I valori dell'esempio sono V(1),…,V(6)=1,2,4,32,4096,536 870 912V(1), \dots, V(6) = 1, 2, 4, 32, 4096, 536\,870\,912: V(3)=22⋅1=4V(3) = 2^2 \cdot 1 = 4, V(4)=42⋅2=32V(4) = 4^2 \cdot 2 = 32, V(5)=322⋅4=4096V(5) = 32^2 \cdot 4 = 4096, V(6)=40962⋅32=224⋅25=229=536 870 912V(6) = 4096^2 \cdot 32 = 2^{24} \cdot 2^5 = 2^{29} = 536\,870\,912. Il controllo con Python coincide con la tabella completa fino a n=15n = 15.

Osservazione sulla taglia dei numeri. Tutti i valori sono potenze di 22: V(ℓ)=2e(ℓ)V(\ell) = 2^{e(\ell)} con e(1)=0e(1) = 0, e(2)=1e(2) = 1 e e(ℓ)=2e(ℓ−1)+e(ℓ−2)e(\ell) = 2e(\ell-1) + e(\ell-2), cioè e=0,1,2,5,12,29,70,…e = 0, 1, 2, 5, 12, 29, 70, \dots (i numeri di Pell). Crescono come (1+2)ℓ(1 + \sqrt 2)^\ell: contare ogni moltiplicazione come costo unitario è lecito solo nel modello aritmetico, perché le cifre in gioco crescono esponenzialmente.

Lezione. Prima di impostare una tabella a due indici conviene guardare i valori per nn piccolo: regolarità come le diagonali costanti riducono lo spazio dei sottoproblemi da Θ(n2)\Theta(n^2) a Θ(n)\Theta(n) e il lavoro da quadratico a lineare.

Domande d'esame

  1. (Risposta numerica.) Quanto vale M(1,5)M(1, 5)? E quante moltiplicazioni esegue il codice standard per n=7n = 7, e quello con le diagonali? Traccia: M(1,5)=V(5)=4096M(1, 5) = V(5) = 4096; (n−1)(n−2)=30(n-1)(n-2) = 30 contro 2(n−2)=102(n-2) = 10.
  2. (Quesito aperto.) Perché per analizzare un algoritmo memoizzato si conta il numero di nodi interni dell'albero delle chiamate e non si usa la ricorrenza sul numero di operazioni? Traccia: la ricorrenza conta tutte le espansioni; nel memoizzato ogni sottoistanza non di base è espansa una volta sola e le altre chiamate sono letture di tabella a costo nullo.
  3. (Dimostrazione.) Provare che le diagonali sono costanti. Traccia: induzione sull'indice di diagonale ℓ\ell; per ℓ≥3\ell \ge 3 la cella è il prodotto di una cella sulla diagonale ℓ−2\ell - 2 e di due sulla ℓ−1\ell - 1.

Altri esercizi d'esame con memoizzazione e conteggio sull'albero delle chiamate: Esercizio - ricorrenza t(i,j) con calcolo memoizzato e conteggio esatto (compito del 26 giugno 2025, es. 2) e Esercizio - ricorrenza c(i,j) con valore di default e conteggio delle operazioni (simulazione d'esame 2023-24, problema 2).

Versione ripasso

  • Ricorrenza. M(i,i)=1M(i,i) = 1, M(i,i+1)=2M(i,i+1) = 2, M(i,j)=M(i+1,j−1)⋅M(i+1,j)⋅M(i,j−1)M(i,j) = M(i+1,j-1) \cdot M(i+1,j) \cdot M(i,j-1) per j>i+1j > i+1, su 1≤i≤j≤n1 \le i \le j \le n. Si vuole M(1,n)M(1,n) e il numero esatto di moltiplicazioni.
  • Codice standard. INIT_M(n): casi base (n=1→1n = 1 \to 1, n=2→2n = 2 \to 2), M[i,i]=1M[i,i] = 1, M[i,i+1]=2M[i,i+1] = 2, il resto a 00 (valido: tutti i valori sono >0> 0), poi REC_M(1,n). REC_M(i,j): se M[i,j]=0M[i,j] = 0 allora M[i,j]←M[i,j] \leftarrow prodotto delle tre chiamate; restituisce M[i,j]M[i,j].
  • Costo. INIT_M non moltiplica. Nodo interno una volta per ogni coppia (i,j)(i,j) con j≥i+2j \ge i+2, tutte raggiunte da (1,n)(1,n), due moltiplicazioni ciascuno; il resto sono foglie a lavoro nullo:

T(n)=∑i=1n−2∑j=i+2n2=2∑i=1n−2(n−i−1)=2∑k=1n−2k=(n−1)(n−2).T(n) = \sum_{i=1}^{n-2}\sum_{j=i+2}^{n} 2 = 2\sum_{i=1}^{n-2}(n-i-1) = 2\sum_{k=1}^{n-2} k = (n-1)(n-2).

Per n=6n = 6: 2020 moltiplicazioni, 1010 nodi interni.

  • Diagonali costanti. Per n=6n = 6 la riga 11 è 1,2,4,32,4096,536 870 9121, 2, 4, 32, 4096, 536\,870\,912 e ogni riga successiva è la stessa spostata a destra. Tutti gli M(i,j)M(i,j) con j−i+1=ℓj - i + 1 = \ell valgono V(ℓ)V(\ell), con V(1)=1V(1) = 1, V(2)=2V(2) = 2, V(ℓ)=V(ℓ−1)2⋅V(ℓ−2)V(\ell) = V(\ell-1)^2 \cdot V(\ell-2) (induzione su ℓ\ell: un fattore sulla diagonale ℓ−2\ell - 2 e due sulla ℓ−1\ell - 1). Risposta: M(1,n)=V(n)M(1,n) = V(n).
REC_V(l): if V[l] = 0 then V[l] <- (REC_V(l-1))^2 * REC_V(l-2); return V[l]

INIT_V: casi n=1,2n = 1, 2, V[1]=1V[1] = 1, V[2]=2V[2] = 2, V[ℓ]=0V[\ell] = 0 per ℓ≥3\ell \ge 3. Un nodo interno per ogni ℓ=3..n\ell = 3..n, due moltiplicazioni: T(n)=2(n−2)T(n) = 2(n - 2), lineare. Per n=6n = 6: 88 contro 2020.

  • Valori. V(3)=22⋅1=4V(3) = 2^2 \cdot 1 = 4, V(4)=42⋅2=32V(4) = 4^2 \cdot 2 = 32, V(5)=322⋅4=4096V(5) = 32^2 \cdot 4 = 4096, V(6)=40962⋅32=229=536 870 912V(6) = 4096^2 \cdot 32 = 2^{29} = 536\,870\,912. Sono potenze di 22 con esponenti di Pell 0,1,2,5,12,29,…0, 1, 2, 5, 12, 29, \dots (e(ℓ)=2e(ℓ−1)+e(ℓ−2)e(\ell) = 2e(\ell-1) + e(\ell-2)): il costo unitario per moltiplicazione vale solo nel modello aritmetico.
  • Lezione. Guardare i valori per nn piccolo prima di costruire la tabella: le regolarità riducono lo spazio dei sottoproblemi da Θ(n2)\Theta(n^2) a Θ(n)\Theta(n). Nelle dispense, nel passo induttivo, compare M(i−1,j)M(i-1,j) al posto di M(i+1,j)M(i+1,j).
  • Domande tipiche: M(1,5)=4096M(1,5) = 4096; per n=7n = 7: 3030 moltiplicazioni contro 1010.
  • Errori tipici: contare tutte le n(n+1)/2n(n+1)/2 celle come nodi interni (i casi base non moltiplicano); dimenticare che i nodi interni hanno due moltiplicazioni e non una; usare come sentinella un valore che la ricorrenza può produrre; usare la ricorrenza sul numero di operazioni invece dell'albero delle chiamate.

Teoria collegata