Salta al contenuto
Note per Studenti Esercizio - ricorrenza c(i,j) con valore di default e conteggio delle operazioni (simulazione d'esame 2023-24, problema 2)

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 n≥0n \ge 0, si consideri la seguente definizione per ricorrenza della quantità intera c(i,j)c(i, j), con 0≤i,j≤n0 \le i, j \le n:

c(i,j)={3⋅(i+j)(i=0)∨(j=0)3⋅(c(i−1,j−1)+c(i−1,j)−c(i,j−1))altrimentic(i, j) = \begin{cases} 3 \cdot (i + j) & (i = 0) \lor (j = 0) \\ 3 \cdot \big(c(i-1, j-1) + c(i-1, j) - c(i, j-1)\big) & \text{altrimenti} \end{cases}

  • Punto 1 [9 punti] Si fornisca lo pseudocodice di un algoritmo memoizzato per il calcolo di c(n,n)c(n, n). 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 c(i,j)c(i, j) non può mai assumere. Qui c(i,j)c(i, j) può essere zero, positivo o negativo: per esempio c(1,1)=3(0+3−3)=0c(1, 1) = 3(0 + 3 - 3) = 0 e c(1,3)=−36c(1, 3) = -36. Quindi non vanno bene né 00 né "un numero negativo": il caso c(1,1)=0c(1,1) = 0 sarebbe scambiato per una cella da calcolare e verrebbe ricalcolato a ogni chiamata, rovinando il conteggio.

Osservazione chiave: per ogni i,ji, j il valore c(i,j)c(i, j) è un multiplo di 33: nei casi base è 3⋅(i+j)3 \cdot (i + j); nel caso ricorsivo è 33 per un intero. Quindi basta scegliere come default un intero che non è multiplo di 33, per esempio 11.

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 00 e colonna 00, che hanno i valori 3i3i e 3j3j) e dalla scelta del default: una cella contiene 11 se e solo se non è ancora stata calcolata, perché nessun valore vero è 11. Ogni chiamata con i,j≥1i, j \ge 1 ha come figli (i−1,j−1)(i-1, j-1), (i−1,j)(i-1, j) e (i,j−1)(i, j-1), che hanno almeno un indice minore: la ricorsione termina sui casi base (riga o colonna 00), già pieni.

Punto 2: albero delle chiamate e operazioni

  • INIT_C: per ogni i=1,…,ni = 1, \dots, n si fa una moltiplicazione (v=3iv = 3i), usata per scrivere sia C[i,0]C[i, 0] sia C[0,i]C[0, i]: in tutto nn operazioni. Per n=0n = 0 nessuna.
  • REC_C(n, n): nell'albero delle chiamate, ogni coppia (i,j)(i, j) con 1≤i,j≤n1 \le i, j \le n compare come nodo interno una volta sola: da (n,n)(n, n) si raggiunge ogni coppia con indici ≥1\ge 1 scendendo di un passo alla volta, e dopo il primo calcolo la cella contiene un multiplo di 33 diverso dal default, quindi tutte le altre chiamate restituiscono subito il valore (foglie a costo nullo). Le chiamate sui casi base (i=0i = 0 o j=0j = 0) sono anch'esse foglie a costo nullo. Ogni nodo interno esegue tre operazioni aritmetiche: una somma, una sottrazione e una moltiplicazione per 33.

I nodi interni sono n2n^2. Il numero totale di operazioni è

T(n)=3n2⏟REC_C+n⏟INIT_C.T(n) = \underbrace{3n^2}_{\texttt{REC\_C}} + \underbrace{n}_{\texttt{INIT\_C}}.

Controllo: per n=0n = 0 si ha 00; per n=1n = 1: INIT_C\texttt{INIT\_C} calcola v=3v = 3 (11 operazione) e REC_C(1,1)=3(c(0,0)+c(0,1)−c(1,0))=3(0+3−3)=0\texttt{REC\_C}(1,1) = 3(c(0,0) + c(0,1) - c(1,0)) = 3(0 + 3 - 3) = 0 (33 operazioni): totale 4=3⋅1+14 = 3 \cdot 1 + 1.

Esempio (n=3n = 3)

Riga e colonna 00: c(i,0)=c(0,i)=3ic(i, 0) = c(0, i) = 3i, cioè 0,3,6,90, 3, 6, 9. Poi, per i,j≥1i, j \ge 1:

  • c(1,1)=3(c(0,0)+c(0,1)−c(1,0))=3(0+3−3)=0c(1,1) = 3(c(0,0) + c(0,1) - c(1,0)) = 3(0 + 3 - 3) = 0;
  • c(1,2)=3(c(0,1)+c(0,2)−c(1,1))=3(3+6−0)=27c(1,2) = 3(c(0,1) + c(0,2) - c(1,1)) = 3(3 + 6 - 0) = 27;
  • c(1,3)=3(c(0,2)+c(0,3)−c(1,2))=3(6+9−27)=−36c(1,3) = 3(c(0,2) + c(0,3) - c(1,2)) = 3(6 + 9 - 27) = -36;
  • c(2,1)=3(c(1,0)+c(1,1)−c(2,0))=3(3+0−6)=−9c(2,1) = 3(c(1,0) + c(1,1) - c(2,0)) = 3(3 + 0 - 6) = -9;
  • c(2,2)=3(c(1,1)+c(1,2)−c(2,1))=3(0+27+9)=108c(2,2) = 3(c(1,1) + c(1,2) - c(2,1)) = 3(0 + 27 + 9) = 108;
  • c(2,3)=3(c(1,2)+c(1,3)−c(2,2))=3(27−36−108)=−351c(2,3) = 3(c(1,2) + c(1,3) - c(2,2)) = 3(27 - 36 - 108) = -351;
  • c(3,1)=3(c(2,0)+c(2,1)−c(3,0))=3(6−9−9)=−36c(3,1) = 3(c(2,0) + c(2,1) - c(3,0)) = 3(6 - 9 - 9) = -36;
  • c(3,2)=3(c(2,1)+c(2,2)−c(3,1))=3(−9+108+36)=405c(3,2) = 3(c(2,1) + c(2,2) - c(3,1)) = 3(-9 + 108 + 36) = 405;
  • c(3,3)=3(c(2,2)+c(2,3)−c(3,2))=3(108−351−405)=−1944c(3,3) = 3(c(2,2) + c(2,3) - c(3,2)) = 3(108 - 351 - 405) = -1944.
i\ji \backslash j 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 c(3,3)=−1944c(3, 3) = -1944, calcolato con 3⋅9+3=303 \cdot 9 + 3 = 30 operazioni. Controllato con Python (codice con contatori contro la definizione ricorsiva, per n≤11n \le 11). Senza memoizzazione la stessa ricorrenza a tre rami, T(n,n)=T(n−1,n−1)+T(n−1,n)+T(n,n−1)+3T(n, n) = T(n-1, n-1) + T(n-1, n) + T(n, n-1) + 3, produrrebbe un numero esponenziale di chiamate.

Versione ripasso

Testo. c(i,j)=3(i+j)c(i,j) = 3(i + j) se i=0i = 0 o j=0j = 0; altrimenti c(i,j)=3 (c(i−1,j−1)+c(i−1,j)−c(i,j−1))c(i,j) = 3\,(c(i-1,j-1) + c(i-1,j) - c(i,j-1)), per 0≤i,j≤n0 \le i, j \le n. Punto 1 [9 punti]: pseudocodice memoizzato per c(n,n)c(n,n), 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 00, positivi o negativi (c(1,1)=0c(1,1) = 0, c(1,3)=−36c(1,3) = -36): né 00 né un negativo vanno bene. Ogni c(i,j)c(i,j) è un multiplo di 33 (3(i+j)3(i+j) o 3⋅3 \cdot intero), quindi si usa 11 (non multiplo di 33).
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_C fa nn moltiplicazioni (v=3iv = 3i). In REC_C(n,n) ogni (i,j)(i,j) con 1≤i,j≤n1 \le i,j \le n è nodo interno una volta (raggiunta scendendo di un passo), con 33 operazioni (somma, sottrazione, prodotto per 33); le altre chiamate (casi base, letture) sono foglie a costo nullo:

T(n)=3n2+n(0 per n=0, 4 per n=1, 30 per n=3).T(n) = 3n^2 + n \quad (0 \text{ per } n = 0,\ 4 \text{ per } n = 1,\ 30 \text{ per } n = 3).

  • Esempio n=3n = 3: c(1,1)=0c(1,1) = 0, c(1,2)=27c(1,2) = 27, c(1,3)=−36c(1,3) = -36, c(2,1)=−9c(2,1) = -9, c(2,2)=108c(2,2) = 108, c(2,3)=−351c(2,3) = -351, c(3,1)=−36c(3,1) = -36, c(3,2)=405c(3,2) = 405, c(3,3)=−1944c(3,3) = -1944.
  • Errori tipici: default 00 o negativo (collide con valori veri e provoca ricalcoli); contare solo 3n23n^2 dimenticando le nn moltiplicazioni di INIT_C; contare (n+1)2(n+1)^2 nodi interni (riga e colonna 00 sono foglie); contare due operazioni invece di tre per nodo.

Teoria collegata