Programmazione dinamica Algoritmi e Strutture Dati Capitolo Programmazione dinamica Alberto Montresor Università di Trento This work is licensed under the Creative Commons AttributionNonCommercialShareAlike License. To view a copy of this license, visit http://creativecommons.org/licenses/byncsa/./ or send a letter to Creative Commons, Howard Street, th Floor, San Francisco, California, 9, USA. Divideetimpera Tecnica ricorsiva Approccio topdown (problemi divisi in sottoproblemi) Vantaggioso solo quando i sottoproblemi sono indipendenti Altrimenti, gli stessi sottoproblemi possono venire risolti più volte Programmazione dinamica Tecnica iterativa Approccio bottomup Vantaggiosa quando ci sono sottoproblemi in comune Esempio semplice: il triangolo di Tartaglia Coefficienti binomiali Triangolo di Tartaglia Coefficienti binomiali C(n, k) = Il numero di modi di scegliere k oggetti da un insieme di n oggetti I coefficienti di un equazione di grado n n = k ( n! k! (n k)! = se k =_ k = n C(n,k ) + C(n,k) altrimenti Versione ricorsiva Deriva direttamente dalla definizione Domanda Complessità? nx (a + b) n = C(n, k)a n k= k b k integer C(integer n, k) if n = k or k =then return else return C(n,k ) + C(n,k)
Triangolo di Tartaglia Quando applicare la programmazione dinamica? Versione iterativa Sottostruttura ottima Basata su programmazione dinamica Domanda Complessità? tartaglia(integer n, integer[][]c) for i to n do C[i, ] C[i, i] for i to n do for j to i do C[i, j] C[i,j ] + C[i,j] E' possibile combinare le soluzioni dei sottoproblemi per trovare la soluzione di un problema più grande PS: In tempo polinomiale! Le decisioni prese per risolvere un problema rimangono valide quando esso diviene un sottoproblema di un problema più grande Sottoproblemi ripetuti Un sottoproblema può occorrere più volte Spazio dei sottoproblemi Deve essere polinomiale Programmazione dinamica Catena di moltiplicazione di matrici Caratterizzare la struttura di una soluzione ottima Definire ricorsivamente il valore di una soluzione ottima La soluzione ottima ad un problema contiene le soluzioni ottime ai sottoproblemi Calcolare il valore di una soluzione ottima bottomup (cioè calcolando prima le soluzioni ai casi più semplici) Si usa una tabella per memorizzare le soluzioni dei sottoproblemi Evitare di ripetere il lavoro più volte, utilizzando la tabella Costruire la (una) soluzione ottima. Problema: Data una sequenza di matrici A, A, A,, An, compatibili a al prodotto, vogliamo calcolare il loro prodotto. Cosa vogliamo ottimizzare La moltiplicazione di matrici si basa sulla moltiplicazione scalare come operazione elementare. Vogliamo calcolare il prodotto impiegando il numero minore possibile di moltiplicazioni Attenzione: Il prodotto di matrici non è commutativo......ma è associativo: (A A) A = A (A A) 7 8
Catena di moltiplicazione tra matrici matrici: A B C x x x Catena di moltiplicazione tra matrici matrici: A B C D x x x x (A B ) ((A B ) C ) (B C) (A (B C)) # Moltiplicazioni Memoria = = = = ((( A B ) C ) D ) : 87 moltiplicazioni (( A ( B C )) D ) : moltiplicazioni (( A B ) ( C D )) : moltiplicazioni ( A (( B C ) D )) : moltiplicazioni ( A ( B ( C D ))) : moltiplicazioni ((( A B ) C ) D ) : 87 ( A B ) = (( A B ) C ) = (( A B ) C ) D = 7 87 9 Applicare la programmazione dinamica Parentesizzazione Le fasi principali: Definizione: Una parentesizzazione Pi,j del prodotto Ai Ai+ Aj consiste Caratterizzare la struttura di una soluzione ottima nella matrice Ai, se i = j; Definire ricorsivamente il valore di una soluzione ottima Calcolare il valore di una soluzione ottima bottomup (dal basso verso l alto) nel prodotto di due parentesizzazioni (Pi,k Pk+,j), altrimenti. (A (A A )) (A (A A )) Costruzione di una soluzione ottima Nei lucidi successivi: Vediamo ora una ad una le quattro fasi del processo di sviluppo applicate al problema della parentesizzazione ottima Esempio: (A (A A )) (A (A A )) k= Ultima moltiplicazione (A (A A )) (A (A A )) A (A A ) A (A A ) A A A A
Parentesizzazione ottima Parentesizzazione ottima Parentesizzazione ottima Definiamo una relazione di ricorrenza Determinare il numero di moltiplicazioni scalari necessari per i prodotti tra le matrici in ogni parentesizzazione Scegliere una delle parentesizzazioni che richiedono il numero minimo di moltiplicazioni P(n): numero di parentesizzazioni per n matrici A A A An L'ultima moltiplicazione può occorrere in n posizioni diverse Fissato l'indice k dell'ultima moltiplicazione, abbiamo P(k) parentesizzazioni per A A A Ak Motivazione: P(nk) parentesizzazioni per Ak+ Ak+ An Vale la pena di spendere un po' di tempo per cercare la parentesizzazione migliore, per risparmiare tempo dopo Domanda P n P (n) = k= P (k)p (n k) n> n = Quante sono le parentesizzazioni possibili? n=, n=, n=??? n 7 8 9 P(n) 9 8 Parentesizzazione ottima Definizioni Equazione di ricorrenza: Denoteremo nel seguito con: P n P (n) = k= P (k)p (n k) n> n = A A A An il prodotto di n matrici da ottimizzare ci il numero di righe della matrice Ai Soluzione: nesimo numero catalano P (n) =C(n ) = n n = n + n n / ci il numero di colonne della matrice Ai A[i..j] il prodotto Ai Ai+ Aj P[i..j] una parentesizzazione di A[i..j] (non necessariamente ottima) Domanda: Più semplicemente, dimostrare che P(n) = Ω( n ) Conseguenza: la forza bruta (tentare tutte le possibili parentesizzazioni) non funziona
Struttura di una parentesizzazione ottima Struttura di una parentesizzazione ottima Sia A[i..j] = Ai Ai+ Aj una sottosequenza del prodotto di matrici Teorema (sottostruttura ottima) Si consideri una parentesizzazione ottima P[i..j] di A[i..j] Esiste una ultima moltiplicazione : in altre parole, esiste un indice k tale che P[i..j] = P[i..k] P[k+..j] Se P[i..j] = P[i..k] P[k+..j] è una parentesizzazione ottima del prodotto A[i..j], allora P[i..k] e P[k+..j] sono parentesizzazioni ottime dei prodotti A[i..k] e A[k+..j], rispettivamente. Domanda: Quali sono le caratteristiche delle due sottoparti P[i..k] e P[k+..j]? ( P[i..k] ) ( P[k+..j] ) P[i..k] P[k+..j]?? Dimostrazione Ragionamento per assurdo Supponiamo esista un parentesizzazione ottima P'[i..k] di A[i..k] con costo inferiore a P[i..k] Allora, P'[i..k] P[k+..j] sarebbe una parentesizzazione di A[i..j] con costo inferiore a P[i..j], assurdo. 7 8 Struttura di una parentesizzazione ottima Definire ricorsivamente il valore di una soluzione ottima In altre parole: Il teorema afferma che esiste una sottostruttura ottima: Ogni soluzione ottima al problema della parentesizzazione contiene al suo interno le soluzioni ottime dei due sottoproblemi Programmazione dinamica: L'esistenza di sottostrutture ottime è una delle caratteristiche da cercare per decidere se la programmazione dinamica è applicabile Definizione: sia M[i,j] il numero minimo di prodotti scalari richiesti per calcolare il prodotto A[i,j] Come calcolare M[i,j]? Caso base: i=j. Allora, M[i,j] = Passo ricorsivo: i < j. Esiste una parentesizzazione ottima P[i..j] = P[i..k] P[k+..j]; sfruttiamo la ricorsione: M[i,j] = M[i,k] + M[k+,j] + ci ck cj Prossima fase: Definire ricorsivamente il costo di una soluzione ricorsiva Prodotti per P[i..k] Prodotti per P[k+..j] Prodotto di una coppia di matrici: u n. righe: prima matrice u n. colonne: ultima matrice 9
Definire ricorsivamente il valore di una soluzione ottima Ma qual è il valore di k? Non lo conosciamo...... ma possiamo provarli tutti! k può assumere valori fra i e j La formula finale: M[i,j] = mini k < j { M[i,k] + M[k+, j] + ci ck cj } Esempio L R L R M[,] = min k < { M[,k] + M[k+,] + c c k c } = M[,] + M[,] + c c c = c c c i \ j Esempio L R L R L R M[,] = min k< { M[,k] + M[k+,] + c c k c } = min { M[,] + M[,] + c c c, M[,] + M[,] + c c c } i \ j Esempio L R L R L R M[,] = min k< { M[,k] + M[k+,] + c c k c } = min { M[,] + M[,] + c c c, M[,] + M[,] + c c c, M[,] + M[,] + c c c } i \ j
Esempio Esempio L i \ Rj L i \ Rj M[,] = min k< { M[,k] + M[k+,] + c c k c } = min { M[,] + M[,] + c c c, M[,] + M[,] + c c c, M[,] + M[,] + c c c, M[,] + M[,] + c c c } M[,] = min k< { M[,k] + M[k+,] + c c k c } = min { M[,] + M[,] + c c c, M[,] + M[,] + c c c, M[,] + M[,] + c c c, M[,] + M[,] + c c c, M[,] + M[,] + c c c } Calcolo bottomup del valore della soluzione Soluzione ricorsiva topdown Passiamo ora al terzo passo della programmazione dinamica: calcolare in modo bottomup il valore della soluzione ottima Ma la definizione ricorsiva di M[i,j] suggerisce di utilizzare un approccio ricorsivo topdown per risolvere il problema: Lanciamo il problema sulla sequenza completa [,n] Il meccanismo ricorsivo individua i sottoproblemi da risolvere Proviamo... male non fa ;) Input: un array c[..n] con le dimensioni delle matrici, c[] è il numero di righe della prima matrice c[i] è il numero di colonne della matrice Ai u integer recpar(integer[]c, integer i, integer j) if i = j then return else min + for integer k i to j do integer q recpar(c, i, k)+recpar(c, k +, j)+c[i if q<min then min q return min Domanda: Complessità risultante? ] c[k] c[j] 7 8
Critica all'approccio topdown Calcolare la soluzione ottima in modo bottomup Alcune riflessioni E' interessante notare che il numero di possibili problemi è molto inferiore a n La soluzione ricorsiva topdown è Ω( n ) Non è poi migliore dell'approccio basato su forza bruta! Qual è il problema? Il problema principale è che molti problemi vengono risolti più volte Ogni sottoproblema uno per ogni scelta di i e j (con i j n): n + n = (n ) Sottoproblemi con i < j È risolvibile utilizzando le soluzioni dei sottoproblemi che sono state eventualmente già calcolate e memorizzate nell'array Idea chiave della programmazione dinamica: Sottoproblemi con i = j Mai calcolare più di una volta la soluzione ad un sottoproblema 9 Calcolare la soluzione ottima in modo bottomup Algoritmo L algoritmo parentesizzazione() prende in ingresso un array c[..n] con le dimensioni delle matrici c[] è il numero di righe della A c[i] è il numero di righe della matrice Ai+ il numero di colonne della matrice Ai utilizza (e ritorna) due matrici n n ausiliarie: M[i,j] che contiene i costi minimi dei sottoproblemi A[i..j] S[i,j] che contiene il valore di k che minimizza il costo per il sottoproblema parentesizzazione(integer[]c, integer[][]m, integer[][]s, integer n) integer i, j, h, k, t for i to n do h varia sulle diagonali sopra quella principale M[i, i] for h to n do i e j assumono i valori delle for i to n h +do celle nella diagonale h j i + h M[i, j] + for k i to j do t M[i, k]+m[k +,j]+c[i ] c[k] c[j] if t<m[i, j] then M[i, j] t L S[i, j] k Calcola tutti i possibili valori e conserva solo il più piccolo
L i \ i Rj 7 8 7 i c i 7 L i \ Rj i c i 7 M[ ] 7 7 8 8 S[ ] 8 9 9 M[,] = min k { M[,k] + M[k+,] + c c k c } = min { M[,] + M[,] + c c c, M[,] + M[,] + c c c, M[,] + M[,] + c c c } = min { + + 7 * 8 *, + + 7 * *, 7 + + 7 * * } = min { 8,, 8 } = 8 M[,] = min k { M[,k] + M[k+,] + c c k c } = min { M[,] + M[,] + c c c, M[,] + M[,] + c c c, M[,] + M[,] + c c c } = min { + + 7 * 8 *, + + 7 * *, 7 + + 7 * * } = min { 8,, 8 } = 8 Calcolare la soluzione ottima in modo bottomup Costruire una soluzione ottima Considerazioni sull'algoritmo Costo computazionale: O(n ) Nota Lo scopo della terza fase era calcolare in modo bottomup il valore della soluzione ottima Questo valore si trova in M[,n] Possiamo definire un algoritmo che costruisce la soluzione a partire dall'informazione calcolata da parentesizzazione(). La matrice S ci permette di determinare il modo migliore di moltiplicare le matrici. S[i,j]=k contiene infatti il valore k su cui dobbiamo spezzare il prodotto A[i..j] Ci dice cioè che per calcolare A[i..j] dobbiamo prima calcolare A[i..k] e A[k+..j] e poi moltiplicarle tra loro. Per alcuni problemi E' anche necessario mostrare la soluzione trovata Ma questo è un processo facilmente realizzabile tramite un algoritmo ricorsivo Per questo motivo registriamo informazioni sulla soluzione mentre procediamo in maniera bottomup
Costruire una soluzione ottima stampapar(integer[][]s, integer i, integer j) if i = j then print A[ ; print i; print ] else print ( stampapar(s, i,s[i, j]) print stampapar(s, S[i, j]+, j) print ) Costruire una soluzione ottima integer[][]multiply(integer[][]s, integer i, integer j) if i = j then return A[i] else integer[][]x multiply(s, i, S[i, j]) integer[][]y multiply(s, S[i, j]+,j) return matrixmultiplication(x, Y ) 7 8 Esempio di esecuzione Numeri di Fibonacci A = A k A k+ = A A A = A k A k+ =A A A = A k A k+ =A.. A A = A k A k+ = A A A = A k A k+ =A A A L i \ Rj S[ ] = ( ( A (A A ) ) ( (A A ) A ) ) Definiti ricorsivamente F() = F() = F(n) = F(n)+F(n) Un po' di storia Leonardo di Pisa, detto Fibonacci Utilizzati per descrivere la crescita di una popolazione di conigli (!) In natura: Pigne, conchiglie, parte centrale dei girasoli, etc. In informatica: Alberi AVL minimi, Heap di Fibonacci, etc. 9
Implementazione ricorsiva Implementazione iterativa integer fibonacci(integer n) if n then return else return fibonacci(n ) + fibonacci(n ) Complessità computazionale integer fibonacci(integer n) integer[]f new integer[...max(n, )] F [] F [] for integer i to n do F [i] F [i ] + F [i ] return F [n] Complessità In tempo: O(n) In spazio: O(n) Array di n elementi T (n) = T (n ) + T (n ) + () n> () n = n 7 f [ ] 8 Soluzione T(n) = O( n ) Implementazione iterativa risparmio memoria Zaino integer fibonacci(integer n) integer F,F,F F F F for integer i to n do F F F F F F + F return F n 7 Complessità In tempo: O(n) In spazio: O() variabili Input Un intero positivo C la capacità dello zaino n oggetti, tali che l oggetto iesimo è caratterizzato da: Problema un profitto pi e un volume vi, entrambi interi positivi trovare un sottoinsieme S di {,..., n} di oggetti tale che il volume totale non superi la capacità massima e il profitto totale sia massimo F 8 v(s) = X is v i apple C F 8 F 8 p(s) = X is p i
Zaino Zaino Caratterizzazione del problema Tabella per programmazione dinamica P(i, c) è il sottoproblema dato dai primi i oggetti da inserire in uno zaino con capacità c Il problema originale corrisponde a P(n, C) Teorema sottostruttura ottima Sia S(i, c) una soluzione ottima per il problema P(i, c) Possono darsi due casi: Se i S(i, c), allora S(i, c){i} è una soluzione ottima per il sottoproblema P(i, cvi ) Se i S(i, c), allora S(i, c) è una soluzione ottima D[i, c] contiene il profitto massimo ottenibile per il problema P(i,c) 8 >< se c< D[i, c] = se i =_ c = >: max{d[i,c],d[i,c v i ]+p[i]} altrimenti Alcune considerazioni Costo di un algoritmo di programmazione dinamica bottomup: O(nC) Non è detto che tutti i problemi debbano essere risolti per il sottoproblema P(i, c) Dimostrazione per assurdo Memoization Zaino annotato Memoization (annotazione) Tecnica che fonde l'approccio di memorizzazione della programmazione dinamica con l'approccio topdown di divideetimpera Quando un sottoproblema viene risolto per la prima volta, viene memorizzato in una tabella ogni volta che si deve risolvere un sottoproblema, si controlla nella tabella se è già stato risolto precedentemente SI: si usa il risultato della tabella NO: si calcola il risultato e lo si memorizza In tal modo, ogni sottoproblema viene calcolato una sola volta e memorizzato come nella versione bottomup Note sulla soluzione è un valore speciale per indicare che un certo problema non è stato risolto Gli elementi della tabella D sono inizializzati con il valore integer zaino(integer[]p, integer[]v, integer i, integer c, integer[][]d) if c<then return if i =or c =then return if D[i, c] =? then D[i, c] max(zaino(p, v,i, c,d), zaino(p, v,i, c v[i], D)+p[i]) return D[i, c] 7 8
Discussione su memoization Memoization in Python Caso pessimo Nel caso pessimo, è comunque O(nC) Quando si verifica? Inizializzazione E necessario inizializzare D costo O(nC) Se il costo dell inizializzazione è asintoticamente inferiore al costo di ricombinare i risultati, si ottiene un guadagno Altrimenti: è possibile utilizzare una tabella hash Complessità pseudopolinomiale La complessità O(nC) è polinomiale nella dimensione dell input? from functools import wraps def memo(func): cache = {} @wraps(func) def wrap(*args): if args not in cache: cache[args] = func(*args) return cache[args] return wrap @memo def fib(n): if n<: return else: return fib(n) + fib(n) 9 Bioinformatica Caratterizzazione del problema DNA Definizione Una stringa di molecole chiamate basi Solo quattro basi: Adenina, Citosina, Guanina, Timina Esempi Due esempi di DNA: AAAATTGA, TAACGATAG Date due sequenze, è lecito chiedersi quanto siano simili Una sequenza T è una sottosequenza di P se T è ottenuta da P rimuovendo uno o più elementi Alternativamente: T è definito come il sottoinsieme degli indici l'insieme di elementi di P che compaiono anche in T Gli elementi rimanenti devono comparire nello stesso ordine, anche se non devono essere necessariamente contigui in P Una è sottostringa dell'altra? Esempio Distanza di editing: costo necessario per trasformare una nell'altra P = AAAATTGA, T = AAATA La più lunga sottosequenza (anche non contigua) comune ad entrambe Nota La sequenza nulla è una sottosequenza di ogni sequenza
Caratterizzazione del problema Caratterizzazione del problema Definizione: Problema LCS Date due sequenze P e T, una sequenza Z è una sottosequenza comune di P e T se Z è sottosequenza sia di P che di T Input: due sequenze di simboli, P e T Output: Trovare la più lunga sottosequenza Z comune a P e T Scriviamo Z CS(P, T) Common Subsequence, o CS Esempio P = AAAATTGA Definizione: T = TAACGATA Date due sequenze P e T, una sequenza è una sottosequenza comune massimale di P e T, se Z CS(P, T) e non esiste una sequenza W CS(P, T) tale che W > Z LCS(P,T) =???? Scriviamo Z LCS(P, T) Longest Common Subsequence, o LCS Prima di provare con la programmazione dinamica, proviamo di forza bruta... Risoluzione tramite enumerazione Caratterizzazione della soluzione ottima integer LCSesaustivo(ITEM[]P,ITEM[]T) integer max ITEM[] nil foreach sottosequenza Z di P do if Z è una sottosequenza di T then if Z > max then max Z Z return Data una sequenza P=(p,, pn): denoteremo con P(i) l iesimo prefisso di P, cioè la sottosequenza ( p,, pi ) Esempio: P = ABDCCAABD P() denota la sottosequenza nulla P() = ABD P() = ABDCCA u Domanda: Quante sono le sottosequenze di P?
Caratterizzazione della soluzione ottima Dimostrazione Teorema (Sottostruttura ottima) Date le due sequenze P=(p,, pm) e T=(t,, tn), sia Z=(z,,zk ) una LCS di P e T. pm= tn zk = pm = tn e Z(k) LCS( P(m), T(n) ). pm tn e zk pm Z LCS( P(m), T ). pm tn e zk tn Z LCS( P, T(n) ) Dimostrazione Punto Supponiamo per assurdo che zk pm = tn Si consideri W=Zpm. Allora W CS(P,T) e W > Z, assurdo Quindi zk = pm = tn Supponiamo per assurdo che Z(k) LCS( P(m), T(n) ) Allora esiste W LCS( P(m), T(n) ) tale che W > Z(k) Quindi Wpm CS(P,T) e Wpm > Z, assurdo P[,,m] T[,,n] a a P[,,m] T[,,n] Z[,,k] a Z[,,k] 7 8 Dimostrazione Cosa ci dice il teorema? Punto (Punto simmetrico) Se pm = tn, dobbiamo risolvere un sottoproblema Se zk pm, allora Z CS(P(m), T) LCS( P(m), T(n) ) Per assurdo ipotizziamo che Z LCS(P(m), T) allora esiste W LCS(P(m), T) tale che: W > Z La definizione ricorsiva è la seguente: LCS(P,T) = LCS( P(m), T(n) ) pm Allora è anche vero che W LCS(P, T), contraddicendo l'ipotesi Se pm tn, dobbiamo risolvere due sottoproblemi LCS( P(m), T ) LCS( P, T(n) ) P[,,m] T[,,n] Z[,,k]? b a P[,,m] T[,,n] Z[,,k]? b A questo punto, dobbiamo scegliere la LCS più lunga fra le La definizione ricorsiva è la seguente: LCS(P,T) = longest( LCS( P(m), T ), LCS( P, T(n) ) ) 9
LCS basato su programmazione dinamica LCS basato su programmazione dinamica Definiamo una tabella per memorizzare la lunghezza della LCS associati ai sottoproblemi: Definiamo una tabella per memorizzare informazioni necessarie ad ottenere la stringa finale D[... m,... n] tabella di (m+) (n+) elementi, dove P = m, T = n B[... m,... n] tabella di (m+) (n+) elementi, dove P = m, T = n D[i,j] lunghezza della LCS di P(i) e T(j) Goal finale: Calcolare D[m,n] lunghezza della LCS di P e T B[i,j] puntatore alla entry della tabella stessa che identifica il sottoproblema ottimo scelto durante il calcolo del valore D[i,j] Valori possibili: Formulazione ricorsiva se i = j = D[i, j] = D[i,j ] + se i> j> p i = t j max{d[i,j],d[i, j ]} se i> j> p i = t j deriva da i,j deriva da i,j deriva da i,j j j TACCBT ATBCBD deriva da i,j deriva da i,j deriva da i,j i A T B C B D T A C ACGGCT CTCTGT deriva da i,j deriva da i,j deriva da i,j i C T C T G T A C G C G B C T T se i = j = D[i, j] = D[i,j ] + se i> j> p i = t j max{d[i,j],d[i, j ]} se i> j> p i = t j se i = j = D[i, j] = D[i,j ] + se i> j> p i = t j max{d[i,j],d[i, j ]} se i> j> p i = t j
Calcolo del valore della soluzione ottima integer lcs(integer[][]d, ITEM[]P, ITEM[]T, integer i, integer j) if i =or j =then return if D[i, j] =? then if P [i] =T [j] then D[i, j] lcs(d, P, T, i,j ) + else D[i, j] max(lcs(d,p, T, i, j), lcs(d,p, T, i, j )) return D[i, j] ABDDBE BEBEDE deriva da i,j deriva da i,j deriva da i,j j i B E B E D E A B D D B E se i = j = D[i, j] = D[i,j ] + se i> j> p i = t j max{d[i,j],d[i, j ]} se i> j> p i = t j TACCBT ATBCBD deriva da i,j deriva da i,j deriva da i,j j i A T B C B D T A C C B T Alcune ottimizzazioni La matrice B può essere eliminata: Il valore di D[i,j] dipende solo dai valori D[i,j], D[i,j] e D[i,j]. In tempo costante si può quindi determinare quale di questi tre è stato usato, e perciò quale sia il tipo di freccia Se ci serve solo calcolare la lunghezza della LCS, possiamo ridurre la tabella D[i,j] a due sole righe di lunghezza min{ n, m } Ad ogni istante (cioè per ogni coppia i,j), ci servono i valori D[i,j], D[i,j] e D[i,j] Esercizio se i = j = D[i, j] = D[i,j ] + se i> j> p i = t j max{d[i,j],d[i, j ]} se i> j> p i = t j 7 8
Stampa della soluzione ottima LCS e diff integer printlcs(item[]t,item[]p, integer[][]d, integer i, integer j) if i = and j = then if T [i] =P [j] then printlcs(t,p,d,i,j ) print P [i] else if D[i,j] >D[i, j ] then printlcs(t,p,d,i,j) else printlcs(t,p,d,i,j ) Costo computazionale A ogni passo, almeno uno fra i e j viene decrementato: θ(m+n) diff Esamina due file di testo, e ne evidenzia le differenze a livello di riga. Lavorare a livello di riga significa che i confronti fra simboli sono in realtà confronti fra righe, e che n ed m sono il numero di righe dei due file Ottimizzazioni diff è utilizzato soprattutto per codice sorgente; è possibile applicare euristiche sulle righe iniziali e finali per distinguere le righe utilizzo di funzioni hash 9 Questo è il testo originale alcune linee non dovrebbero cambiare mai altre invece vengono rimosse altre vengono aggiunte Figura.: Questo è il testo nuovo alcune linee non dovrebbero cambiare mai altre invece vengono cancellate altre vengono aggiunte come questa Questo è il testo originale + Questo è il testo nuovo alcune linee non dovrebbero cambiare mai altre invece vengono rimosse + cancellate altre vengono aggiunte + come questa Il file original.txt (a sinistra); il file new.txt (al centro); l output di diff original.txt new.txt (a destra). 7 String matching approssimato Esempio Input Input una stringa P = p pm (pattern) questoèunoscempio una stringa T = t tn (testo), con m n unesempio Definizione Domanda Un occorrenza kapprossimata di P in T, con k m, è una copia della stringa P nella stringa T in cui sono ammessi k errori (o differenze) tra caratteri di P e caratteri di T, del seguente tipo: () i corrispondenti caratteri in P e in T sono diversi (sostituzione) () un carattere in P non è incluso in T (inserimento) () un carattere in T non è incluso in P (cancellazione) Qual è il minimo valore k per cui si trova una occorrenza kapprossimata? A partire da dove? Con quali errori? Problema: Trovare un occorrenza kapprossimata di P in T per cui k sia minimo. 7 7
Sottostruttura ottima Sottostruttura ottima Definizione Definizione Tabella D[...m,...n], i cui elementi D[i,j] contengono il minimo valore k per cui esiste una occorrenza kapprossimata di P(i) in T(j) Tabella D[...m,...n], i cui elementi D[i,j] contengono il minimo valore k per cui esiste una occorrenza kapprossimata di P(i) in T(j) D[i,j] può essere uguale a D[i,j], se pi = tj avanza su entrambi i caratteri (uguali) D[i,j]+, se pi tj avanza su entrambi i caratteri (sostituzione) D[i,j]+ avanza sul pattern (inserimento) D[i,j]+ avanza sul testo (cancellazione) p i t D[i, j] D[i, j] t j j +/+ + Ricordate che cerchiamo il minimo: se i = D[i, j] = i se j = min{d[i, j ] +,D[i, j] +, D[i, j ] + } altrimenti =, se p i = t j, oppure =, se p i = t j. ormale, ma è chiaro che tale definizione illustra p i D[i, j] D[i, j] + 7 7 Ricostruzione della soluzione finale Algoritmo String matching approssimato Si noti che: D[m,j] = k se e solo se c è un occorrenza kapprossimata di P in T che termina in tj la soluzione del problema è data dal valore di D[m,j] più piccolo, per j n A B A B A P T integer stringmatching(item[]p, ITEM[]T, integer m, integer n) integer[][]d new integer[...m][...n] for integer i to m do D[i, ] i for integer j to n do D[,j] for i to m do for j to n do integer t D[i,j ] + iif(p [i] =T [j],, ) t min(t, D[i,j] + ) t min(t, D[i, j ] + ) D[i, j] t Domanda: complessità? B A B integer min D[m, ] integer pos for j to n do if D[m, j] < min then min D[m, j] pos j return pos 7 7
String matching approssimato Insieme indipendente di intervalli pesati Variante dello string matching approssimato: Distanza di editing (Distanza di Levenshtein) Date due stringhe, vogliamo conoscere il numero minimo di operazioni (sostituzione, inserimento, cancellazione) necessarie per trasformare una nell altra (o viceversa, visto che inserimento e cancellazione sono simmetriche) Esempio: distanza fra google e yahoo? Algoritmo? Come (e se) dobbiamo modificare le condizioni iniziali? Come (e se) dobbiamo modificare la definizione ricorsiva? Input Siano dati n intervalli distinti [a, b[,..., [an,bn[ della retta reale, aperti a destra, dove all intervallo i è associato un peso wi, i n. Definizione Due intervalli i e j si dicono disgiunti se: bj ai oppure bi aj Output: ai bi aj Trovare un insieme indipendente di peso massimo, ovvero un sottoinsieme di intervalli tutti disgiunti tra di loro tale che la somma dei pesi degli intervalli nel sottoinsieme sia la più grande possibile bj Esempio: Prenotazione di una sala conferenza in un hotel 77 78 Preelaborazione Definizione ricorsiva Per poter applicare la programmazione dinamica, è necessario effettuare una preelaborazione Ordiniamo gli intervalli per estremi finali non decrescenti b b bn Per ogni intervallo i, sia pi = j il predecessore di i, dove j < i è il massimo indice tale che [aj, bj [ è disgiunto da [ai, bi [ (se non esiste j, allora pi = ) Definizione ricorsiva del peso di una soluzione ottima D[n] è il problema originario D[i] = max{d[i ],w i + D[p i ]}, i =,...,n. Costo della procedura risultante O(n log n) per l ordinamento Teorema sottostruttura ottima Sia P[i] il sottoproblema dato dai primi i intervalli e sia S[i] una sua soluzione ottima di peso D[i] Se l intervallo iesimo non fa parte di tale soluzione, allora deve valere D[i] = D[i ], dove si assume D[] = ; O(n log n) per il calcolo degli indici pi O(n) per il riempimento della tabella O(n) per la ricostruzione della soluzione Esercizio: altrimenti, deve essere D[i] = wi + D[pi] Scrivere algoritmo per il calcolo degli indici pi 79 8
Codice SET maxinterval(integer[]a, integer[]b, integer[]w, integer n) { ordina gli intervalli per estremi di fine crescenti } { calcola p j } integer[]d new integer[...n] D[] for i to n do D[i] max(d[i ],w[i]+d[p i ]) i n SET S Set() while i>do if D[i ] >w[i]+d[p i ] then i i else S.insert(i) i p i return S 8