Capitolo 6 Programmazione dinamica 6.4 Il problema della distanza di edit tra due stringhe x e y chiede di calcolare il minimo numero di operazioni su singoli caratteri (inserimento, cancellazione e sostituzione) per trasformare x in y (o viceversa). Fornire un algoritmo quadratico di programmazione dinamica per calcolare la distanza di edit tra x e y. 6.11 Progettare l algoritmo per stampare il massimo insieme indipendente di un albero. 6.17 Progettare un algoritmo di programmazione dinamica per trovare la sequenza di moltiplicazioni che minimizzi il costo complessivo del prodotto A = A 0 A 1 A n 1 di n matrici, dove la loro taglia è specificata mediante una sequenza di n+1 interi positivi d 0,d 1,...,d n : la matrice A i ha taglia d i d i+1 per 0 i n 1. Ipotizzare che il costo della moltiplicazione di due matrici di taglia r s e s t sia proporzionale a r s t. Soluzioni 6.4 Applichiamo il paradigma della programmazione dinamica per il calcolo della distanza di edit, in tempo polinomiale O(mn) dove m = x e n = y. Definiamo una notazione per la distanza di edit tra un prefisso di x e uno di y D(i,j)=edit(x[0,i 1],y[0,j 1]) dove 0 i m e0 j n (adottiamo la convenzione che x[0, 1] sia vuota e che x[0, 1] sia vuota). Osserviamo che D(m, n) fornisce la soluzione al nostro problema e definiamo i sotto-problemi elementari come D(i, 0)=i e D(0, j)=j, in quanto se (almeno)
18 Capitolo 6 Programmazione dinamica una delle sequenze è vuota, allora occorrono un numero di operazioni di edit pari alla lunghezza dell altra stringa. Forniamo la definizione ricorsiva in termini dei sotto-problemi D(i, j) per i>0 e j>0, secondo la seguente regola: D(i, j)=min max{i,j} se i = 0oj = 0 D(i 1,j 1) se i,j>0ex[i 1]=y[j 1] D(i 1,j 1)+1 se i,j>0ex[i 1] = y[j 1] D(i,j 1)+1 se j>0ex[i 1] = y[j 1] D(i 1,j)+1 se i>0ex[i 1] = y[j 1] (6.1) La prima riga della regola in (6.1) riporta i valori per i sotto-problemi elementari (i 0 o j 0). Le successive quattro righe in (6.1) descrivono come ricombinare le soluzioni dei sotto-problemi (i,j>0): x[i 1] =y[j 1]: se k = D(i 1,j 1) è l edit distnace per x[0,i 2] e y[0,j 2], allora k + 1 lo è per x[0,i 1] e y[0,j 1], in quanto il loro ultimo elemento è uguale; x[i 1] = y[j 1]: se D(i 1,j 1), D(i,j 1) e D(i 1,j) sono le distanze di edit, allora la minima tra queste più uno fornisce la distanza D(i,j), perché dobbiamo sicurante effettuare una sostituzione, una cancellazione o una inserzione. 1 EDIT( a, b ): pre: x e y sono di lunghezza m e n 2 for (i = 0; i <= m; i = i+1) 3 distanza[i][0] = i; 4 for (j = 0; j <= n; j = j+1) 5 distanza[0][j] = j; 6 for (i = 1; i <= m; i = i+1) 7 for (j = 1; j <= n; j = j+1) { 8 if (x[i-1] == y[j-1]) { 9 distanza[i][j] = distanza[i-1][j-1]; 10 } else { 11 distanza[i][j] = 1 + MIN{ distanza[i-1][j-1], distanza[i][j-1], distanza[i-1][j] }; 12 } 13 } 14 return distanza[m][n]; 6.11 La modifica è semplice poiché basta stabilire se il nodo corrente contribuisce o meno al massimo insieme indipendente di un albero. Ipotizziamo per
Programmare algoritmi 19 semplicità che ogni nodo u abbia due campi dove è stato memorizzato il corrispondente valore di sizet e sizef. La chiama iniziale è con u uguale alla radice dell albero ed escluso posto a false. 1 Stampa( u, escluso ): 2 if (u.primo == null) return; 3 if (escluso) { 4 for (v = u.primo; v!= NULL; v = v.fratello) { 5 Stampa( v, false ); 6 } 7 } else { 8 if (u.sizet >= sizef) { 9 print u; 10 for (v = u.primo; v!= NULL; v = v.fratello) { 11 Stampa( v, true ); 12 } 13 } else { 14 for (v = u.primo; v!= NULL; v = v.fratello) { 15 Stampa( v, false ); 16 } 17 } 18 } 6.17 Applichiamo la programmazione dinamica al calcolo della sequenza ottima di moltiplicazioni di n>2 matrici A = A 0 A 1 A n 1. Ai fini della nostra discussione, utilizziamo l algoritmo immediato che moltiplica due matrici di taglia r s e s t con l ipotesi semplificativa che tale algoritmo richieda un numero di operazioni esattamente pari a r s t, notando che quanto descritto si applica anche agli algoritmi più veloci come quello di Strassen. Dovendo eseguire n 1 moltiplicazioni per ottenere A, osserviamo che l ordine con cui le eseguiamo può cambiare il costo totale quando le matrici hanno taglia differente: nel seguito indichiamo con d i d i+1 la taglia della matrice A i, dove 0 i n 1. Date ad esempio n = 4 matrici tali che d 0 = 100, d 1 = 20, d 2 = 1000, d 3 = 2ed 4 = 50, nella seguente tabella mostriamo con le parentesi tutti i possibili ordini di valutazione del loro prodotto A = A 0 A 1 A 2 A 3 (di taglia d 0 d 4 ), riportando il corrispettivo costo totale di esecuzione per ottenere A :
20 Capitolo 6 Programmazione dinamica (A 0 (A 1 (A 2 A 3 )) d 2 d 3 d 4 + d 1 d 2 d 4 + d 0 d 1 d 4 = 1.200.000 (A 0 ((A 1 A 2 ) A 3 ) d 1 d 2 d 3 + d 1 d 3 d 4 + d 0 d 1 d 4 = 142.000 ((A 0 A 1 ) (A 2 A 3 )) d 0 d 1 d 2 + d 2 d 3 d 4 + d 0 d 2 d 4 = 7.100.000 (((A 0 A 1 ) A 2 ) A 3 ) d 0 d 1 d 2 + d 0 d 2 d 3 + d 0 d 3 d 4 = 2.210.000 ((A 0 (A 1 A 2 )) A 3 ) d 1 d 2 d 3 + d 0 d 1 d 3 + d 0 d 3 d 4 = 54.000 Per esempio, la quarta riga corrisponde all ordine naturale di moltiplicazione da sinistra verso destra: la moltiplicazione A 0 A 1 richiede d 0 d 1 d 2 operazioni e restituisce una matrice di taglia d 0 d 2 ; la successiva moltiplicazione per A 2 richiede d 0 d 2 d 3 operazioni e restituisce una matrice di taglia d 0 d 3 ); l ultima moltiplicazione per A 3 richiede d 0 d 3 d 4 operazioni. Sommando tali costi e sostituendo i valori di d 0,...,d 4, otteniamo un totale di 2.210.000 operazioni. Le altre righe della tabella mostrano che il costo complessivo del prodotto può variare in dipendenza dell ordine in cui sono effettuate le singole moltiplicazioni per ottenere lo stesso risultato: in questo caso, appare molto più conveniente effettuare le moltiplicazioni nell ordine indicato nella quinta riga, che fornisce un costo di sole 54.000 operazioni. Il problema che ci poniamo è quello di trovare la sequenza di moltiplicazioni che minimizzi il costo complessivo del prodotto A = A 0 A 1 A n 1 per una data sequenza di n + 1 interi positivi d 0,d 1,...,d n (ricordiamo la nostra ipotesi che il costo della moltiplicazione di due matrici di taglia r s e s t sia pari a r s t). Possiamo ottenere un modo efficiente di affrontare il problema considerando il sotto-problema di trovare il costo per effettuare il prodotto di un gruppo consecutivo di matrici, A i A i+1 A j, dove 0 i j n 1, indicando con M(i, j) il corrispondente costo minimo: chiaramente, in tal modo siamo anche in grado di risolvere il problema iniziale calcolando M(0,n 1). Possiamo immediatamente notare che M(i, i)=0 per ogni i, in quanto ha costo nullo calcolare il prodotto della sequenza composta dalla sola matrice A i. Se passiamo al caso di M(i,j) con i<j, osserviamo che possiamo ottenere la matrice A i A i+1 A j (di taglia d i d j+1 ) fattorizzandola come una moltiplicazione tra A i A r (di taglia d i d r+1 )ea r+1 A j (di taglia d r+1 d j+1 ), per un qualunque intero r tale che i r<j. Il costo di tale moltiplicazione è pari a d i d r+1 d j+1, a cui vanno aggiunti i costi minimi M(i,r) e M(r + 1,j) per calcolare rispettivamente A i A r e A r+1 A j. Supponiamo di aver già calcolato induttivamente quest ultimi costi per tutti i possibili valori r con i r<j: allora il costo minimo M(i,j) sarà dato dal minimo, al variare di r, tra i valori M(i,r)+M(r + 1,j)+d i d r+1 d j+1. Da
Programmare algoritmi 21 1 CostoMinimoIterativo( ): 2 for (i = 0; i < n; i = i+1) { 3 costi[i][i] = 0; 4 } 5 for (diagonale = 1; diagonale < n; diagonale = diagonale+1) { 6 for (i = 0; i < n-diagonale; i = i+1) { 7 j = i + diagonale; 8 costi[i][j] = + ; 9 for (r = i; r < j; r = r+1) { 10 costo = costi[i][r] + costi[r+1][j]; 11 costo = costo + d[i] d[r+1] d[j+1]; 12 if (costo < costi[i][j]) { 13 costi[i][j] = costo; 14 indice[i][j] = r; 15 } 16 } 17 } 18 } 19 return costi[0][n-1]; Codice 6.1 Algoritmo iterativo per il calcolo dei costi minimi di moltiplicazione M(i,j). ciò deriva la seguente regola ricorsiva: 0 M(i,j)= min ir<j M(i,r)+M(r + 1,j)+di d r+1 d j+1 se i j altrimenti (6.2) Calcoliamo una sola volta i valori M(i, j) memorizzandoli in una tabella dei costi, realizzata mediante un array bidimensionale costi di taglia n n, in modo tale che costi[i][j] =M(i, j) per 0 i j n 1 (gli elementi della tabella corrispondenti a valori di i e j tali che i>jnon sono utilizzati dall algoritmo). L algoritmo descritto nel Codice 6.1 effettua il calcolo dei valori M(i, j) secondo quanto appena illustrato e, basandosi sulla regola in (6.2), riempie l array costi dal basso in alto, a partire dai valori costi[i][i], per 0 i<n, fino a ottenere il valore costi[0][n 1] da restituire. A partire dai valori noti costi[i][i] =0 sulla diagonale 0 (righe 2 3), l algoritmo determina tutti i valori costi[i][j] sulla diagonale 1 (con 0 i<n 1e j = i +1), poi quelli sulla diagonale 2 (con 0 i<n 2ej = i +2), e così via, fino al valore costi[0][n 1] sulla diagonale n 1 (righe 5 18): come possiamo notare, gli elementi su una data diagonale sono identificati nel codice dai
22 Capitolo 6 Programmazione dinamica due indici i e j tali che 0 i<n diagonale e j = i + diagonale. A ogni iterazione, il ciclo alle righe 9 16 calcola il minimo costo. Osserviamo che la complessità dell algoritmo nel Codice 6.1 è polinomiale, in quanto esegue tre cicli annidati, ciascuno di n iterazioni al più, per un totale di O(n 3 ) operazioni (chiaramente, le istruzioni all interno di tali cicli possono essere eseguite in tempo costante rispetto al numero n di matrici da moltiplicare). Nel corso della sua esecuzione, inoltre, l algoritmo usa le matrici costi e indice, aventi ciascuna n righe e n colonne, e una quantità costante di altre locazioni di memoria. Da ciò possiamo concludere che la complessità temporale dell algoritmo è O(n 3 ), mentre la sua complessità spaziale è O(n 2 ).