Esercizi di Algoritmi e Strutture Dati Moreno Marzolla [email protected] Ultimo aggiornamento: 10 novembre 2010 1 La “bandiera nazionale” (problema 4.7 del libro di testo). Il problema della bandiera nazionale italiana è cosı́ definito. Sia A[1..n] un array i cui elementi possono assumere solo uno di tre possibili valori: bianco, verde e rosso. Vogliamo ordinare l’array in modo che tutti gli elementi verdi precedano quelli bianchi, e gli elementi bianchi precedano quelli rossi. L’algoritmo deve richiedere tempo O(n) nel caso peggiore, può solo scambiare elementi e non deve usare altri array temporanei di appoggio, quindi al più deve richiedere memoria aggiuntiva O(1). Non può nemmeno utilizzare contatori per tenere traccia del numero di elementi di un certo colore presenti (quindi non si può usare una variante dell’algoritmo counting sort, che vedremo a lezione). L’algoritmo, infine, deve ordinare gli elementi facendo una singola scansione dell’array. Assicurarsi che l’algoritmo funzioni anche se mancano elementi di qualcuno dei colori. Suggerimento: ripensare a come funziona la procedura partition dell’algoritmo Quick Sort, e provare ad adattarla per risolvere questo problema. /* * * * * * * * * * * * * * * * * * * * B a n d i e r a . java - questo p r o g r a m m a risolve il p r o b l e m a 4.7 p . 116 di Demetrescu , Finocchi , Italiano , " A l g o r i t m i e s t r u t t u r e dati " ( seconda e d i z i o n e ) , McGraw - Hill , 2008. Il p r o b l e m a della b a n d i e r a n a z i o n a l e e ’ d e f i n i t o nel modo s e g u e n t e : Sia A un array di d i m e n s i o n e n , i cui e l e m e n t i possono a s s u m e r e solo uno di tre p o s s i b i l i colori : bianco , verde e rosso . V o g l i a m o o r d i n a r e l ’ array in modo che tutti gli e l e m e n t i verdi p r e c e d a n o quelli bianchi , e gli e l e m e n t i bianchi p r e c e d a n o quelli rossi . L ’ a l g o r i t m o deve r i c h i e d e r e tempo O ( n ) nel caso peggiore , puo ’ solo s c a m b i a r e el e m e n t i e non deve usare altri array t e m p o r a n e i di a p p o g g i o . Non puo ’ nemmeno u t i l i z z a r e c o n t a t o r i per tenere traccia del numero di e l e m e n t i di un certo colore p r e s e n t i ( quindi non si puo ’ usare una v a r i a n t e dell ’ a l g o r i t m o c o u n t i n g sort , che vedremo a lezione ). L ’ algoritmo , infine , deve o r d i n a r e gli e l e m e n t i facendo una singola s c a n s i o n e dell ’ array . A s s i c u r a r s i che l ’ a l g o r i t m o f u n z io n i anche se mancano e l e m e n t i di q u a l c u n o dei colori . 1 * Version 0.3 del 2 0 0 9 / 1 1 / 2 5 * Autore : Moreno M a r z o l l a ( m a r z o l l a ( at ) cs . unibo . it ) * * This file has been r e l e a s e d by the author in the Public Domain */ public class Bandiera { public static final int VERDE = 1; public static final int BIANCO = 2; public static final int ROSSO = 3; /* * * Scambia tra di loro gli e l e m e n t i A [ i ] e A [ j ]. A t t e n z i o n e : * nessun c o n t r o l l o viene e f f e t t u a t o circa la v a l i d i t a ’ degli * indici i e j . */ protected static void swap ( int [] A , int i , int j ) { System . out . println ( " Scambia " + i + " con " + j ); int tmp = A [ i ]; A [ i ] = A [ j ]; A [ j ] = tmp ; } /* * * Stampa a video il c o n t e n u t o dell ’ array A */ public static void stampa ( int [] A ) { for ( int i =0; i < A . length ; ++ i ) { System . out . print ( A [ i ]); System . out . print ( " " ); } System . out . println (); } /* * * Risolve il p r o b l e m a della b a n d i e r a n a z i o n a l e : ordina l ’ array * A [] ( che puo ’ c o n t e n e r e solo i valori 1= VERDE , 2= BIANCO , * 3= ROSSO ) in una unica passata . * * Questo metodo ha c o m p l e s s i t a ’ Theta ( n ). */ public static void ordina ( int [] A ) { int n = A . length ; // numero e l e m e n t i de int v = 0; // Prima p o s i z i o n e in cui si puo ’ i n s e r i r e un e l e m e n t o verde int i = 0; // Prima p o s i z i o n e in cui si puo ’ i n s e r i r e un e l e m e n t o bianco int r = A . length -1; // Prima p o s i z i o n e in cui si puo ’ i n s e r i r e un e l e m e n t o rosso /* * * L ’ a l g o r i t m o m a n t i e n e le s e g u e n t i i n v a r i a n t i : * * 1. Gli e l e m e n t i A [0]... A [v -1] sono verdi ( se v =0 , non ci * sono e l e m e n t i verdi ); * * 2. Gli e l e m e n t i A [ v ].. A [i -1] sono bianchi ( se i == v , non ci 2 * sono e l e m e n t i bianchi ); * * 3. Gli e l e m e n t i A [ r +1].. A [n -1] sono rossi ( se r == n -1 , non * ci sono e l e m e n t i rossi ); * * 4. Gli e l e m e n t i A [ i ]... A [ r ] possono essere di q u a l s i a s i colore * * Ad ogni iterazione , l ’ a l g o r i t m o esamina A [ i ] , e f f e t t u a n d o * o p p o r t u n i scambi per s p o s t a r e l ’ e l e m e n t o nella p o s i z i o n e * c o r r e t t a . Ad ogni iterazione , i viene i n c r e m e n t a t o OPPURE r * viene d e c r e m e n t a t o . Cio ’ a s s i c u r a che sia g a r a n t i t a la * t e r m i n a z i o n e dell ’ a l g o r i t m o . */ while ( i <= r ) { if ( BIANCO == A [ i ] ) { i ++; } else { if ( VERDE == A [ i ] ) { swap ( A , v , i ); v ++; i ++; } else { // A [ i ] e ’ rosso swap ( A , i , r ); r - -; } } } } public static void main ( { int p1 [] = {1 , 2 , 3 , Bandiera . ordina ( p1 ); Bandiera . stampa ( p1 ); int p2 [] = {1 , 1 , 1 , Bandiera . ordina ( p2 ); Bandiera . stampa ( p2 ); int p3 [] = {2 , 2 , 2 , Bandiera . ordina ( p3 ); Bandiera . stampa ( p3 ); int p4 [] = {3 , 3 , 3 , Bandiera . ordina ( p4 ); Bandiera . stampa ( p4 ); int p5 [] = {3 , 1 , 1 , Bandiera . ordina ( p5 ); Bandiera . stampa ( p5 ); int p6 [] = {3 , 3 , 3 , Bandiera . ordina ( p6 ); Bandiera . stampa ( p6 ); } String [] args ) 1 , 3 , 1 , 2 , 3 , 3 , 1 , 1 , 2 , 3 , 1 , 1 }; 1}; 2}; 3}; 3 , 1 , 3}; 2}; } 3 2 Un problema apparentemente complicato (problema 4.8 del libro di testo). Descrivere un algoritmo che dato un array A[1..n] di interi appartenenti all’insieme {1, 2, . . . k}, preprocessa l’array in tempo O(n + k) in modo da generare una opportuna struttura dati che consenta di rispondere in tempo O(1) a query del tipo: “quanti elementi di A sono compresi nell’intervallo [a, b]?” (per qualsiasi 1 ≤ a ≤ b ≤ k) /* * I n t e r v a l l i . java - questo p r o g r a m m a risolve il p r o b l e m a 4.9 p . 116 * di Demetrescu , Finocchi , Italiano , " A l g o r i t m i e s t r u t t u r e dati " * ( seconda e d i z i o n e ) , McGraw - Hill , 2008. * * D e s c r i v e r e un a l g o r i t m o che dato un array A di n numeri interi , * tutti c o m p r e s i nell ’ i n t e r v a l l o [1 , k ] , p r e p r o c e s s a l ’ array in tempo * O ( n + k ) in modo da g e ne r a r e una o p p o r t u n a s t r u t t u r a dati che * c o n s e n t a di r i s p o n d e r e in tempo O (1) a query del tipo : ‘‘ quanti * e l e m e n t i di $A$ sono c o m p r e s i nell ’ i n t e r v a l l o [a , b ]? ’ ’ ( per * q u a l s i a s i $1 \ leq a \ leq b \ leq k$ ) * * Version 0.1 del 2 0 0 9 / 1 1 / 0 9 * Autore : Moreno M a r z o l l a ( m a r z o l l a ( at ) cs . unibo . it ) * * This file has been r e l e a s e d by the author in the Public Domain */ public class Intervalli { int sum []; // sum [ i ] e ’ il numero di e l e m e n t i di A che sono minori // o uguali di i , i =1 ,2 ,... k . sum [0] vale zero int k ; /* * * P r e p r o c e s s a l ’ array A [] ( che puo ’ c o n t e n e r e e s c l u s i v a m e n t e * valori interi c o mp r e s i nell ’ i n t e r v a l l o [1 ,... k ]) in modo da * poter poi r i s p o n d e r e in tempo O (1). * * Questo metodo ha c o m p l e s s i t ’ Theta ( n + k ) , essendo n il numero di * e l e m e n t i in A []. */ public Intervalli ( int A [] , int k ) { this . k = k ; int i ; sum = new int [ k +1]; // I n i t i a l i z z a sum [] a zero for ( i =0; i < k +1; ++ i ) sum [ i ] = 0; // Calcola i valori si sum [] i t e r a n d o una sola volta sull ’ array A [] for ( i =0; i < A . length ; ++ i ) { sum [ A [ i ]]++; } // Calcola le somme for ( i =1; i < k +1; ++ i ) { sum [ i ] = sum [i -1]+ sum [ i ]; } 4 } /* * * R e s t i t u i s c e il numero di e l e m e n t i di A [] c o m p r e s i tra a e b * ( estremi inclusi ). Questo metodo ha c o m p l e s s i t ’ O (1). */ public int conta ( int a , int b ) { // si assume che 1 <= a <= b <= k return ( sum [ b ] - sum [a -1]); } public static void main ( String args [] ) { int A [] = {1 , 2 , 3 , 1 , 3 , 2 , 2 , 4 , 2 , 3 , 1 , 3 , Intervalli interv = new Intervalli (A ,4); System . out . println ( " Numero elementi in [1 ,2] = System . out . println ( " Numero elementi in [2 ,3] = System . out . println ( " Numero elementi in [3 ,4] = } 2 , 2 , 4 , 1 , 1 , 1 , 2}; " + interv . conta (1 ,2)); " + interv . conta (2 ,3)); " + interv . conta (3 ,4)); } 3 L’attitudine al problema vs l’attitudine alla soluzione (problema 4.11 del libro di testo). Progettare un algoritmo che, dato in input un array A[1..n] contenente n valori reali, restituisca in tempo O(n) un qualsiasi numero che non appartenga ad A. Dimostrare che Ω(n) è un limite inferiore al costo computazionale di questo problema. Soluzione Visto che qui si richiede di individuare un numero qualsiasi che non appartiene all’insieme dato, si può procedere nel modo seguente: • Si calcola il minimo x dell’array (questa operazione ha costo Θ(n)); • Si restituisce come risultato il valore x−1, che sicuramente non appartiene ad A. È immediato che l’algoritmo cosı̀ fatto ha complessità Θ(n). Per quanto riguarda il limite inferiore, è sufficiente considerare che per individuare un numero che non appartenga al vettore dato, è sempre necessario esaminare ciascun elemento del vettore almeno una volta. Se i valori non venissero tutti presi in considerazione, ci sarebbe sempre la possibilità che il valore restituito possa essere presente tra quelli non esaminati. Da ciò si conclude the Ω(n) è un limite inferiore alla complessità di questo problema. 5 4 Verifica Min-Heap Quesito presente nel primo parziale dell’Anno Accademico 2009/2010 Scrivere un algoritmo efficiente per risolvere il seguente problema: dato un array A[1..n] di n > 0 valori reali, restituire true se l’array A rappresenta un min-heap binario, false altrimenti. Calcolare il costo computazionale nel caso pessimo e nel caso ottimo dell’algoritmo proposto, motivando le risposte. Soluzione Una possibile soluzione è la seguente: Algoritmo isMinHeap( Array A[1..n] ) for i:=1 to n do if ( 2*i <= n && A[i] > A[2*i] ) then return false; endif if ( 2*i+1 <= n && A[i] > A[2*i+1] ) then return false; endif endfor return true; Si notino i controlli (2*i<=n) e (2*i+1<=n): tali controlli sono indispensabili per essere sicuri che i figli dell’elemento A[i] (rispettivamente A[2 ∗ i] e A[2 ∗ i + 1]) siano effettivamente presenti nell’array. Se tali controlli non sono effettuati, l’algoritmo è da considerarsi sbagliato in quanto può causare l’indicizzazione di un array al di fuori dei limiti. Il caso pessimo si ha quando il ciclo “for” viene eseguito interamente, ossia quando l’array A[1..n] effettivamente rappresenta un min-heap. In questo caso il costo è Θ(n). Il caso ottimo si verifica quando la radice A[1] risulta maggiore di uno dei due figli (A[2] o A[3], se esistono). In questo caso il ciclo “for” esce alla prima iterazione restituendo false, e il costo risulta O(1). In definitiva, si può dire che il costo di questo algoritmo è O(n). 5 Modifica di Selection Sort Quesito presente nel primo parziale dell’Anno Accademico 2009/2010 Considerare la seguente variante dell’algoritmo selectionSort(): Algoritmo mioSelectionSort(array A[1..n]) for k:=0 to n-2 do m:=k+1; for j:=k+2 to n do if (A[j] <= A[m]) then m := j; endif endfor 6 scambia A[k+1] con A[m]; endfor Ricordiamo che un algoritmo di ordinamento è stabile se preserva l’ordinamento relativo degli elementi uguali presenti nell’array da ordinare. L’algoritmo mioSelectionSort() è un algoritmo di ordinamento stabile? In caso negativo, mostrare come modificarlo per farlo diventare stabile. Motivare le risposte. Soluzione L’algoritmo mioSelectionSort() è quasi identico all’algoritmo SelectionSort() descritto nel libro di testo, con la differenza che la condizione stata scritta come if ( A[j] <= A[m] ) (usando l’operatore ≤ anziché =). Questo fa sı̀ che mioSelectionSort non sia un algoritmo di ordinamento stabile, perché in caso di elementi uguali mette in prima posizione l’ultimo elemento trovato. Per renderlo stabile sufficiente riscrivere l’if come if ( A[j] < A[m] ) 7