Corso di Algoritmi e Strutture Dati—Informatica

Corso di Algoritmi e Strutture Dati—Informatica per il Management
Moreno Marzolla, [email protected]
Esercizi, 7/3/2011
Esercizio 1.
Sia V[1..n] un vettore composto da n numeri reali arbitrari (possono essere presenti valori positivi e negativi).
Per ogni i, j con 1≤i≤j≤n, sia S(i, j) la somma dei valori degli elementi del sottovettore V[i..j]; si noti che S(i,
i) = V[i] per ogni i. Scrivere un algoritmo in grado di calcolare i valori S(i, j), per ogni i, j, in tempo Θ(n2).
Modificare l'algoritmo per risolvere il problema di determinare il sottovettore di somma massima in tempo
Θ(n2); ricordiamo che tale problema può essere risolto in tempo Θ(n) usando la programmazione dinamica.
(Suggerimento: usare una matrice per memorizzare i valori S(i, j); è possibile riempire la matrice in
maniera opportuna per ottenere il costo asintotico richiesto).
Soluzione
Sfruttiamo un approccio “ispirato” alla programmazione dinamica per calcolare i costi nel modo seguente.
Memorizziamo i valori S(i, j) in una matrice quadrata n×n. Si noti come solo la parte triangolare superiore
della matrice verrà utilizzata, dato che i valori che ci interessano sono tutti e soli quelli per cui i≤j.
Riempiamo la matrice una riga alla volta. Per ogni riga, il primo valore S(i, i) è facilmente calcolato, dato che
S(i, i) = V[i] per definizione. Notiamo che una volta calcolato S(i, j), possiamo calcolare l'elemento
successivo S(i, j+1) = S(i, j) + V[j+1]. Quindi ogni elemento della matrice può essere calcolato in tempo
O(1); poiché ci sono Θ(n2) elementi, il costo computazionale dell'algoritmo sarà Θ(n2).
algoritmo calcolaS( array V[1..n] ) → matrice [1..n, 1..n]
S := nuova matrice n´n
for i:=1 to n do
S[i, i] := V[i];
for j:=i+1 to n do
S[i, j] := S[i,j-1] + V[j]
endfor
endfor
return S
Per risolvere il problema del calcolo del sottovettore di somma massima, possiamo estendere l'algoritmo
precedente come segue:
algoritmo sottovettoreSMax( array V[1..n] )
imax := 1; // Indice primo elemento del vettore di somma massima
jmax := 1; // Indice ultimo elemento del vettore di somma massima
Smax := V[1]; // Somma degli elementi del vettore di somma massima
S := nuova matrice n´n
for i:=1 to n do
S[i, i] := V[i];
for j:=i+1 to n do
S[i, j] := S[i,j-1] + V[j]
endfor
// Aggiorniamo il massimo per ogni riga
for j:=i to n do
if ( S[i, j] > Smax) then
Smax := S[i, j];
imax := i;
jmax := j;
endif
endfor
endfor
Stampa Smax, imax, jmax
Esercizio 2.
Si consideri un array A[1..n] contenente tutti gli interi appartenenti all'intervallo [1..n+1], tranne uno. L'array
A è ordinato in senso crescente, e non contiene valori duplicati. Scrivere un algoritmo efficiente che, dato
l'array A[1..n], determina il valore mancante. Ad esemipo, se A=[1, 2, 3, 4, 6, 7], l'algoritmo deve restituire il
valore 5. Determinare il costo computazionale dell'algoritmo proposto. (Suggerimento: per una soluzione
efficiente si può usare una tecnica divide-et-impera simile alla ricerca binaria. Prestare attenzione al fatto
che l'algoritmo deve fornire la risposta corretta anche nel caso in cui il valore mancante sia n+1).
Soluzione
Una soluzione non efficiente consiste nell'esaminare il vettore A dall'inizio; non appena si incontra il primo
valore i tale che A[i] ≠ i, si può immediatamente concludere che il valore mancante è i. Lo pseudocodice è il
seguente:
algoritmo trova_mancante_1(array A[1..n]) → int
for i:=1 to n do
if ( A[i] != i ) then
return i;
endif
endfor
// Se siamo arrivati qui, significa che il valore mancante è n+1
return n+1;
Il costo computazionale dell'algoritmo trova_mancante_1 è O(n) (non Θ(n), in quanto si osserva che se ad
esempio il numero mancante è il numero 1, l'algoritmo termina in tempo O(1)).
Sfruttando il fatto che l'array è ordinato, possiamo seguire il suggerimento di utilizzare una variante della
ricerca binaria per determinare in modo efficiente il valore che manca. L'idea è la seguente: si individua la
posizione centrale m tra gli estremi i e j. Se A[m] contiene il valore m, allora sicuramente nella prima parte
del vettore non ci sono elementi mancanti, e la ricerca continua in A[m+1..j]. Invece, se A[m] ≠ m, significa
che il valore mancante si trova nel sottovettore A[i..m] (attenzione, non A[i..m-1]). Si presti attenzione alla
gestione corretta del caso particolare in cui il valore mancante sia n+1.
algoritmo trova_mancante_2(array A[1..n], int i, int j) → int
if ( i == j ) then
if ( A[i] == i ) then
return i+1;
else
return i;
endif
else
m := (i+j)/2; // arrotondato
if ( A[m] == m ) then
return trova_mancante_2(A,m+1,j);
else
return trova_mancante_2(A,i,m);
endif
endif
L'algoritmo di cui sopra viene invocato con trova_mancante_2(A,1,n); il costo computazionale è
O(log n), come per la ricerca binaria.
Esercizio 3.
Si consideri un array A[1..n] contenente valori reali tutti distinti e ordinati in senso crescente. Scrivere un
algoritmo ricorsivo efficiente di tipo divide-et-impera che, dato l'array A e due numeri reali l<u, calcola
quanti valori di A appartengono all'intervallo [l, u]. Determinare il costo computazionale dell'algoritmo
proposto nel caso in cui tutti i valori di A appartengano all'intervallo dato, e nel caso in cui nessun valore in
A appartenga all'intervallo.
Soluzione
L'algoritmo seguente conta il numero di elementi del sottovettore A[i..j] il cui valore appartiene all'intervallo
[l, u].
algoritmo conta(array A[1..n], int i, int j, double l, double u) → int
if ( j < i ) then
return 0;
else
m := (i+j)/2; // posizione elemento centrale
c1 := conta(A, i, m-1, l, u);
c2 := conta(A, m+1, j, l, u);
if ( A[m] >= l && A[m] <= u ) then
return 1+c1+c2;
else
return c1+c2;
endif
endif
Il costo T(n) nel caso pessimo soddisfa la seguente equazione di ricorrenza:
T  n=
{
O1
n=0
2T n/ 2O 1 n0
L'applicazione del Master Theorem porta alla soluzione T(n) = Θ(n). Si noti che l'algoritmo ha lo stesso costo
computazionale anche nel caso migliore: infatti la ricorsione prosegue sempre fino al sottovettore di
lunghezza zero.
Con un minimo di astuzia è possibile diminuire il costo computazionale nel caso ottimo. Si consideri
l'algoritmo seguente:
algoritmo conta2(array A[1..n], int i, int j, double l, double u) → int
if ( j<i || l>u ) then
return 0;
else
m := (i+j)/2; // posizione elemento centrale
if ( A[m] < l ) then
return conta2(A, m+1, j, l, u); // (*)
else if ( A[m] > u ) then
return conta2(A, i, m-1, l- u); // (*)
else
return 1+conta2(A, i, m-1, l, A[m])+
conta2(A, m+1, j, A[m], u);
endif
endif
L'algoritmo conta2() può effettuare la ricorsione solo in uno dei due sottovettori, nel caso in cui l'altro
sicuramente non contenga elementi di interesse. Quindi il caso ottimo si verifica quando la ricorsione
avviene in uno dei due sottovettori (righe di codice indicate con l'asterisco). L'equazione di ricorrenza del
costo T(n) risulta:
T  n=
{
O 1
n=0
T n /2O1 n0
che in base al Master Theorem ha soluzione T(n) = Θ(log n). Si noti che è possibile fare molto meglio di
così, in particolare è possibile risolvere il problema in tempo O(log n) nel caso peggiore, utilizzando un
algoritmo differente. In particolare, è sufficiente modificare l'algoritmo di ricerca binaria per restituire
l'indice del primo elemento con valore precedente a l e successivo a u nel vettore dato. Questo può essere
realizzato con due ricerca di costo O(log n) nel caso peggiore; il numero di elementi il cui valore è compreso
tra u e l può quindi essere calcolato in tempo O(1) a partire dagli indici calcolati.