Universita' di Ferrara Dipartimento di Matematica e Informatica. Algoritmi e Strutture Dati



Documenti analoghi
Tecniche avanzate di sintesi di algoritmi: Programmazione dinamica Algoritmi greedy

Programmazione dinamica

Introduzione alla tecnica di Programmazione Dinamica

Esercizi Capitolo 6 - Alberi binari di ricerca

Definire all'interno del codice un vettore di interi di dimensione DIM, es. int array[] = {1, 5, 2, 4, 8, 1, 1, 9, 11, 4, 12};

Siamo così arrivati all aritmetica modulare, ma anche a individuare alcuni aspetti di come funziona l aritmetica del calcolatore come vedremo.

Sommario della lezione

Convertitori numerici in Excel

Proof. Dimostrazione per assurdo. Consideriamo l insieme complementare di P nell insieme

Corso di Informatica

4 3 4 = 4 x x x 10 0 aaa

Funzioni in C. Violetta Lonati

Dimensione di uno Spazio vettoriale

Esercitazione Informatica I AA Nicola Paoletti

APPUNTI DI MATEMATICA LE FRAZIONI ALGEBRICHE ALESSANDRO BOCCONI

f(x) = 1 x. Il dominio di questa funzione è il sottoinsieme proprio di R dato da

Ricerca Operativa Esercizi sul metodo del simplesso. Luigi De Giovanni, Laura Brentegani

Introduzione al MATLAB c Parte 2

PROBLEMA DELLA RICERCA DI UN ELEMENTO IN UN ARRAY E ALGORITMI RISOLUTIVI

Algoritmi e Strutture Dati II: Parte B Anno Accademico Lezione 11

Algoritmi e Strutture Dati

Appunti sulla Macchina di Turing. Macchina di Turing

Matematica generale CTF

b. Che cosa succede alla frazione di reddito nazionale che viene risparmiata?

Corrispondenze e funzioni

Logica Numerica Approfondimento 1. Minimo Comune Multiplo e Massimo Comun Divisore. Il concetto di multiplo e di divisore. Il Minimo Comune Multiplo

LEZIONE 23. Esempio Si consideri la matrice (si veda l Esempio ) A =

Iniziamo con un esercizio sul massimo comun divisore: Esercizio 1. Sia d = G.C.D.(a, b), allora:

AA LA RICORSIONE

Appunti di informatica. Lezione 2 anno accademico Mario Verdicchio

