Algoritmi e Strutture Dati (Complementi) Esercizi di Programmazione Dinamica Proff. Paola Bonizzoni / Giancarlo Mauri Anno Accademico 2002/2003 Appunti scritti da Alberto Leporati e Rosalba Zizza Esercizio 1 Si determini un algoritmo per trovare in tempo O(n2 ) la più lunga sottosequenza monotona (crescente) di una sequenza di n numeri interi. Soluzione. Sia X = x1 , . . . , xn la sequenza di numeri interi; per ogni i ∈ {1, 2, . . . , n}, indichiamo con Xi la sottosequenza x1 , . . . , xi . Sempre per i ∈ {1, 2, . . . , n}, sia c[i] la lunghezza della più lunga sequenza crescente di Xi che termina con xi (cioè che contiene effettivamente xi ). Il motivo per cui imponiamo che l’elemento xi appartenga effettivamente alla sottosoluzione ottima relativa alla sottosequenza Xi è che quando andiamo a considerare un elemento xj con j > i, per sapere se tale elemento può far parte della sottosoluzione ottima relativa alla sottosequenza Xj dobbiamo verificare che la condizione xi < xj sia verificata; questo chiaramente lo possiamo fare solo se sappiamo qual è l’ultimo elemento della sottosequenza crescente più lunga contenuta in Xi . Dovendo quindi memorizzare da qualche parte questa informazione, la cosa migliore è quella di incorporarla (come abbiamo fatto sopra) nel valore di c[i]. D’altra parte, osserviamo che se l’elemento xi non appartenesse alla più lunga sottosequenza crescente contenuta in Xi vorrebbe dire che esiste un elemento xk , con k < i, che termina tale sequenza. Ma allora tale sequenza sarebbe anche la più lunga sottosequenza crescente contenuta in Xk ; quindi, tanto vale definire il valore di c[i] come abbiamo fatto sopra. Ricaviamo ora un’equazione di ricorrenza che ci consenta di calcolare il valore di c[i] per ogni i ∈ {1, 2, . . . , n}. È facile vedere che vale c[i] = 1. Infatti, le sottosequenze possibili di X1 (che è formata solamente da x1 ) sono due: quella che contiene x1 e quella che non lo contiene. Entrambe sono crescenti, e hanno rispettivamente lunghezza 1 e 0. Quindi la più lunga sottosequenza crescente di X1 ha lunghezza 1, e pertanto poniamo c[1] = 1. 1 Sia ora i > 1, e supponiamo di aver già calcolato i valori di c[1], c[2], . . ., c[i − 1]. Poiché le sottosequenze di X1 , X2 , . . . , Xi−1 possono essere tutte considerate sottosequenze di Xi−1 , abbiamo che i valori c[1], c[2], . . . , c[i − 1] rappresentano le lunghezze delle più lunghe sottosequenze crescenti di Xi−1 che terminano, rispettivamente, con x1 , x2 , . . . , xi−1 . Tra queste ci saranno alcune sottosequenze alle quali possiamo attaccare xi e altre alle quali l’elemento xi non può essere attaccato. Se prendiamo la più lunga sottosequenza alla quale possiamo attaccare xi , e aggiungiamo xi , otteniamo la più lunga sottosequenza crescente di Xi che termina con xi . La lunghezza di tale sottosequenza sarà uguale a 1 più la lunghezza della sottosequenza alla quale abbiamo attaccato xi . Pertanto il valore di c[i] è dato da: ( 1 + max{c[j] | 1 ≤ j < i, xj < xi } se i > 1 c[i] = 1 se i = 1 Poiché può accadere che l’insieme {c[j] | 1 ≤ j < i, xj < xi } sia vuoto (il che corrisponde al fatto che l’elemento xi è minore di tutti gli elementi precedenti, e quindi non può essere attaccato a nessuna delle sottosequenze di Xi−1 ), assumiamo per definizione che sia max ∅ = 0, cosı̀ che il corrispondente valore di c[i] sia uguale a 1. Una volta calcolati i valori c[1], c[2], . . . , c[n], la soluzione al problema proposto è data da: max c[i] 1≤i≤n che è facilmente ricavabile da un semplice ciclo for che scandisce il vettore c[1..n] alla ricerca del massimo elemento. L’algoritmo, espresso in pseudo–codice, che calcola i valori c[1], c[2], . . . , c[n] è facilmente ricavabile dall’equazione di ricorrenza, ed è il seguente: Max-Growing-Sequence c[1] ← 1 for i ← 2 to n do max ← 0 for j ← 1 to i − 1 do if (xj < xi ) and (c[j] > max) then max ← c[j] endif endfor 2 c[i] ← 1 + max endfor return c È facile verificare che la complessità in tempo dell’algoritmo proposto è O(n2 ), mentre lo spazio richiesto è quello necessario per memorizzare il vettore c[1..n], e quindi Θ(n). Esercizio 2 Siano date n scatole B1 , . . . , Bn . Ogni scatola Bi è descritta da una tripla (ai , bi , ci ), le cui componenti denotano rispettivamente lunghezza, larghezza e altezza della scatola. La scatola Bi può essere inserita nella scatola Bj se e solo se ai < aj , bi < bj e ci < cj ; in particolare, le scatole non possono essere ruotate. Per brevità indichiamo con Bi ⊂ Bj il fatto che la scatola Bi può essere inserita nella scatola Bj . Descrivere un algoritmo efficiente per determinare il massimo valore di k tale che esiste una sequenza Bi1 , . . . , Bik che soddisfa le condizioni: B i1 ⊂ B i2 ⊂ · · · ⊂ B ik e i1 < i 2 < · · · < i k Analizzare la complessità dell’algoritmo proposto. Soluzione. Nonostante questo esercizio sembri più complicato di quello precedente, il problema proposto è isomorfo a quello trattato nell’esercizio precedente. Si tratta infatti di trovare la più lunga sottosequenza crescente di scatole contenuta nella sequenza B1 , . . . , Bn . La tecnica di soluzione di questo problema è identica a quella adottata nell’esercizio precedente, dove al posto di verificare se due interi xj e xi sono tali che xj < xi andiamo a verificare se due scatole Bj e Bi sono tali che Bj ⊂ Bi , ovvero se aj < ai , bj < b i e c j < c i . Sia allora z[1..n] un vettore di n componenti (non lo chiamiamo c, come abbiamo fatto nell’esercizio precedente, per non confoderci con le altezze delle scatole). Detta z[i] la lunghezza massima di una sottosequenza crescente ad 3 elementi in B1 , . . . , Bi che contiene Bi , calcoliamo i valori z[1], z[2], . . . , z[n] in questo ordine. La soluzione del problema proposto sarà data dal valore di: max z[i] 1≤i≤n L’equazione di ricorrenza che consente di ricavare il valore di z[i] è praticamente identica a quella dell’esercizio precedente: ( 1 + max{z[j] | 1 ≤ j < i, aj < ai , bj < bi , cj < ci } se i > 1 z[i] = 1 se i = 1 dove, come abbiamo osservato nell’esercizio precedente, assumiamo che valga max ∅ = 0. L’algoritmo che calcola i valori di z[1], z[2], . . . , z[n] è il seguente: Max-Boxes-Chain z[1] ← 1 for i ← 2 to n do max ← 0 for j ← 1 to i − 1 do if (aj < ai ) and (bj < bi ) and (cj < ci ) and (z[j] > max) then max ← z[j] endif endfor z[i] ← 1 + max endfor return z È facile verificare che la complessità in tempo dell’algoritmo proposto è O(n2 ), mentre lo spazio richiesto è quello necessario per memorizzare il vettore z[1..n], e quindi Θ(n). Esercizio 3 Scrivere un algoritmo efficiente che, date due sequenze di interi positivi X = x1 , . . . , xn e Y = y1 , . . . , ym , determini la lunghezza della più lunga 4 sottosequenza crescente comune a X e a Y , ovvero: max{k | ∃ i1 , i2 , . . . , ik ∈ {1, 2, . . . , n}, j1 , j2 , . . . , jk ∈ {1, 2, . . . , m} : i1 < i 2 < · · · < i k , j 1 < j 2 < · · · < j k , xi 1 = y j 1 < x i 2 = y j 2 < . . . < x i k = y j k } Ad esempio, se X = 1, 4, 12, 3, 7, 16, 8 e Y = 12, 1, 3, 17, 8 allora la lunghezza della più lunga sottosequenza crescente comune a X e a Y è 3 (corrispondente a 1, 3, 8), mentre 12, 3, 8 è una LCS ma non è crescente. Soluzione. La soluzione di questo esercizio è molto simile a quelle date per gli esercizi precedenti. Come al solito, indichiamo con Xi la sottosequenza x1 , x2 , . . . , xi di X, e con Yj la sottosequenza y1 , y2 , . . . , yj di Y . I sottoproblemi naturali del problema proposto consistono nel determinare la lunghezza di una LCS crescente tra Xi e Yj , per i ∈ {1, . . . , n} e j ∈ {1, . . . , m}. Come abbiamo fatto negli esercizi precedenti, per essere sicuri che la soluzione al problema sia valida (cioè che la LCS di cui calcoliamo la lunghezza sia crescente) occorre memorizzare da qualche parte il valore dell’ultimo elemento delle sottosequenze ottime corrispondenti ai sottoproblemi. Conviene pertanto definire una matrice c[1..n, 1..m], dove il valore di c[i, j] è la lunghezza di una LCS crescente tra Xi e Yj tale che l’ultimo elemento della LCS è uguale sia a xi che a yj (e quindi, in particolare, xi = yj ). Ricaviamo l’equazione di ricorrenza che consente di determinare il valore di c[i, j]. Se xi 6= yj , non esiste una LCS crescente tra Xi e Yj che termina sia con xi che con yj ; pertanto, in questo caso abbiamo c[i, j] = 0. Se invece xi = yj , occorre cercare la più lunga tra le LCS crescenti di Xs e Yt , per tutti i valori di s e t tali che 1 ≤ s < i e 1 ≤ t < j, che terminano con un valore minore di xi . Detti s e t i valori di s e t cosı̀ individuati, il valore di c[i, j] sarà uguale a 1 + c[s, t]. Quindi, l’equazione di ricorrenza che consente di ricavare c[i, j] è la seguente: ( 0 se xi 6= yj c[i, j] = 1 + max{c[s, t] | 1 ≤ s < i, 1 ≤ t < j, xs ≤ xi } se xi = yj dove, come abbiamo fatto negli esercizi precedenti, assumiamo che valga max ∅ = 0. La soluzione del problema proposto sarà data dal valore di: max c[i, j] 1≤i≤n 1≤j≤m 5 L’algoritmo che calcola i valori di c[i, j] è il seguente. Si osservi che i valori vengono calcolati dalla prima verso l’ultima riga; all’interno di ogni riga, i valori vengono calcolati da sinistra verso destra. Growing-LCS for i ← 1 to n do for j ← 1 to m do if xi 6= yj then c[i, j] ← 0 else max ← 0 for s ← 1 to i − 1 do for t ← 1 to j − 1 do if xs < xi and c[s, t] > max then max ← c[s, t] endif endfor endfor c[i, j] ← 1 + max endif endfor endfor return c È facile verificare che la complessità in tempo dell’algoritmo proposto è O((nm)2 ), mentre lo spazio richiesto è quello necessario per memorizzare il vettore c[1..n, 1..m], e quindi Θ(nm). 6