Algoritmi e Strutture Dati (Complementi)

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