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.