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∗ = A0 ×
A1 × · · · × An−1 di n matrici, dove la loro taglia è specificata mediante una
sequenza di n+ 1 interi positivi d0 , d1 , . . . , dn : la matrice Ai ha taglia di × di+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 e 0 � 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)
PA
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:

max{i, j}




 D(i − 1, j − 1)
D(i, j) = min D(i − 1, j − 1) + 1


 D(i, j − 1) + 1


D(i − 1, j) + 1
se i = 0 o j = 0
se i, j > 0 e x[i − 1] = y[j − 1]
se i, j > 0 e x[i − 1] �= y[j − 1]
se j > 0 e x[i − 1] �= y[j − 1]
se i > 0 e x[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],
12
13
14
}
distanza[i][j-1], distanza[i-1][j] };
}
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
PA
Programmare algoritmi
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∗ = A0 × A1 × · · · × An−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 di × di+1 la taglia della matrice Ai , dove 0 � i � n − 1. Date ad esempio n = 4 matrici tali che d0 =
100, d1 = 20, d2 = 1000, d3 = 2 e d4 = 50, nella seguente tabella mostriamo con le parentesi tutti i possibili ordini di valutazione del loro prodotto
A∗ = A0 × A1 × A2 × A3 (di taglia d0 × d4 ), riportando il corrispettivo costo
totale di esecuzione per ottenere A∗ :
19
PA
20
Capitolo 6 – Programmazione dinamica
(A0 × (A1 × (A2 × A3 ))
(A0 × ((A1 × A2 ) × A3 )
((A0 × A1 ) × (A2 × A3 ))
(((A0 × A1 ) × A2 ) × A3 )
((A0 × (A1 × A2 )) × A3 )
d2 d3 d4 + d1 d2 d4 + d0 d1 d4 = 1.200.000
d1 d2 d3 + d1 d3 d4 + d0 d1 d4 = 142.000
d0 d1 d2 + d2 d3 d4 + d0 d2 d4 = 7.100.000
d0 d1 d2 + d0 d2 d3 + d0 d3 d4 = 2.210.000
d1 d2 d3 + d0 d1 d3 + d0 d3 d4 = 54.000
Per esempio, la quarta riga corrisponde all’ordine naturale di moltiplicazione da sinistra verso destra: la moltiplicazione A0 × A1 richiede d0 × d1 × d2
operazioni e restituisce una matrice di taglia d0 × d2 ; la successiva moltiplicazione per A2 richiede d0 × d2 × d3 operazioni e restituisce una matrice di
taglia d0 × d3 ); l’ultima moltiplicazione per A3 richiede d0 × d3 × d4 operazioni. Sommando tali costi e sostituendo i valori di d0 , . . . , d4 , 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∗ = A0 × A1 × · · · × An−1 per
una data sequenza di n + 1 interi positivi d0 , d1 , . . . , dn (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, Ai × Ai+1 × · · · × Aj , 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 Ai . Se
passiamo al caso di M(i, j) con i < j, osserviamo che possiamo ottenere la
matrice Ai × Ai+1 × · · · × Aj (di taglia di × dj+1 ) fattorizzandola come una
moltiplicazione tra Ai × · · · × Ar (di taglia di × dr+1 ) e Ar+1 × · · · × Aj (di
taglia dr+1 × dj+1 ), per un qualunque intero r tale che i � r < j. Il costo di tale
moltiplicazione è pari a di × dr+1 × dj+1 , a cui vanno aggiunti i costi minimi
M(i, r) e M(r + 1, j) per calcolare rispettivamente Ai × · · · × Ar e Ar+1 × · · · ×
Aj . 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) + di dr+1 dj+1 . Da
PA
Programmare algoritmi
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:
M(i, j) =
�
0
�
� se i � j
mini�r<j M(i, r) + M(r + 1, j) + di dr+1 dj+1
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 > j non 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 − 1 e
j = i + 1), poi quelli sulla diagonale 2 (con 0 � i < n − 2 e j = 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
21
PA
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(n3 ) 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(n3 ), mentre la sua complessità spaziale è O(n2 ).