Esercizio - ricorrenza c(i,j) con valore di default e conteggio delle operazioni (simulazione d'esame 2023-24, problema 2)
Fonte: simulazione d'esame 2023-24 di Algoritmi per l'Ingegneria, Ing. Informatica UniPD, seconda parte, problema 2 (14 punti).
Testo
Per , si consideri la seguente definizione per ricorrenza della quantità intera , con :
- Punto 1 [9 punti] Si fornisca lo pseudocodice di un algoritmo memoizzato per il calcolo di . Si presti attenzione alla scelta del valore di default da utilizzare nell'inizializzazione.
- Punto 2 [5 punti] Si determini, analizzando l'albero delle chiamate, il numero esatto di operazioni aritmetiche complessive effettuate dalle due routine (escluse quelle per il calcolo degli indici) che costituiscono il codice memoizzato.
Soluzione
Punto 1: il valore di default
Lo schema è quello della 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 →): INIT_C inizializza la tabella con i casi base e un valore di default, REC_C calcola solo le celle non ancora calcolate. La difficoltà è la sentinella: deve essere un valore che non può mai assumere. Qui può essere zero, positivo o negativo: per esempio e . Quindi non vanno bene né né "un numero negativo": il caso sarebbe scambiato per una cella da calcolare e verrebbe ricalcolato a ogni chiamata, rovinando il conteggio.
Osservazione chiave: per ogni il valore è un multiplo di : nei casi base è ; nel caso ricorsivo è per un intero. Quindi basta scegliere come default un intero che non è multiplo di , per esempio .
INIT_C(n)
if n = 0 then return 0 { c(0,0) = 0 }
C[0, 0] <- 0
for i <- 1 to n do
v <- 3 * i { c(i,0) = c(0,i) = 3i }
C[i, 0] <- v
C[0, i] <- v
for i <- 1 to n do
for j <- 1 to n do C[i, j] <- 1 { 1 = non ancora calcolato }
return REC_C(n, n)
REC_C(i, j)
if C[i, j] = 1 then
C[i, j] <- 3 * (REC_C(i-1, j-1) + REC_C(i-1, j) - REC_C(i, j-1))
return C[i, j]La correttezza segue dalla ricorrenza, dalla corretta inizializzazione dei casi base (riga e colonna , che hanno i valori e ) e dalla scelta del default: una cella contiene se e solo se non è ancora stata calcolata, perché nessun valore vero è . Ogni chiamata con ha come figli , e , che hanno almeno un indice minore: la ricorsione termina sui casi base (riga o colonna ), già pieni.
Punto 2: albero delle chiamate e operazioni
INIT_C: per ogni si fa una moltiplicazione (), usata per scrivere sia sia : in tutto operazioni. Per nessuna.REC_C(n, n): nell'albero delle chiamate, ogni coppia con compare come nodo interno una volta sola: da si raggiunge ogni coppia con indici scendendo di un passo alla volta, e dopo il primo calcolo la cella contiene un multiplo di diverso dal default, quindi tutte le altre chiamate restituiscono subito il valore (foglie a costo nullo). Le chiamate sui casi base ( o ) sono anch'esse foglie a costo nullo. Ogni nodo interno esegue tre operazioni aritmetiche: una somma, una sottrazione e una moltiplicazione per .
I nodi interni sono . Il numero totale di operazioni è
Controllo: per si ha ; per : calcola ( operazione) e ( operazioni): totale .
Esempio ()
Riga e colonna : , cioè . Poi, per :
- ;
- ;
- ;
- ;
- ;
- ;
- ;
- ;
- .
| 0 | 1 | 2 | 3 | |
|---|---|---|---|---|
| 0 | 0 | 3 | 6 | 9 |
| 1 | 3 | 0 | 27 | −36 |
| 2 | 6 | −9 | 108 | −351 |
| 3 | 9 | −36 | 405 | −1944 |
Quindi , calcolato con operazioni. Controllato con Python (codice con contatori contro la definizione ricorsiva, per ). Senza memoizzazione la stessa ricorrenza a tre rami, , produrrebbe un numero esponenziale di chiamate.
Versione ripasso
Testo. se o ; altrimenti , per . Punto 1 [9 punti]: pseudocodice memoizzato per , attenzione al valore di default. Punto 2 [5 punti]: numero esatto di operazioni aritmetiche delle due routine, analizzando l'albero delle chiamate.
- Default: i valori possono essere , positivi o negativi (, ): né né un negativo vanno bene. Ogni è un multiplo di ( o intero), quindi si usa (non multiplo di ).
INIT_C(n)
if n = 0 then return 0
C[0,0] <- 0
for i <- 1 to n do v <- 3*i; C[i,0] <- v; C[0,i] <- v
for i <- 1 to n do for j <- 1 to n do C[i,j] <- 1
return REC_C(n, n)
REC_C(i, j)
if C[i,j] = 1 then C[i,j] <- 3 * (REC_C(i-1,j-1) + REC_C(i-1,j) - REC_C(i,j-1))
return C[i,j]- Costo:
INIT_Cfa moltiplicazioni (). InREC_C(n,n)ogni con è nodo interno una volta (raggiunta scendendo di un passo), con operazioni (somma, sottrazione, prodotto per ); le altre chiamate (casi base, letture) sono foglie a costo nullo:
- Esempio : , , , , , , , , .
- Errori tipici: default o negativo (collide con valori veri e provoca ricalcoli); contare solo dimenticando le moltiplicazioni di
INIT_C; contare nodi interni (riga e colonna sono foglie); contare due operazioni invece di tre per nodo.