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’’).