Algoritmica, appello del 11.01.2005 (PRIMA PARTE) Esercizio 1 [punti 6+4] 1. Dimostrare che (n log n) è il limite inferiore alla complessità in tempo al caso pessimo per gli algoritmi di ordinamento basati su confronti di n elementi. 2. Perché il Counting Sort non rispetta questo limite inferiore ? >>>> Consultare il libro di testo. <<<<< Esercizio 2 [punti 8] Calcolare la complessità in tempo al caso pessimo del seguente algoritmo ricorsivo. Foo(A, n) q=n/3; r=n/2; if(n < 7) return 1; if(n < 19) return (Foo(A,q) + Foo(A,r)); if(n < 29) return (Foo(A,q) * Foo(A,q)); for(i=1; i<n; i= i*2) { for(j=n; j>0; j--) r = r + 5; } return( r + Foo(A,q) ); Siccome la nostra analisi è asintotica possiamo derivare la seguente relazione di ricorrenza: T(n) = O(1), per n < 29; e T(n) = T(n/3) + O(n log n), per n ≥ 29. Infatti, per n<29 il numero di passi ricorsivi che verrebbero eseguiti dall’algoritmo è indipendente da n e dunque costante. Risolvendo la relazione di ricorrenza, calcoliamo il termine nlog_b a =nlog_3 1=n0=(1). Ci troviamo nel caso 3 del teorema principale, per cui la soluzione è T(n) = O(n log n). Esercizio 3 [punti 6+6] Dato un array A[1,n] di elementi distinti e ordinati, un elfo dispettoso decide di scambiare una coppia di elementi distinti in A. 1. Dato A, descrivere un algoritmo ottimo per individuare le due posizioni i e j in cui è avvenuto lo scambio. Commentare la soluzione proposta e calcolarne la complessità. 2. Dato A e la posizione i di uno degli elementi scambiati, descrivere un algoritmo ottimo per individuare la posizione j dell’altro elemento scambiato. Commentare la soluzione proposta e calcolarne la complessità. Trova1_ij(A,n) // La complessità è O(n) for(i=1; (i<n) && (A[i] < A[i+1]); i++) ; for(j=n; (j>1) && (A[j] > A[j-1]); j--) ; return <i,j>; Trova2_ij(A,n,i) // Assumiamo che A[i] sia l’elemento a sinistra Return Cercaj(A,i+1,n,i); L’ipotesi che A[i] sia l’elemento più a sinistra induce la seguente relazione: A[i] > A[i+1]. La procedura seguente cerca A[i] nel subarray A[x,y] sfruttando la proprietà che esso è ordinato in modo crescente e si deve avere che l’elemento cercato A[j] è <A[i+1] ≤ A[x]. Quindi se si trova in A[x,y] un elemento <A[i+1] questo è l’elemento A[j] cercato. La complessità in tempo al caso pessimo è O(log n). Cercaj(A,x,y,i) q = (x+y)/2 if(A[q] < A[i+1]) return q; // Ho trovato A[j] if(A[i] < A[q]){ return Cercaj(A,x,q,i); } else { return Cercaj(A,q,y,i); } Algoritmica, appello del 11.01.2005 (SECONDA PARTE) Esercizio 1 [punti 8] Fornire la definizione di albero binario di ricerca 1-bilanciato (o AVL), e descrivere in pseudo-codice l’algoritmo che calcola il valore minimo in esso contenuto, discutendone la complessità al caso pessimo. >>>> Consultare il libro di testo. <<<<< Esercizio 2 [punti 12] Sia dato un albero binario i cui nodi sono colorati di Rosso, Nero o Giallo. Progettare un algoritmo ricorsivo che conta il numero di sottoalberi i cui nodi sono colorati soltanto con due tipi di colori, e questi appaiono in ugual numero. Analizzarne anche la complessità al caso pessimo. La procedura verrà invocata sulla radice dell’albero di cui si vuole effettuare il conteggio suddetto. La procedura invocata sul nodo u restituisce una quadrupla <c,nrossi,nneri,ngialli> dove c = numero di sottoalberi che discendono da u e soddisfano la proprietà richiesta, nrossi = numero di nodi che discendono da u e sono colorati di rosso, nneri = numero di nodi che discendono da u e sono colorati di nero, ngialli = numero di nodi che discendono da u e sono colorati di giallo. La complessità è O(n) trattandosi di una semplice visita posticipata. Nell’algoritmo useremo [expr] per indicare un valore 0 o 1 a seconda che l’espressione booleana expr sia falsa o vera, rispettivamente. Conta_sottoalberi(u) if (u==NULL) return <0,0,0,0>; <c_left,nr_left,nn_left,ng_left> = Conta_sottoalberi(u.left); <c_right,nr_right,nn_right,ng_right> = Conta_sottoalberi(u.right); nrossi = nr_left + nr_right + [u.color == rosso]; nneri = nn_left + nn_right + [u.color == nero]; ngialli = ng_left + ng_right + [u.color == giallo]; c = c_left + c_right; num_col_distinti = (ngialli>0) + (nneri > 0) + (nrossi > 0); if ((num_col_distinti == 2) && ((ngialli==nneri) || (ngialli == nrossi) || (nrossi=nneri)) ) { c++; } return <c,nrossi,nneri,ngialli>; Esercizio 3 [punti 10] Sia dato un grafo G non orientato. Un sottoinsieme K di nodi di G si definisce Clique se ogni nodo di K è collegato a tutti gli altri nodi di K. Progettare un algoritmo che stabilisce se i nodi di G possono essere ripartiti in due insiemi disgiunti K’ e K’’ tali che i nodi di K’ e i nodi di K’’ formino due Clique distinte. Analizzarne la complessità al caso pessimo. Utilizziamo lo schema algoritmico delle combinazioni per generare tutti i vettori binari B di lunghezza n. Assumiamo che B[i]=0 indichi l’appartenenza del vertice i al sottoinsieme K’, e B[i]=1 indichi l’appartenenza del vertice i al sottoinsieme K’’. La procedura Controllo(G,B) verificherà dunque che K’ e K’’ siano due clique distinte, possibilmente una delle due è vuota. La complessità finale è di O(2|V| |E|) Clique(G,B,b) // b è uguale a 0 (K’) o 1 (K’’) for(i=1,dim=0; i≤n; i++) // Calcola dimensione sottografo if (B[i]== b) {dim++;} if(dim≤1) return true; // è certamente una Clique for(i=1; i≤n; i++) { if (B[i] == b) { // “i” è un vertice del sottografo corrente nadj=0; //dimensione della stella uscente di “i” foreach (v Adj[i]) do { nadj++; if (B[v]!=b) return false; // Arco “esce” dal sottografo } if (nadj!=dim-1) return false; } } Controllo (G,B) If (Clique(G,B,0) && Clique(G,B,1)) success; Esiste un modo ottimo di risolvere questo problema. Basta osservare che, preso un qualunque vertice u in G, settiamo K’ = Adj[u] {u} e K’’ = V – K’. Poi verifichiamo che K’ e K’’ siano delle Clique. Questo costa O(|V|) per la creazione dei due insiemi, e O(|E|) per la verifica che siano delle Clique. Infatti basta osservare che i nodi di K’ (resp. K’’) devono avere O(|K’|2) (resp. O(|K’’|2) ) archi in totale e questi devono connettere sono nodi di K’ (resp. K’’).