Dati e Algoritmi 1: A. Pietracaprina Alberi 1 Cos’è un albero? root$ Albero Informalmente: • collezione di nodi; • collegamenti: • minimali (che garantiscano la connessione); • di tipo padre-figlio • struttura gerarchica (lista = caso estremo di albero) 2 Campi Applicativi 1 strutture dati: dizionari; code con priorità 2 esplorazione risorse: filesystem; siti di e-commerce; 3 sistemi distribuiti e reti di comunicazione: sincronizzazione, broadcast, gathering 4 analisi di algoritmi: albero della ricorsione 5 classificazione: alberi di decisione 6 compressione di dati (codici di Huffman) 7 biologia computazionale: alberi filogenetici 3 Alberi (Tree) (Capitolo 8 [GTG14]) Definizione di Albero radicato (rooted tree) Un albero radicato T è una collezione di nodi tale che • T è vuoto oppure . • ∃ un nodo speciale r ∈ T (r = radice) • ∀v ∈ T , v 6= r : ∃!u ∈ T : u è padre di v (v è figlio di u) • ∀v ∈ T , v 6= r : risalendo di padre in padre si arriva a r (= ogni nodo è discendente dalla radice.) r" u" v" 4 Nota Nel libro la condizione: • ∀v ∈ T , v 6= r : risalendo di padre in padre si arriva a r (= ogni nodo è discendente dalla radice.) manca. Senza di questa il seguente r" u" v" con u padre di v e v padre di u è un albero... 5 Alberi: altre definizioni r" u" v" w" z" Antenati x è antenato di y se x = y oppure x è antenato del padre di y (es: u è antenato di w Discendenti x è discendente di y se y è antenato di x Nodi interni Nodi con ≥ 1 figli 6 Alberi: altre definizioni r" u" v" w" z" Nodi esterni (= foglie) Nodi senza figli. Sottoalbero con radice v Tv = albero formato da tutti i discendenti di v Albero ordinato T è un albero ordinato se per ogni nodo interno v ∈ T è definito un ordinamento lineare tra i figli u1 , u2 , . . . , uk di v . 7 Definizione Ricorsiva Definizione ricorsiva di albero radicato Un albero radicato T è una collezione di nodi tale che: • T è vuoto oppure • È definita una partizione T = {r } ∪ T1 ∪ T3 ∪ · · · ∪ Tk con k ≥ 0, dove: • r è radice • ∀i, 1 ≤ i ≤ k: Ti è albero non vuoto con radice ui figlio di r r" u1" u2" T1" T2" uk" …" Tk" 8 Alberi: ancora definizioni Profondità (depth) di un nodo v in un albero T Definizioni alternative: 1 profondità depthT (v ) di v = |antenati(v )| - 1; 2 • se v = r radice ⇒ depth(v ) = 0 • altrimenti depth(v ) = 1 + depth(padre(v )) Livello i Insieme dei nodi a profondità i (∀i ≥ 0) Altezza (height) di un nodo v in un albero T • se v è foglia ⇒ height(v ) = 0 • altrimenti height(v ) = 1 + maxw :w figlio di v (height(w )) 9 Alberi: ancora definizioni... e una proposizione Altezza di un albero T Altezza height(T ) di T = height(r ), r è radice di T Proposizione (Proposition 8.3 [GTG14]) Dato un abero T , height(T ) = maxv ∈T :v foglia (depth(v )) 10 Esempio DEPTH& LIVELLO& h=4& 0& 0& h=2& 1& 2& h=0& h=1& h=0& h=2& h=3& h=1& h=0& h=0& h=1& 1& h=0& h=2& h=0& 2& h=0& 3& 4& 3& h=0& altezza& 4& 11 Proposizione (Proposition 8.3 [GTG14]) Dato un albero T , height(T ) = maxv ∈T :v foglia (depth(v )) Dimostrazione (Esercizio R-8.2 [GTG14]) Induzione su n = numero di nodi di T • base: n=1 ⇒ OK • passo induttivo: fisso n ≥ 1 Hp. induttiva: proprietà vale per alberi con m nodi, 1≤m≤n T = albero con n + 1 nodi. n + 1 > 1 ⇒ T è del tipo: r" u1" u2" T1" T2" uk" k≥1" …" Tk" 12 Dimostrazione (continua) r" u1" u2" T1" height(T ) uk" k≥1" …" T2" =(def ) =(hp.ind.) = = = Tk" 1 + max (height(ui )) 1≤i≤k 1 + max ( max (depthTi (v ))) 1≤i≤k v ∈Ti v foglia max ( max (1 + depthTi (v ))) 1≤i≤k v ∈Ti v foglia max max depthT (v ) 1≤i≤k v ∈Ti v foglia max depthT (v ) v ∈T v foglia 13 Implementazione di un albero generale (Sezione 8.1.1 [GTG]) Struttura linkata . Nodo (= Position<E>) parent& element& children& . Albero (= Tree<E>) numElements$ root$ 14 15 Calcolo di profondità ed altezza (Par. 8.1.2 [GTG14]) depth(T ,v ) Input: v ∈ T Output: profondità di v in T if T .isRoot(v ) then return 0; else return 1+depth(T ,T .parent(v )); Osservazione Il libro non usa i metodi dell’interfaccia Tree 16 Complessità In funzione della profondità d di v ( c1 t(d) = t(d − 1) + c2 d =0 d >0 c1 , c2 > 0 costanti Dimostrare per induzione su d che t(d) = c2 d + c1 , per ogni d ≥ 0 (ESERCIZIO PER CASA) ⇒ t(d) = Θ (d + 1) N.B.: [GTG14] è meno formale e non usa una relazione di ricorrenza 17 Algoritmo iterativo per la profondità iter-depth(T ,v ) Input: v ∈ T Output: profondità di v in T d ←0; while !(T .isRoot(v )) do v ← T .parent(v ); d ←d +1 return d Complessità Θ (d + 1) 18 height(T ,v ) Input: v ∈ T Output: altezza di v in T h ← 0; foreach w ∈ T .children(v ) do h ← max{h, 1+height(T ,w )} ; return h Osservazione La complessità dell’algoritmo può essere espressa tramite una relazione di ricorrenza, in funzione della taglia (= numero di nodi) del sottoalbero Tv . Tuttavia l’utilizzo della relazione di ricorrenza risulta complicato dal fatto che il numero di chiamate ricorsive e la taglia dei sottoproblemi risolti da queste chiamate somno variabili che dipendono dalla istanza. In questo caso, per l’analisi facciamo un argomento ad hoc (pag. 287 [GTG14]). 19 Complessità di height(T , v ) Claim 1 L’algoritmo viene invocato esattamente una volta su ogni nodo u ∈ Tv . Dimostrazione: per induzione sull’altezza di Tv (ESERCIZIO) Claim 2 (Proposizione 8.4 [GTG14]) Si denoti con cu il numero di figli di un P nodo u ∈ Tv e sia n il numero di nodi in Tv . Allora si ha che u∈Tv cu = n − 1. Dimostrazione: Ogni nodo di Tv tranne la radice ha un padre (unico) in Tv e contribuisce 1 alla sommatoria. 20 Complessità di height(T , v ) (continua) Si osservi che in una qualsiasi invocazione height(T ,u) viene eseguito un numero di operazioni proporzionale a (1 + cu ), escludendo le operzioni eseguite nelle chiamate ricorsive all’interno di tale invocazione. Di conseguenza, la complessità di height(T ,v ) (in funzuione di n = |Tv |) è theight (n) = Θ (sumu∈Tv (1 + cu )) (Claim 1) X = Θ n + cu u∈Tv = Θ (n) (Claim 2) Osservazione L’analisi vale per qualsiasi istanza, ovvero per qualsiasi sottoalbero Tv con n nodi. Infatti la complessità è stata espressa con Θ (). 21 Algoritmo alternativo per il calcolo dell’altezza di un albero heightBad(T ) Input: albero T Output: altezza di T h ← 0; foreach w ∈ T .positions() do if T .isExternal(w ) then h ← max{h, depth(T ,w ) }; return h Complessità Escludendo le invocazioni a depth: Ω (n), dove n = |T | P ⇒ complessità ∈ Ω n + v :v foglia (1 + dv ) dove dv = depth(T ,v ). 22 Esercizio C-8.27 [GTG] P Ω n + v :v foglia (1 + dv ) ∈ Ω n2 Idea n/2$ n/2$ n/2$ n/2$ n/2$ …" n/2$ n/2$ …" n/2$ Dimostrare che la complessità è Θ n2 : ESERCIZIO PER CASA 23 Visite di Alberi Visita Accedere a tutti i nodi dell’albero eseguendo una qualche operazione ad ogni nodo (“visita”). Preorder Prima il padre e poi (ricorsivamente) i sottoalberi radicati nei figli. Postorder Prima (ricorsivamente) i sottoalberi radicati nei figli, poi il padre. 24 Algoritmo preorder preorder(T ,v ) Input: nodo v ∈ T Output: visita di tutti i nodi di Tv visita v ; foreach w ∈ T .children(v ) do preorder(T ,w ) Nota Se l’albero è ordinato la visita tocca i figli di un nodo A nell’ordine dato. A Complessità B Θ (n), n = |Tv | (analisi: come per height(T ,v )) Chiamata iniziale preorder(T ,T .root()) C B E E C FF D D G G A(B(E(F(C(D(G( A(B(E(F(C(D(G( 25 Algoritmo postorder postorder(T ,v ) Input: nods v ∈ T Output: visita di tutti i nodi di Tv foreach w ∈ T .children(v ) do postorder(T ,w ) visita v ; Nota Se l’albero è ordinato la visita tocca i figli di un nodo A nell’ordine dato. A Osservazione B L’algoritmo height è un esempio di visita in postorder. Complessità Θ (n) (analisi: come per height(T ,v )) C B E E C FF D D G G E(F(B(C(G(D(A( E(F(B(C(G(D(A( 26 Applicazioni delle visite Preorder . Albero = struttura di un libro in sezioni (capitoli, paragrafi, ecc.) . . Visita = indice del libro Operazione = stampa nome sezione Book: Chapter1, Section 1.1., Section 1.2, Section 1.2.1, Section 1.2.2, Chapter 2, Section 2.1, Section 2.2, Section 2.2.1, Section 2.2.2 Altro esempio: modo in cui spesso i file system presentano la sequenza di cartelle, file 27 Applicazioni delle visite Postorder . Albero = struttura del file system . Nodo interno = directory . Nodo foglia = file . Operazione = calcolo spazio totale di ogni directory . v .size = spazio occupato dal nodo v all’inizio. Se v è interno v .size non include lo spazio dei suoi discendenti. 28 Algoritmo diskSpace(T ,v ) Input: nodo v ∈ T Output: spazio aggregato di Tv + aggiornamento di u.size ∀u ∈ Tv con lo spazio aggregato di Tu s ← 0; foreach w ∈ T .children(v ) do s ← s+diskSpace(T ,w ) ; v .size ← v .size +s; return v .size; Esempio 1K# 1K# 10K# 5K# 3K# 4K# 1K# 8K# 3K# 29 Definizione Sia T albero ordinato, u, v ∈ T allo stesso livello: u è a sinistra di v (⇒ v è a destra di u) se u viene prima di v nella visita in preorder (NB: si assume che nella visita i figli di un nodo siano visitati secondo l’ordine a loro assegnato) Osservazione La definizione è coerente con il modo di disegnare gli alberi. 30 Riferimenti dal testo [GTG14]: • Par. 8.1 • Par. 8.4.1 31 Esercizi Esercizio 1 Si supponga che la visita in preorder di un albero ordinato di 6 nodi incontri i nodi nell’ordine ABCDEF. 1 Dire quali delle seguenti sequenze può rappresentare la visita in postorder dello stesso albero (motivando la risposta): BAFECD, CDBFEA, CDAEFB. 2 Disegnare l’albero compatibile con le due sequenze di preorder e postorder. 1 Dall’ordine preorder si deduce che A è radice e quindi deve essere visitato per ultimo in postorder. Quindi l’unica sequenza compatibile è CDBFEA Ossevazioni: B deve essere il primo figlio di A (da preorder); C e D devono essere figli di B (da preorder e postorder); E deve essere il secondo figlio di A (da preorder); ed F deve essere il figlio di E (da postorder). Il disegno è lasciato come esercizio. 2 32 Esercizio 2 Sia T un albero con n nodi che memorizzano stringhe. Sviluppare un algoritmo ricorsivo che stampi, per ogni v ∈ T la stringa in esso contenuta e la sua altezza, analizzandone la complessità. L’algoritmo è una semplice modifica dell’algoritmo height. Algoritmo allHeights(T ,v ) Input: v ∈ T Output: altezza di v e stampa di tutte le altezze dei nodi in Tv h ← 0; foreach w ∈ T .children(v ) do h ← max{h, 1+allHeights(T ,w )} ; print(h); return h Analisi • Prima invocazione: allHeights(T ,T .root()) • Complessità: Θ (n), come height 33 Esercizio 3 Sia T un albero con n nodi. Sviluppare un algoritmo ricorsivo che stampi, per ogni v ∈ T la sua profondità. Osservazioni • Fare una visita di tutti i nodi e invocare l’algoritmo depth da ciascun nodo per stamparne poi il risultato, non èuna buona strategia in quanto richiederebbe un tempo Θ n2 . • Una strategia efficiente è quella di eseguire una visita in preorder passando come parametro all chiamata sul nodo v anche la sua profondità. 34 L’algoritmo è il seguente: Algoritmo allDepths(T ,v ,d) Input: v ∈ T , d = profondità di v Output: stampa di tutte le profondità dei nodi in Tv print(d); foreach w ∈ T .children(v ) do allDepths(T ,w , d + 1)} ; Analisi • Prima invocazione: allDepths(T ,T .root(),0) • Complessità: Θ (n), come la visita in preorder 35 Esercizio 4 (C-8.50 [GTG14]) Sia T un albero con n nodi. Dati due nodi v , w ∈ T si definisce il Lowest Common Ancestor di v e w (LCA(v , w )) come l’antenato comune più profondo. Sviluppare un algoritmo efficiente per trovare LCA(v , w ), analizzandone la complessità. L’idea dell’algoritmo è di risalire dal più profondo dei due nodi passati in input sino all’antenato che sta alla stessa profondità dell’altro, e da qui risalire da entrambi sino a trovare il primo antenato comune. (Si osservi che un antenato in comune esiste sicuramente ed è la radice.) Per determinare la profondità di un nodo si utilizza l’algoritmo depth visto a lezione. 36 L’algoritmo richiesto è il seguente: Algoritmo LCA(v , w ) input v , w ∈ T output LCA(v , w ) dv ← depth(v ); dw ← depth(w ) if (dv > dw ) then for i ← 1 to dv − dw do v ← T .parent(v ) else for i ← 1 to dw − dv do w ← T .parent(w ) while (v 6= w ) do { v ← T.parent(v ) w ← T.parent(w ) } return v 37 Analisi Le invocazioni di depth hanno complessità proporzionale alle profondità di v e w . I cicli for e il ciclo while eseguono, ciascuno, un numero di iterazioni limitato superiormente dalla massima profondità dei due nodi, e in ciascuna iterazione eseguono un numero costante di operazioni. Dato che la profondità di un nodo di un albero è limitata superiormente dall’altezza dell’albero, concludiamo che la complessità dell’algoritmo è O (h), con h altezza di T . 38