Esercizi di analisi della complessità di algoritmi ricorsivi

Esercizi risolti
Esercizi di analisi della complessità di algoritmi ricorsivi
Individuazione di un cammino radice-foglia la cui somma delle chiavi è pari a un
valore dato in input, in un albero binario qualsiasi.
Calcolo altezza in O(lg n) in un AVL, dove ogni nodo contiene il fattore di
bilanciamento.
Calcolo del numero degli alberi di Fibonacci
Individuazione antenato più vicino di due nodi dati in un ABR.
Split di un ABR intorno a una chiave data.
Esercizi analisi complessità
Si imposti la relazione di ricorrenza che ne descrive la complessità
e la si risolva utilizzando il metodo della sostituzione
test (A,lo,hi)
if hi ≤ lo then return 1
m = (lo+hi)/2
k=m
a=1
while k ≥ 1 do a = 2*a
k = k-2
return test(A,lo,m) and test(A,m+1,hi)
Il ciclo while viene eseguito n/2 volte quindi la relazione è:
T(n) = 2T(n/2) + Θ(n/2), quindi T(n) = 2T(n/2) + Θ(n)
Questa è la relazione del mergesort che ha soluzione in O(n lg n)
Esercizi analisi complessità
Si imposti la relazione di ricorrenza che ne descrive la complessità e la si
risolva utilizzando il metodo della sostituzione
void f(int T [], int inizio, int fine) {
int n = fine – inizio + 1
int S[n]
if (n>0){
copia(S, T, inizio, fine)
Mergesort(S,n)
f(T, inizio, inizio+n/3)
f(T, inizio+n/3+1, inizio+2*n/3)
f(T, inizio+2*n/3+1, fine)
}
La funzione copia(S,T,inizio,fine) copia i dati da T in S.
Il codice all’esterno delle chiamate viene eseguito in Θ(n lg n)
quindi la relazione è:
T(n) = 3T(n/3) + Θ(nlg n).
La soluzione è in O(n lg2 n)
Esercizi analisi complessità
Si imposti la relazione di ricorrenza che ne descrive la complessità e la si
risolva utilizzando il metodo della sostituzione
test (intero n)
if n ≤ 1 then return 1
k = n*n
while k ≥ 1 do k = k/2
return k + 3*test(n/4)
Il codice all’esterno delle chiamate viene eseguito ha un tempo di
esecuzione in Θ(lg n) quindi la relazione è:
T(n) = T(n/4) + Θ(lg n).
La soluzione è in O(lg2 n)
Esercizi analisi complessità
Si imposti la relazione di ricorrenza che ne descrive la complessità e la si
risolva utilizzando il metodo della sostituzione
test (intero n)
if n ≤ 81 then return 1
k=1
h=1
while k ≤ n do
for j = 1 to k do h++
k=k+1
return 9*h + test(n/3)
Il codice all’esterno delle chiamate viene eseguito ha un tempo di
esecuzione in Θ(n2) quindi la relazione è:
T(n) = T(n/3) + Θ(n2).
La soluzione è in O(n2)
somma dei cammini radice-foglia
Si definisca un algoritmo che prende in input un un albero T e un intero k e restituisce
vero se c’è un cammino radice foglia la cui somma delle chiavi è uguale a k. Si dimostri la
correttezza e si analizzi la complessità dell’algoritmo proposto.
Nell’esempio se k - (x + y) = 0 e anche se k-(x+z) = 0 la risposta è true
altrimenti è false
x
y
z
B
A
C
T1
T2
In generale, al rientro dalla chiamata su A, si vuole avere
vero se il cammino cercato era tra la radice e una foglia
nel sotto albero radicato in A. Ciò vuol dire che la risposta
al problema applicato al figlio sinistro è la risposta al
problema, in caso di risposta true. Il sotto problema
consiste nel determinare un cammino radice foglia nel
sotto albero radicato in A, di somma k diminuito del
valore della chiave di B. Inoltre in questo caso non si
deve fare la chiamata su C.
Se al rientro da A la risposta fosse false, bisogna cercare
nel sotto albero destro e di nuovo la chiamata va fatta
considerando k diminuito del valore della chiave di B.
Se mettessimo in or i due risultati, otterremmo di non fare
la chiamata a destra in caso di risposta true sul figlio
sinistro.
somma dei cammini radice-foglia
Si definisca un algoritmo che prende in input un un albero T e un intero k e
restituisce vero se c’è un cammino radice foglia la cui somma delle chiavi è uguale a
k. Si dimostri la correttezza e si analizzi la complessità dell’algoritmo proposto.
Questo è un esempio in cui si può sfruttare in modo semplice la modifica del
parametro ingresso, k, perché è importante il valore assunto al raggiungimento
di una foglia e non interessa il valore intermedio che si ritrova rientrando nelle
chiamate.
Se l’albero ha un solo nodo allora la risposta è true se la chiave del nodo
coincide con k e falso altrimenti.
Il caso base sarà quindi il caso di un nodo foglia.
Ringrazio Daniele Carnevale per la segnalazione.
somma dei cammini radice-foglia: pseudocodice
PathSum(T, k)
input: T è un albero binario e k un intero
output: dà vero se c’è un cammino radice foglia la cui somma delle chiavi è uguale a k
if (T è una foglia) return (k == T.key);
if (T.left) then B = PathSum(T.left, k - T.key) else B = false
if B return B
if (T.right) return PathSum(T.right, k - T.key))
Correttezza: deriva dalle considerazioni fatte prima.
Complessità: Il tempo di esecuzione al di fuori delle chiamate è costante, nel caso
peggiore il cammino cercato è il più a destra. In tal caso si eseguono sempre le due
chiamate e quindi il tempo di esecuzione asintotico è in O(n).
Infatti la relazione di ricorrenza è T(n) = T(k) + T(n-k-1) + Θ(1), la cui soluzione è in
O(n).
Nel caso migliore il cammino cercato è il più a sinistra e quindi in questo caso il tempo
asintotico è in O(h).
Considerazione sull’uso dei parametri
Abbiamo visto tre esempi relativi all’ uso di un parametro di una funzione ricorsiva:
la select, in cui il valore del parametro modificato doveva essere dato come valore
in output al rientro dalla chiamata, perché serviva un valore diverso da quello
proprio della chiamata. Nella caso del calcolo del cammino interno, il valore del
parametro al rientro dalla chiamata è proprio quello che serve per il calcolo, qui il
risultato relativo all’ultima modifica o viene dato in output o viene semplicemente
composto con quello della successiva chiamata.
Esercizio Alberi di Fibonacci
Si scriva e si analizzi un algoritmo che costruisce un albero binario bilanciato in
altezza di altezza h, con il minimo numero di nodi. Se ne dimostri la
correttezza. Non si deve valorizzare il campo chiave, che potrebbe anche
non essere presente.
Ricordiamo la definizione ricorsiva di albero di Fibonacci:
BASE:
L’albero con un solo nodo è un albero di Fibonacci, di altezza 0, T0
L’albero con due nodi, radice e figlio sinistro, è un albero di Fibonacci, di altezza
1, T1
Passo induttivo:
Se Th-1 e è un albero di Fibonacci di altezza h - 1 e Th-2 e è un albero di
Fibonacci di altezza h - 2, allora Th è ottenuto creando un nuovo nodo radice
che ha come sotto alberi, rispettivamente sinistro e destro, Th-1 e Th-2
Si potrebbe pensare di utilizzare questa definizione direttamente.
10
E. Fachini - Introduzione agli algoritmi
Soluzione Alberi di Fibonacci
algoritmoRic AlberoFib(int h)
input: un intero ≥ -1
output: la radice di un albero di Fibonacci di altezza h
if h = -1 return NIL
T = newNode dà il puntatore a un nodo con i campi a NIL
T.left = AlberoFib(h-1)
AlberoFib(3 )
T.right = AlberoFib(h-2)
return T
Complessità?
T(h) = T(h-1) + T(h-2) +
Θ(1) se n>0
T(h) = Θ(1) se n≤0
T(h) ≥ Fh > (phi)h/51/2 -1
dove phi è la sezione
aurea.
A cosa è dovuta questa
crescita esponenziale?
AlberoFib(2 )
AlberoFib(0)
AlberoFib(1 )
NIL
AlberoFib(1)
NIL
AlberoFib(0)
AlberoFib(0 )
11
E. Fachini - Introduzione agli algoritmi
Soluzione Alberi di Fibonacci
algoritmoRic AlberoFib(int h)
input: un intero ≥ -1
output: la radice di un albero di Fibonacci di altezza h
if h = -1 return NIL
T = newNode
T.left = AlberoFib(h-1)
T.right = AlberoFib(h-2)
return T
Osserviamo che per calcolare AlbFib(h) servono AlbFib(h-1) e AlbFib(h-2), e poi per
calcolare FibAlb(h-1) serve calcolare di nuovo FibAlb(h-2) oltre a FibAlb(h-3),
quindi si potrebbe adottare una soluzione che conserva e "passa" i valori già
calcolati.
I valori già calcolati vengono memorizzati in due parametri della funzione per poter
essere utilizzati nelle diverse chiamate
12
E. Fachini - Introduzione agli algoritmi
Soluzione alberi di Fibonacci
algoritmo AlberoFib2(int h)
input: un intero ≥ -1
output: la radice di un albero di Fibonacci di altezza h
if h = -1 return NIL
Th = newNode
Th-1 = NIL
return AlbFibAus(Th, Th-1,h)
AlbFibAus(Th, Th-1,h)
if h == 0 return Th
T = newNode
T.left = Th
T.right = Th-1
return AlbFibAus(T,Th, h-1)
Il tempo di esecuzione ora è O(h), ma la ricorsione è sostanzialmente
un’iterazione!
13
E. Fachini - Introduzione agli algoritmi
Soluzione Iterativa alberi di Fibonacci
algoritmo AlberoFibIt(int h)
input: un intero ≥ -1
output: la radice di un albero di Fibonacci di altezza h
t = NULL
t1 = NULL
t2 = NULL
i = -1
do
t2 = t1
t1 = t
i = i+1
t = newNode
t.left = t1
t.right = t2
while i<h
return t
14
E. Fachini - Introduzione agli algoritmi
Altezza per AVL
Si supponga che ogni nodo sia implementato con una struttura con tre
campi puntatori, al figlio sinistro, al figlio destro e al padre, e due campi
a valore intero, la chiave e il fattore di bilanciamento. E’ possibile, in tal
caso, calcolare l’altezza di un AVL in O(lg n), invece di O(n), necessario
per un albero qualunque?
15
E. Fachini - Introduzione agli algoritmi
Soluzione
Se il fattore di bilanciamento di un nodo è +1 o 0 si può chiamare la funzione per il
calcolo dell’altezza sul sotto albero sinistro, perché di altezza maggiore o uguale del
destro. Altrimenti si fa la chiamata sul figlio destro. Così si scende dalla radice a una
foglia di profondità massima, incrementando ad ogni passo il valore ottenuto dalla
chiamata.
B
A
B
FB = 0
h
T1
T2
C
A
C
h
h+1
h+1
h
T2
T1
B
A
FB = -1
C
h-1
h
T1
FB = 1
h+1
T2
16
E. Fachini - Introduzione agli algoritmi
h+2
Lo pseudocodice
algoritmo AltezzaAVL(T)
input: T è la radice di un AVL
output: da in output l’altezza di T
if (T = NULL) return -1
if (T.FB == 1 or T.FB == 0)
then return AltezzaAVL(T.left) + 1
return AltezzaAVL(T.right) + 1
Il tempo di esecuzione è in O(h).
E. Fachini - Introduzione agli algoritmi
17
Split ABR
Preso un ABR T (con chiavi tutte distinte) e una sua chiave k, lo split di T intorno
a k è una partizione di T in due ABR T1 e T2 il primo contenente gli elementi più
piccoli di k e l’altro i più grandi.
Come realizzeresti uno split e quanto costa in termini di tempo la soluzione
proposta?
Sugg. Si utilizzino le rotazioni.
Split ABR
Innanzi tutto si trova la chiave k in T.
Poi si risale verso la radice facendo delle rotazioni per portare k alla radice. Se T è
figlio sinistro si fa una rotazione a destra su suo padre, altrimenti a sinistra.
Quando T è diventata la radice il suo sotto albero sinistro e quello destro sono i
due ABR cercati.
Ogni rotazione diminuisce di uno la distanza di k dalla radice e costa O(1), quindi
al più il costo è O(h).
In alternativa si può arricchire la ricerca di k con l’esecuzione delle rotazioni: se k è
minore della chiave del nodo T correntemente in esame, allora k è nel sotto albero
sinistro e facendo una rotazione a destra su T si fa salire il suo figlio sinistro di un
livello; altrimenti, cioè se k è minore della chiave di T, si fa una rotazione a sinistra
facendo salire il figlio destro di T di un livello. Il processo si ripete fino a che la
chiave k è portata alla radice, quindi come nel caso precedente gli alberi cercati
sono quelli radicati nel figlio sinistro rispettivamente destro della radice.
split ABR, nodo figlio sinistro
Rotazione a destra su j
u
u
k
j
T1
T1
j
k
T2
T4
T2
T3
T3
T4
split ABR, nodo figlio destro
u
Rotazione a sinistra su u
k
k
u
T1
j
j
T2
T1
T3
T4
T2
T3
T4
Antenato più vicino
Presi due valori n1 ed n2 ed un ABR T rappresentato tramite puntatori ai
figli sinistro e destro, si scriva un algoritmo ricorsivo, che trovi il più vicino
antenato dei due nodi aventi chiavi n1 ed n2.
Si può assumere che le due chiavi siano presenti in T. L'algoritmo
progettato deve restituire il puntatore all'antenato comune più vicino, cioè
quello i cui discendenti non sono antenati comuni dei nodi chiave di n1 e
n2.
Si dimostri la correttezza e si analizzi la complessità dell’algoritmo
proposto, che dovrebbe essere O(h), dove h denota l'altezza dell'albero.