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= { O1 n=0 2T n/ 2O 1 n0 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 /2O1 n0 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.