Sequenziamento a minimo costo di commutazione in macchine o celle con costo lineare e posizione home (In generale il metodo di ottimizzazione

Calcolatori: Algebra Booleana e Reti Logiche

Esercizi su lineare indipendenza e generatori

RICORSIVITA. Vediamo come si programma la soluzione ricorsiva al problema precedente: Poniamo S 1 =1 S 2 =1+2 S 3 =1+2+3

Note su quicksort per ASD (DRAFT)

CALCOLO COMBINATORIO

Epoca k Rata Rk Capitale Ck interessi Ik residuo Dk Ek 0 S 0 1 C1 Ik=i*S Dk=S-C1. n 0 S

10 - Programmare con gli Array

LE FUNZIONI A DUE VARIABILI

Plate Locator Riconoscimento Automatico di Targhe

Esercizi per il corso di Algoritmi e Strutture Dati

UNA LEZIONE SUI NUMERI PRIMI: NASCE LA RITABELLA

Corso di Algoritmi e Strutture Dati Informatica per il Management Prova Scritta, 25/6/2015

ESEMPIO 1: eseguire il complemento a 10 di 765

Analisi di scenario File Nr. 10

GESTIONE INFORMATICA DEI DATI AZIENDALI

Algoritmi e strutture dati. Codici di Huffman

Esempi di algoritmi. Lezione III

INTRODUZIONE AGLI ALGORITMI INTRODUZIONE AGLI ALGORITMI INTRODUZIONE AGLI ALGORITMI INTRODUZIONE AGLI ALGORITMI

[MANUALE VISUAL BASIC SCUOLA24ORE PROF.SSA PATRIZIA TARANTINO] 14 dicembre 2008

Strutture. Strutture e Unioni. Definizione di strutture (2) Definizione di strutture (1)

void funzioneprova() { int x=2; cout<<"dentro la funzione x="<<x<<endl; }

Uso di JUnit. Fondamenti di informatica Oggetti e Java. JUnit. Luca Cabibbo. ottobre 2012

Parte 2. Determinante e matrice inversa

La selezione binaria

Studente: SANTORO MC. Matricola : 528

Lezione 9: Cambio di base

Un modello matematico di investimento ottimale

2.1 Definizione di applicazione lineare. Siano V e W due spazi vettoriali su R. Un applicazione

Progetto Lauree Scientifiche Liceo Classico L.Ariosto, Ferrara Dipartimento di Matematica Università di Ferrara 24 Gennaio 2012

Processo di risoluzione di un problema ingegneristico. Processo di risoluzione di un problema ingegneristico

APPUNTI SU PROBLEMI CON CALCOLO PERCENTUALE

Corso di Informatica Generale (C. L. Economia e Commercio) Ing. Valerio Lacagnina Rappresentazione in virgola mobile

Semantica Assiomatica

Realizzazione di Politiche di Gestione delle Risorse: i Semafori Privati

Informatica. Rappresentazione dei numeri Numerazione binaria

1 Applicazioni Lineari tra Spazi Vettoriali

A intervalli regolari ogni router manda la sua tabella a tutti i vicini, e riceve quelle dei vicini.

Anno 3. Funzioni: dominio, codominio e campo di esistenza

PROBABILITÀ - SCHEDA N. 2 LE VARIABILI ALEATORIE

Algoritmi di Ricerca. Esempi di programmi Java

1. PRIME PROPRIETÀ 2

Applicazioni lineari

Excel. A cura di Luigi Labonia. luigi.lab@libero.it

4. Operazioni elementari per righe e colonne

Matematica in laboratorio

Capitolo 2. Operazione di limite

Esercizi su. Funzioni

RAPPRESENTAZIONE GRAFICA E ANALISI DEI DATI SPERIMENTALI CON EXCEL

Sistema operativo: Gestione della memoria

ControlloCosti. Cubi OLAP. Controllo Costi Manuale Cubi

Informatica 3. LEZIONE 21: Ricerca su liste e tecniche di hashing. Modulo 1: Algoritmi sequenziali e basati su liste Modulo 2: Hashing

1. Limite finito di una funzione in un punto

Permutazione degli elementi di una lista

2. Leggi finanziarie di capitalizzazione

Allocazione dinamica della memoria - riepilogo

Per lo svolgimento del corso risulta particolarmente utile considerare l insieme

Equazioni alle differenze finite (cenni).

I database relazionali sono il tipo di database attualmente piu diffuso. I motivi di questo successo sono fondamentalmente due:

Sommario. Definizione di informatica. Definizione di un calcolatore come esecutore. Gli algoritmi.

Metodi e Modelli per l Ottimizzazione Combinatoria Il problema del flusso di costo minimo

Ottimizzazione delle interrogazioni (parte I)

Il principio di induzione e i numeri naturali.

LA MASSIMIZZAZIONE DEL PROFITTO ATTRAVERSO LA FISSAZIONE DEL PREZZO IN FUNZIONE DELLE QUANTITÀ

Informatica B. Sezione D. Scuola di Ingegneria Industriale Laurea in Ingegneria Energetica Laurea in Ingegneria Meccanica

Corso di Fondamenti di Informatica

Raccolta degli Scritti d Esame di ANALISI MATEMATICA U.D. 1 assegnati nei Corsi di Laurea di Fisica, Fisica Applicata, Matematica

Statistica e biometria. D. Bertacchi. Variabili aleatorie. V.a. discrete e continue. La densità di una v.a. discreta. Esempi.

Prof. Giuseppe Chiumeo. Avete già studiato che qualsiasi algoritmo appropriato può essere scritto utilizzando soltanto tre strutture di base:

SISTEMI DI NUMERAZIONE DECIMALE E BINARIO

Transcript:

Universita' di Ferrara Dipartimento di Matematica e Informatica Algoritmi e Strutture Dati Strategie per la progettazione di algoritmi: memoizzazione e programmazione dinamica Numeri di Fibonacci Coefficienti binomiali Problema subset sum semplificato Sequenza ottima per il calcolo del prodotto di n matrici non quadrate Copyright 2015 by Claudio Salati. Lez. 11c 1 Acknowledgments La prima parte di questa lezione e in gran parte derivata da dispense di: Maria Federico, @UniFe Ugo Vaccaro, @UniSa L ultima parte, relativa alla determinazione della sequenza ottima per il calcolo del prodotto di n matrici non quadrate e derivata da un esercizio svolto disponibile in rete a corredo del libro di testo P. Crescenzi, G. Gamboni, R. Grossi, G. Rossi, Strutture di dati e algoritmi, 2a ed., Pearson Il link alle risorse di rete associate al libro e : http://wps.pearsoned.it/crescenzi_strutture-dati-algoritmi2/ 2 1

Divide et Impera: criticita Tipicamente i sottoproblemi che si ottengono dall applicazione del passo Divide dello schema sono diversi e disgiunti Ciascun sottoproblema viene individualmente risolto dalla relativa chiamata ricorsiva una e una sola volta Ci sono pero situazioni in cui i sottoproblemi ottenuti, direttamente o ricorsivamente, al passo Divide potrebbero essere simili, o addirittura uguali In questo caso l algoritmo Divide et Impera risolverebbe lo stesso problema più volte, svolgendo lavoro inutile! La stessa criticita esiste utilizzando la metodologia di progettazione top-down se due diversi sottoproblemi, Pn e Pm, generano iterativamente uno stesso sottoproblema Ps Ps sarebbe risolto due volte in modo indipendente E per questo che la metodologia top-down e (deve essere) complementata dalla metodologia bottom-up 3 Esempio: calcolo dei numeri di Fibonacci La sequenza dei numeri di Fibonacci e definita dalla relazione di ricorrenza seguente: F 0 = 1 F 1 = 1 Per n 2: F n = F n-1 + F n-2 La sequenza dei numeri di Fibonacci e : 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987, 1597, 2584, 4181, 6765, 10946, 17711, Il k-esimo numero di Fibonacci è approssimato da F k Φk F k = 5 dove Φ = 1,61803398874989484820 (il numero aureo) Quindi i numeri di Fibonacci crescono esponenzialmente! Ma noi qui li tratteremo (barando!) come se fossero sempre rappresentabili attraverso il tipo C unsigned 4 2

Calcolo dei numeri di Fibonacci: fibonacci1() L algoritmo piu ovvio per il calcolo del k-esimo numero di Fibonacci e basato, banalmente, sulla definizione: unsigned fibonacci1(unsigned n) { if (n <= 1) return(1); return(fibonacci1(n-1) + fibonacci1(n-2)); L algoritmo e evidentemente in stile divide&conquer La complessita T1(n) di questo algoritmo e : T1(0) = T1(1) = O(1) T1(n) = T1(n-1) + T1(n-2) + O(1) se n 2 5 Calcolo di T1(n) (complessita di fibonacci1()) Poiche per n 2 e T1(n-1) T1(n-2), si ha che T1(n) T (n), dove T (0) = T (1) = O(1) T (n) = 2 * T (n-2) + O(1) se n 2 L albero di ricorsione e un albero binario completo di altezza n/2 e quindi un albero con 2 n/2 foglie Quindi T1(n) = O(2 n ) La complessita di questo algoritmo e esponenziale! Perche una complessita esponenziale? Perche la funzione fibonacci1() viene invocata per lo stesso valore n di ingresso piu volte! I sottoproblemi in cui il problema originale viene decomposto non sono distinti, ma l algoritmo ricorsivo si comporta come se lo fossero, e li risolve daccapo ogni volta 6 3

Esempio di esecuzione di fibonacci1() fibonacci1(5) fibonacci1(4) fibonacci1(3) fibonacci1(3) fibonacci1(2) fibonacci1(2) fibonacci1(1) fibonacci1(2) fibonacci1(1) fibonacci1(1) fibonacci1(0) fibonacci1(1) fibonacci1(0) fibonacci1(1) fibonacci1(0) fibonacci1(1) e calcolato 5 volte, fibonacci1(0) e calcolato 3 volte, fibonacci1(2) e calcolato 3 volte, fibonacci1(3) e calcolato 2 volte 7 Calcolo dei numeri di Fibonacci: fibonacci2() unsigned fibonacci2(unsigned n) { if (n <= 1) return(1); unsigned *memo = malloc((n+1)*sizeof(unsigned)); memset(memo, 0, (n+1)*sizeof(unsigned)); unsigned result = memofib(n, memo); free(memo); return(result); unsigned memofib(unsigned n, unsigned memo[]) { // memo[n]==0 memofib() non ha ancora calcolato F n if (n <= 1) return(1); if (memo[n]!= 0) return(memo[n]); return(memo[n] = memofib(n-1) + memofib(n-2)); L algoritmo utilizza la struttura dati ausiliaria unsigned memo[n] 8 4

Complessita di fibonacci2() Memorizziamo nel vettore memo[] i valori F k, 2 k n, quando li si calcola per prima volta: in questo modo nelle future chiamate ricorsive a memofib() per valori gia calcolati non ci sara bisogno di (ri- )calcolarli, ma bastera leggerli dal vettore memo[] Risparmiamo tempo di calcolo (complessita temporale) alle spese di un aumento di occupazione di memoria (complessita spaziale) N.B.: la complessita spaziale implica anche una complessita temporale almeno dello stesso ordine: in questo caso dobbiamo inizializzare il vettore memo[] La dimensione del vettorememo[] e pari a n, e quindi e O(n) anche il tempo necessario ad inizializzarlo (assumiamo O(1) la complessita di malloc() e free()) Prima di sviluppare la ricorsione per calcolare qualche quantita memofib(k), per qualche k<n, memofib() controlla se essa e stata calcolata precedentemente: se si, la ricorsione non viene sviluppata e la complessita temporale di memofib() si riduce O(1) Poiche per ognik n memofib(k) viene effettivamente calcolato una 9 sola volta la complessita di fibonacci2() e O(n) Wikipedia Memoizzazione e oltre La memoizzazione è una tecnica di programmazione che consiste nel salvare in memoria i valori restituiti da una funzione in modo da averli a disposizione per un riutilizzo successivo senza doverli ricalcolare Una funzione può essere memoizzata soltanto se soddisfa la trasparenza referenziale, cioè se non ha effetti collaterali e restituisce sempre lo stesso valore quando riceve in input gli stessi parametri Considerando le chiamate ricorsive di memofib() si vede che gli elementi del vettore memo[] sono calcolati e memorizzati nell ordine seguente: memo[2], memo[3], memo[4], memo[5] L ordine e l opposto di quello che vorremmo, cioe di quello delle attivazioni ricorsive: questo e tipico dell utilizzo della ricorsione Se forzazzimo l ordine di calcolo inverso, sostituendo la struttura ricorsiva dell algoritmo con quella iterativa, potremmo sfruttare il fatto che per il calcolo di F n (n>1) non serve conoscere tutti i valori 10 F 0..F n-1, bastano F n-2 e F n-1 5

Calcolo dei numeri di Fibonacci: fibonacci3() unsigned fibonacci3(unsigned n) { if (n <= 1) return(1); unsigned k, Fk_2, Fk_1, Fk; Fk_2 = Fk_1 = 1; k = 1; while (k < n) { k += 1; Fk = Fk_2 + Fk_1; Fk_2 = Fk-1; Fk_1 = Fk; return(fk); fibonacci3() ha complessita temporale O(n) come fibonacci2() fibonacci3() ha complessita spaziale O(1), a differenza di fibonacci2() che ha complessita spaziale O(n) 11 Programmazione dinamica La funzione fibonacci3() e un esempio di algoritmo sviluppato secondo la tecnica della programmazione dinamica Sia la memoizzazione che la programmazione dinamica si basano su uno stesso principio di base: Memorizzare le soluzioni di sottoproblemi che sono già state calcolate al fine di non doverle ricalcolare Di solito si parla di memoizzazione quando ci si propone di rendere più efficiente un algoritmo ricorsivo, conservandone la struttura ricorsiva di base Nel caso della programmazione dinamica, invece, viene dato maggiore rilievo ai dettagli della pianificazione delle operazioni che consentono di risolvere i vari sottoproblemi e quindi il problema complessivo Si rende esplicito l'ordine temporale di esecuzione delle operazioni e, di conseguenza, l algoritmo viene ad assumere una struttura iterativa Se la soluzione è derivata da un algoritmo ricorsivo, l algoritmo con programmazione dinamica non lo sara piu 12 6

n Esempio: calcolo del coefficiente binomiale r Il numero di modi con cui possiamo scegliere r oggetti da un insieme di n n elementi, cioe il numero delle combinazioni di n elementi a r a r, e r Per scegliere r oggetti da un insieme di n elementi, possiamo o scegliere di prendere il primo oggetto dell insieme: in questo caso dovremo scegliere i restanti r 1 oggetti n da un insieme di n 1 elementi, e questo si potra fare in r modi 1 1 o scegliere di non prendere il primo oggetto dell insieme: in questo secondo caso dovremo scegliere tutti gli r noggetti 1 da un insieme di r n 1 elementi, e questo si potra fare in modi n n 1 n 1 r r 1 r Quindi: = + n n! r E ricordando che: = r! ( n r )! n n n 1 0 si ha: = = 13 n Calcolo del coefficiente binomiale r unsigned binomial1(unsigned n, unsigned r) { assert(n >= r); if ((n == r) (r == 0)) return(1); return(binomial1(n-1, r-1) + binomial1(n-1, r)); Complessita : sia T(n, r) il numero di operazioni effettuate dall algoritmo binomial1(n, r), e sia T(n) = max r T(n, r) T(n) = c con c costante, se n=1 2 * T(n-1) + d con d costante, se n>1 Ed espandendo ricorsivamente: T(n) = 2*T(n-1)+d = 2*(2*T(n-2)+d)+d = 4*T(n-2)+(2*d+d) = k 1 2k T n k d 2i = 2 2 *T(n-2)+d*(1+2) = = + = ( ) i 0 = 14 7

n Calcolo del coefficiente binomiale r Ed espandendo ricorsivamente fino a k=n-1: k T(n) = ( ) 1 n 2 2k T n k d 2i 2n 1 T 1 d 2i + = + = = c * 2 n-1 + d * (2 n-1-1) = (c + d) * 2 n-1 d Quindi: T(n) = O(2 n ) i 0 = ( ) i = L algoritmo risolve lo stesso sottoproblema piu volte Esempio: albero (parziale) delle chiamate ricorsive di binomial(6, 4) 0 15 Memoizzazione della funzione binomial() L algoritmo binomial1() per il calcolo dei coefficienti binomiali e inefficiente in quanto calcola ripetutamente (mediante chiamate ricorsive a se stesso) valori gia calcolati in precedenza Il problema puo essere risolto con la tecnica della memoizzazione: Introduciamo una tabella i memo[i][j] che contenga i valori dei coefficienti binomiali j man mano che questi sono calcolati Ogni chiamata ricorsiva memobinomial(i, j) sara preceduta da un controllo i per verificare se il valore del coefficiente binomiale j e stato gia calcolato: Se si, non ci sara bisogno di sviluppare la ricorsione e ci limiteremo a leggerne il valore, memo[i][j], nella tabella Se no, per calcolarlo attiveremo la ricorsione, ma cio sara necessario una sola volta: dopo che lo avremo calcolato, ovviamente, lo memorizzeremo in memo[i][j] n r 16 8

Memoizzazione della funzione binomial() unsigned binomial2(unsigned n, unsigned r) { assert(n>=r); unsigned **memo = memoallocinit(n, r); unsigned result = memobinomial(n, r, memo); memofree(memo, n); return(result); unsigned memobinomial(unsigned n, unsigned r, unsigned **memo) { if (r == 0 n == r) return(1); if (memo[n][r] == 0) memo[n][r] = memobinomial(n 1, r 1) + memobinomial(n 1, r); return(memo[n][r]); 17 Memoizzazione della funzione combinations() unsigned **memoallocinit(unsigned n, unsigned r) { unsigned **memo = malloc((n+1) * sizeof(unsigned*)); int i = 0; for (i=0; i<=n; i+=1) { memo[i] = malloc((r+1) * sizeof(unsigned)); int j = 0; for (j=0; j<=r; j+=1) memo[i][j] = 0; // memo[i][j]==0 memo[i][j]==undefined void memofree(unsigned **memo, unsigned n) { int i = 0; for (i=0; i<=n; i+=1) free(memo[i]); free(memo); 18 9

Albero delle chiamate per binomial1() 3 5 2 4 1 12 3 4 3 2 7 1 3 2 3 13 2 0 2 3 4 2 1 2 8 11 1 2 2 2 14 17 1 2 2 5 0 1 6 1 1 9 0 1 10 1 1 15 0 1 16 1 1 18 3 3 L ordine delle chiamate ricorsive e indicato da un numero rosso a fianco dei rami dell albero 19 Albero di chiamate e memoizzazioni per binomial2() 3 5 6 2 4 1 10 3 4 4 5 3 2 7 1 3 2 3 11 12 2 2 3 0 2 3 4 2 1 2 8 9 1 2 2 2 1 2 2 1 5 0 1 6 1 1 0 1 1 1 0 1 1 1 Il valore dei coefficienti binomiali di forma e non viene effettivamente memoizzato 3 3 L ordine di memoizzazione e indicato da un numero blu a fianco del coefficiente binomiale al termine del cui calcolo la memoizzazione e avvenuta Le attivazioni ricorsive evitate grazie alla memoizzazione sono barrate 0 n n n 20 10

Struttura della matrice memo[][] Il valore dei coefficienti binomiali di forma e non viene effettivamente memoizzato: il loro valore e 1, e possiamo considerarlo noto a priori (in verde) Indichiamo in rosso l ordine di inserimento dei valori di coefficienti binomiali che sono effettivamente calcolati In grigio sono indicati gli elementi della matrice memo[][] che sono strutturalmente ignorati dall algoritmo 0 1 2 3 0 1 1 1 1 2 1 1 1 3 1 2 3 1 4 1 4 5 5 1 6 Il problema del calcolo del coefficiente binomiale potrebbe quindi essere risolto calcolando nell ordine appropriato tutti gli elementi rilevanti della matrice memo[][] fino all elemento di indici [n][r] 21 0 n n n N.B.: le frecce indicano quali elementi di memo contribuiscono al calcolo di quali altri elementi: memo[i][j] = memo[i-1][j-1] n r + memo[i-1][j] Regole di calcolo per memo[][] A noi interessa calcolare l elemento di indici [n][r]: per poterlo calcolare e pero necessario conoscere l elemento di indici [n-1][r], e per poter calcolare questo, ricorsivamente, fino all elemento di indici [r][r] che e noto, in quanto vale 1 Nella colonna r dobbiamo cioe calcolare n-r elementi, ovviamente in ordine di numero di riga crescente, da quello di riga r+1 a quello di riga n Ma per poter calcolare questi n-r elementi della colonna r e necessario conoscere gli n-r elementi della colonna r-1 che si trovano nelle righe da r a n-1 E questo argomento si estende fino alla necessita di calcolare n-r elementi della colonna 1, da quello di riga 2 a quello di riga n-r+1 Ma gli elementi di colonna 1 sono tutti calcolabili, visto che quelli di colonna 0 sono noti: valgono tutti 1 In realta, in colonna 0 ci interessano solo gli elementi delle righe da 1 a n-r 22 11

Regole di calcolo per memo[][] Il calcolo degli elementi di memo[][] avviene quindi colonna dopo colonna, a partire da colonna 1 e fino a colonna r In ogni colonna si calcolano n-r elementi: nella colonna k, quelli che vanno da riga k+1 a riga k+(n-r) Il calcolo degli elementi di una colonna avviene riga dopo riga, dalla riga di indice minore a quella di indice maggiore Notare che della riga di indice 0 non ce ne facciamo niente: ma siccome la colonna di indice 0 e essenziale, risulta comodo tenere anche la riga di indice 0 Notare anche che, utilizzando per l allocazione della matrice memo[][] la funzione memoallocinit(), potremmo allocare righe della lunghezza strettamente necessaria a contenere solo i loro elementi significativi In realta, nel seguito, faremo riferimento ad una funzione memoalloc() che assumeremo non effettuare l allocazione in forma ottimizzata ma non effettuare nemmeno nessuna 23 inizializzazione della matrice Esempi di matrice memo[][] per il calcolo di 5 1 4 7 5 1 6 8 5 1 6 7 6 1 8 6 1 9 6 1 8 6 Calcolo di 2 Calcolo di 4 Calcolo di 3 6 n = 6, r = 2 n = 6, r = 3 n = 6, r = 4 n-r = 4 L inizializzazione delle caselle gialle e inutile n r 0 1 2 0 1 2 3 0 1 2 3 4 0 1 0 1 0 1 1 1 1 1 1 1 1 1 1 2 1 1 1 2 1 1 1 2 1 1 1 3 1 2 5 3 1 2 4 1 3 1 2 3 1 4 1 3 6 4 1 3 5 7 4 1 4 5 1 n-r = 3 L inizializzazione delle caselle gialle e inutile n-r = 2 L inizializzazione delle caselle gialle e inutile N.B.: numeri in rosso indicano l ordine di calcolo dell elemento corrispondente 24 12

Algoritmo iterativo per il calcolo di n r unsigned binomial3(unsigned n, unsigned r) { assert(n>=r); if (r == 0 n == r) return(1); unsigned **memo = memoalloc(n, r); int row, col, diag; for (diag=0; diag<=r; diag+=1) memo[diag][diag] = 1; for (row=0; row<=n-r; row+=1) memo[row][0] = 1; for (col=1; col<=r; col+=1) { for (row=col+1; row<=col+(n-r); col+=1) { memo[row][col] = memo[row-1][col-1] + memo[row-1][col]; unsigned result = memo[n][r]; memofree(memo, n); return(result); 25 Complessita di binomial3() La dimensione di memo[][] e al massimo O(n 2 ), ogni suo elemento viene calcolato al massimo una volta, e questo calcolo ha complessita O(1) Quindi la complessita di binomial3() e al massimo O(n 2 ) In realta, pero, vediamo che non tutti gli elementi di memo[][] sono utilizzati, quindi potremmo chiederci se la complessita di binomial3() non sia in realta minore di O(n 2 ) Guardando binomial3() si vede che la sua complessita e circa proporzionale a r+(n-r)+r*(n-r) = n+r*(n-r) Quando r=1 o r=n-1 la complessita di binomial3() e effettivamente O(n)! Quando pero r=n/2 la complessita diventa n+n 2 /4, quindi O(n 2 ) Quindi la complessita di binomial3() e O(n 2 ) N.B.: binomial3() ha complessita spaziale O(n 2 ); poiche pero ad ogni iterazione del ciclo principale si utilizzano solo due colonne di memo[][] l algoritmo potrebbe essere riscritto per avere complessita spaziale O(n) Anche binomial3() e un esempio di algoritmo progettato con la tecnica della programmazione dinamica 26 13

Memoizzazione e programmazione dinamica Memoizzazione e programmazione dinamica condividono il principio di base: ricordare le soluzioni di sottoproblemi che sono gia state calcolate per evitare di tornarle a calcolare Entrambe queste strategie sono significative quando la scomposizione del problema originario porta a sottoproblemi non indipendenti Per la registrazione dei risultati parziali si possono utilizzare array, come nei nostri esempi, o strutture dati piu complesse La cosa fondamentale e che sia possibile recuperare i risultati registrati in modo efficiente Nel caso della programmazione dinamica il punto e di pianificare le operazioni che consentono di risolvere (in maniera efficiente) i vari sottoproblemi e quindi il problema complessivo Si rende esplicito l'ordine temporale di soluzione dei sottoproblemi (che invece nel caso della memoizzazione rimane implicito) e, di conseguenza, l algoritmo viene ad assumere una struttura iterativa 27 Programmazione dinamica Come la strategia divide&conquer, anche la programmazione dinamica si basa sulla scomposizione di un problema in sottoproblemi, e sulla risoluzione di un sottoproblema per combinazione delle soluzioni dei suoi sottoproblemi Quindi, per combinazione di soluzioni di problemi di dimensioni inferiori In particolare, la strategia della programmazione dinamica e di tipo bottom-up e prevede che si calcolino le soluzioni di tutti i possibili sottoproblemi, a partire da quelli di dimensione minima, e che a partire da sottosoluzioni si ricavano nuove sottosoluzioni di problemi di dimensioni maggiori, fino a risolvere il problema originale Si fa una scelta ad ogni passo per decidere quali sottoproblemi (gia risolti) utilizzare per risolvere sottoproblemi ancora non risolti Tra i problemi deve esserci un ordinamento naturale, dal piu piccolo al piu grande, ed una formula ricorsiva che permetta di determinare la soluzione per un sottoproblema dalla soluzione di un certo 28 numero di sottoproblemi piu piccoli 14

Esempio: il problema subset sum semplificato Dato un insieme di n numeri naturali positivi (eventualmente con ripetizioni) esiste un suo sottoinsieme sommando i cui elementi si ottiene il numero naturale (non negativo) S? Quello che faremo e progettare un algoritmo int subsetsum(unsigned uvect[], unsigned j, unsigned k); che restituisca valore TRUE (un booleano, rappresentato alla maniera C) se e solo se esiste un sottoinsieme dei primi j elementi del vettore uvect[] la cui somma sia pari a k (altrimenti restituisca FALSE) Per risolvere il problema indicato bastera quindi chiamare subsetsum(uvect, n, S) con uvect[] che contiene gli n numeri dell insieme dato 29 Esempio: il problema subset sum semplificato Basandosi sulla strategia divide&conquer scrivere la funzione subsetsum() e molto semplice: int subsetsum1(unsigned uvect[], unsigned j, unsigned k) { if (j == 0) return(k == 0); // TRUE iff k==0 assert(j>0); return( // se non vogliamo che uvect[j-1] // faccia parte del sottoinsieme soluzione subsetsum1(uvect, j-1, k) // se vogliamo che uvect[j-1] faccia parte // del sottoinsieme soluzione e questo e // possibile (k >= uvect[j-1] && subsetsum1(uvect, j-1, k-uvect[j-1])) ); 30 15

Complessita di subsetsum1() Detto T1(n) il tempo di esecuzione di subsetsum1() (considerando il caso peggiore per il valore di k), si ha che T1(n) = con c e d costanti c se n = 0 2 * T1(n-1) + d se n > 0 Analogamente a quanto si e gia visto per la complessita di binomial1() questa definizione ricorsiva porta ad una complessita esponenziale: T1(n) = O(2 n ) Notare che T1(n) non dipende da S! In realta, per il calcolo di T1(n) si e assunto per S il caso peggiore (vedi seguito per una discussione ulteriore) Quello che fa sostanzialmente subsetsum1() e procedere per tentativi: vengono generati (sostanzialmente) tutti i possibili sottoinsiemi di uvect[] e si verifica se per almeno uno di questi la sommatoria degli elementi e pari a S 31 Complessita di subsetsum1() Albero di attivazioni ricorsive di subsetsum1(uvect, n, S) uvect[n-1] sottoinsieme? uvect[n-1] sottoinsieme uvect[n-2] sottoinsieme? uvect[n-1] sottoinsieme uvect[n-2] sottoinsieme? uvect[n-1] sottoinsieme uvect[n-2] sottoinsieme uvect[n-3] sottoinsieme? uvect[n-1] sottoinsieme uvect[n-2] sottoinsieme uvect[n-3] sottoinsieme? uvect[n-1] sottoinsieme uvect[n-2] sottoinsieme uvect[n-3] sottoinsieme? uvect[n-1] sottoinsieme uvect[n-2] sottoinsieme uvect[n-3] sottoinsieme? Percorrendo questo albero dalla radice ad una foglia si ottiene (in modo univoco) uno dei sottoinsiemi di uvect[] Dato che la cardinalita del powerset di un insieme di n elementi e 2 n, e naturale che la complessita di subsetsum1() sia O(2 n ) Il ruolo di S, se esso e minore di ΣuVect[], e di potare, eventualmente, alcune parti dell albero Se, ad esempio, S=0, tutti i rami in cui appare uvect[j] sottoinsieme, per qualsiasi j, non verrebbero percorsi 32 16

Complessita di subsetsum1() Anche in questo caso possiamo applicare la strategia della programmazione dinamica per migliorare la complessita temporale di subsetsum() al prezzo di un aumento della sua complessita spaziale, introducendo una matrice memo[n+1][s] in cui nell elemento [j][k] viene memorizzato il risultato del calcolo di subsetsum(, j, k) In realta pero dovremmo farci una domanda: perche la strategia della programmazione dinamica risulti conveniente occorre che la scomposizione porti a generare sottoproblemi non indipendenti: Nel nostro caso, possiamo considerare non indipendenti i sottoproblemi solo perche coinvolgono lo stesso prefisso di uvect[]? Il valore k in generale e diverso nei diversi sottoproblemi: non e proprio lo stesso sottoproblema! 33 Struttura e calcolo di memo[][] Il valore di memo[j][k] e posto a TRUE se e solo se (altrimenti e posto a FALSE) E TRUE il valore di memo[j-1][k], o Esiste ed ha valore TRUE l elemento memo[j-1][k-uvect[j-1]] (N.B.: questo potrebbe anche non esistere: capita se l indice k-uvect[j-1] risulta negativo!) Per calcolare memo[j][k] occorre che la riga j 1 sia stata gia calcolata, quindi calcoleremo la matrice memo[][] riga per riga, da sinistra a destra, e dall alto in basso La riga j di memo[][] ci dice quali sono i valori di S per cui subsetsum(, j, S) ritorna TRUE oppure ritorna FALSE La riga 0 rappresenta il sottoinsieme vuoto di uvect[]; subsetsum(, 0, S) ritorna TRUE solo se S=0, altrimenti ritorna FALSE 34 17

Programmazione dinamica e problema subset sum int subsetsum2(unsigned uvect[], unsigned n, unsigned S) { int k, result; int**memo = memoalloc(n, S); memo[0][0] = TRUE; for (k=1; k<=s; k+=1) memo[0][k] = FALSE; for (j=1; j<=n; j+=1) { for (k=0; k<=s; k+=1) { if (k>=uvect[j-1]) memo[j][k] = memo[j-1][k] memo[j-1][k-uvect[j-1]]; else memo[j][k] = memo[j-1][k]; // end if result = memo[n][s]; memofree(memo, n); return(result); 35 Esempio uvect[7] == {1, 2, 2, 4, 5, 2, 4 S == 15 Memo[8][15] 36 18

Complessita di subsetsum2() La complessita temporale di subsetsum2() e evidentemente O(n*S) Notare che in questo caso nell espressione della complessita il valore di S compare esplicitamente! Anche la complessita spaziale di subsetsum2() e evidentemente O(n*S) Ma si vede facilmente che potrebbe essere ridotta a O(S) perche ad ogni istante ci basta avere a disposizione solo 2 righe di memo[][] In realta dall esempio si vede bene che all interno di memo[][] viene ricostruito l albero di enumerazione di tutti i sottoinsiemi di uvect[], salvo per la potatura operata dal valore di S Potatura che avviene comunque anche in subsetsum1() In questo caso, quindi, la programmazione dinamica non porta nessun vantaggio 37 Enumerazione dei sottoinsiemi in memo[][] uvect[0] uvect[0] uvect[1] Sottoinsieme vuoto uvect[1] 38 19

Programmazione dinamica e problemi di ottimizzazione Lo sviluppo di un algoritmo di programmazione dinamica prevede i seguenti passi: 1. Caratterizzazione di una soluzione ottima 2. Definizione ricorsiva del valore di una soluzione ottima 3. Calcolo del valore di una soluzione ottima, secondo una strategia bottom-up: dal sottoproblema piu piccolo al sottoproblema piu grande 4. Costruzione della soluzione ottima (si omette se e richiesto solo il valore di una soluzione ottima) Il presupposto e che la soluzione ottima del problema contiene al suo interno la soluzione ottima dei sottoproblemi Il primo passo per lo sviluppo di un algoritmo di programmazione dinamica richiede l individuazione di una collezione di sottoproblemi derivati dal problema originale dalle cui soluzioni puo poi essere facilmente calcolata la soluzione del problema originario 39 Programmazione dinamica: quanti sottoproblemi? L insieme dei sottoproblemi distinti da risolvere deve essere piccolo, deve cioe avere cardinalita molto inferiore rispetto all insieme delle soluzioni possibili da cui vogliamo selezionare quella ottima Se non fosse cosi si potrebbe procedere per tentativi, enumerando tutte le soluzioni possibili e confrontandole tra di loro Se non fosse cosi la programmazione dinamica non porterebbe nessun vantaggio! Se cio e vero significa che uno stesso problema deve comparire molte volte come sottoproblema di altri problemi 40 20

Esempio: prodotto di piu matrici 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 le cui dimensioni sono specificate 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 Esempio: consideriamo il caso del prodotto di 4 matrici A 0,, A 3 tali che d 0 =100, d 1 =20, d 2 =1000, d 3 =2 e d 4 =50 41 Esempio: prodotto di piu matrici Consideriamo 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 Indichiamo con m(i, j) il corrispondente costo minimo Per risolvere il problema iniziale basta calcolare m(0, n 1) In realta quello che abbiamo fatto qui e cercare il valore di una soluzione ottima: il problema originario ci chiede anche di costruirla! Consideriamo il caso m(i, j) con i < j Possiamo ottenere la matrice A i A i+1 A j (di dimensioni d i d j+1 ) fattorizzandola come una moltiplicazione tra A i A r (di dimensioni d i d r+1 ) e A r+1 A j (di dimensioni d r+1 d j+1 ), per un qualunque intero r tale che i r < j Il costo della moltiplicazione finale e 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) (e evidente come, in questo caso, solo soluzioni parziali ottime possono portare ad una soluzione globale ottima) necessari per calcolare rispettivamente A i A r e A r+1 A j Supponiamo di aver gia calcolato induttivamente questi ultimi costi per tutti i possibili valori r con i r < j: allora m(i, j) sara 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 42 21

Casi particolari: Esempio: prodotto di piu matrici Evidentemente, per ogni i, e m(i, i)=0 Se j=i+1 siamo nel caso del prodotto di 2 matrici adiacenti: r vale necessariamente i e il costo di tale prodotto e d i d i+1 d i+2 Riassumendo: m(i, j) = non significativo se j < i 0 se j = i d i d i+1 d i+2 se j = i+1 min i r < j { m(i, r) + m(r+1, j) + d i d r+1 d j+1 se i+1 < j Calcoliamo una sola volta i valori m(i, j) memorizzandoli in una tabella dei costi, realizzata mediante un array bidimensionale memo[n][n], in modo tale che memo[i][j] = m(i, j) per 0 i j n 1 Ovviamente, gli elementi della tabella corrispondenti a valori di i e j tali che i > j, che non sono significativi, non sono utilizzati dall algoritmo Quindi memo[][] e in realta una matrice triangolare superiore in cui tutti gli elementi diagonali valgono 0 43 Procedura basata sulla programmazione dinamica typedef struct memoel { unsigned min; unsigned r; memoel; void matrixproductplan(unsigned dim[], memoel **memo, unsigned n) { int i, j, r, diag; unsigned c; for (i=0; i<n; i+=1) { memo[i][i].min = 0; // r senza senso for (i=0; i<n-1; i+=1) { memo[i][i+1].min = d[i]*d[i+1]*d[i+2]; memo[i][i+1].r =i; for (diag=2; diag<n; diag+=1) { for (i=0; i<n-diag; i+=1) { j = i + diag; memo[i][j].min = + ; for (r=i; r<j; r+=1) { c = memo[i][r].min + memo[r+1][j].min + d[i]*d[r+1]*d[j+1]; if (c < memo[i][j].min) { memo[i][j].min = c; memo[i][j].r = r; 44 22

Note alla procedura matrixproductplan() L arraymemo[][] deve essere allocato da parte del chiamante e passato in ingresso alla procedura che si occupera poi di riempirlo Il chiamante recuperera il risultato primario calcolato da matrixproductplan() leggendo il valore dell elemento di indici [0][n-1] di memo[][] In realta il valore memo[0][n-1].r indica solo il primo livello di parentesizzazione della sequenza di calcolo individuata I livelli successivi, e via cosi ricorsivamente, saranno indicati per le due parti dagli elementi memo[0][memo[0][n-1].r].r e memo[0][memo[0][n-1].r].r Struttura della matrice memo[][] In grigio gli elementi non significativi In rosso, inizializzazione degli elementi diagonali [i][i] In blu, inizializzazione di prodotti semplici, elementi [i][i+1] In verde il ciclo principale 45 Note alla procedura matrixproductplan() Gli elementi della diagonale 1 (linea blu) avrebbero potuto essere trattati come parte del ciclo generale successivo A partire dai valori noti memo[i][i].min == 0 sulla diagonale 0 e memo[i][i+1].min == d[i]*d[i+1]*d[i+2] sulla diagonale 1 con 0 i < n 1 l algoritmo determina tutti i valori memo[i][j].min sulla diagonale 2 (con 0 i < n 2 e j = i+2), e cosi via, fino al valore memo[0][n-1].min sulla diagonale n 1 Nota che, ogni volta che si deve utilizzare un valore memo[i][j].min, questo e gia stato calcolato in precedenza La complessita temporale dell algoritmo matrixproductplan() e polinomiale, in quanto esegue tre cicli annidati, ciascuno di al piu n iterazioni, per un totale di O(n 3 ) operazioni (chiaramente, le istruzioni all interno di tali cicli sono ciascuna di complessita O(1)) La complessita spaziale dell algoritmo matrixproductplan() e O(n 2 ) 46 23