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