Salta al contenuto
Note per Studenti Visite di alberi

Visite di alberi

In questa pagina 6

Una visita accede a tutti i nodi di un albero eseguendo una operazione su ciascuno. Preorder e postorder sono schemi generali: l'operazione di "visita" si sostituisce con quella che serve al problema. Se l'albero è ordinato i figli si toccano nell'ordine dato.

Algoritmo preorder(T, v)                    Algoritmo postorder(T, v)
Input: nodo v di T                          Input: nodo v di T
visita v                                    foreach w in T.children(v) do
foreach w in T.children(v) do                   postorder(T, w)
    preorder(T, w)                          visita v

Definizione (visite in preorder e postorder). Chiamata iniziale: preorder(T, T.root()). Preorder: prima il padre, poi i sottoalberi dei figli. Postorder: prima i sottoalberi dei figli, poi il padre.

Esempio. Sull'albero con radice rr, figli aa, bb e cc figlio di aa: preorder r,a,c,br,a,c,b e postorder c,a,b,rc,a,b,r.

Sull'albero di riferimento di AlberiAlbero radicato (definizione per padre e ricorsiva), terminologia (antenati, discendenti, nodi interni ed esterni, sottoalbero, albero ordinato), profondità, livello, altezza; altezza = massima profondità delle foglie; algoritmi depth e height con costo; somma dei figli = n-1; esempio di algoritmo Omega(n^2) (heightBad).Alberi → (rr; a,ba,b figli di rr; c,dc,d figli di aa; ee figlio di dd) la traccia è:

visita sequenza come si ottiene
preorder r,a,c,d,e,br, a, c, d, e, b si visita rr, poi tutto TaT_a (che a sua volta inizia da aa, poi cc, poi Td={d,e}T_d=\{d,e\}), poi TbT_b
postorder c,e,d,a,b,rc, e, d, a, b, r si visitano prima i sottoalberi: TaT_a finisce con aa dopo cc e TdT_d (cioè e,de, d), poi bb, infine rr

In entrambi i casi ogni nodo compare una sola volta e rr è primo nel preorder e ultimo nel postorder. La ricorsione è quella di Algoritmi ricorsiviAlgoritmi ricorsivi come induzione eseguita; albero della ricorsione e record di attivazione nello stack; esempi ReverseArray, LinearSum, Power in tempo logaritmico, Fibonacci ricorsivo (esponenziale) e con memoizzazione; analisi della complessità con l'albero della ricorsione e correttezza per induzione.Algoritmi ricorsivi →: lo stack contiene il cammino dalla radice al nodo corrente, quindi occupa Θ(altezza)\Theta(\text{altezza}).

Complessità

Formula (complessità di una visita). L'albero della ricorsione (vedi Algoritmi ricorsiviAlgoritmi ricorsivi come induzione eseguita; albero della ricorsione e record di attivazione nello stack; esempi ReverseArray, LinearSum, Power in tempo logaritmico, Fibonacci ricorsivo (esponenziale) e con memoizzazione; analisi della complessità con l'albero della ricorsione e correttezza per induzione.Algoritmi ricorsivi →) ha un nodo per ogni nodo di TT. Sia tut_u il costo della visita di uu e cuc_u il numero di figli. La chiamata su uu costa Θ(1+cu)+tu\Theta(1 + c_u) + t_u (il ciclo sui figli, escluse le chiamate ricorsive, più la visita). Poiché ∑ucu=n−1\sum_u c_u = n - 1 (vedi AlberiAlbero radicato (definizione per padre e ricorsiva), terminologia (antenati, discendenti, nodi interni ed esterni, sottoalbero, albero ordinato), profondità, livello, altezza; altezza = massima profondità delle foglie; algoritmi depth e height con costo; somma dei figli = n-1; esempio di algoritmo Omega(n^2) (heightBad).Alberi →: ogni nodo non radice ha un solo padre, quindi gli archi sono n−1n-1):

t(n)=∑u∈T(Θ(1+cu)+tu)=Θ(n+∑u∈Ttu).t(n) = \sum_{u \in T} \big(\Theta(1 + c_u) + t_u\big) = \Theta\Big(n + \sum_{u \in T} t_u\Big).

Se ogni visita costa Θ(1)\Theta(1) si ha Θ(n)\Theta(n); se tu∈Θ(log⁡n)t_u \in \Theta(\log n) per ogni uu si ha Θ(nlog⁡n)\Theta(n \log n).

Esempio. In un albero con n=7n=7 nodi ∑ucu=n−1=6\sum_uc_u=n-1=6, quindi ∑u(1+cu)=2n−1=13\sum_u(1+c_u)=2n-1=13.

Esempi

Preorder: indice di un libro. Nodi = capitoli e sezioni, "visita" = stampa del titolo; il preorder produce l'indice nell'ordine di lettura (Capitolo 1, Sezione 1.1, Sezione 1.2, Sezione 1.2.1, ...). È anche il modo in cui i file system mostrano cartelle e file.

Postorder: spazio occupato. Ogni nodo ha size proprio (una directory non include i file contenuti). Si vuole in size lo spazio complessivo. Il padre ha bisogno dei totali dei figli già calcolati, quindi serve il postorder.

Algoritmo diskSpace(T, v)
Input: nodo v di T      Output: spazio totale del sottoalbero T_v (e aggiornamento di v.size)
foreach w in T.children(v) do v.size <- v.size + diskSpace(T, w)
return v.size

Il padre ha bisogno dei risultati dei figli esattamente come in una sommatoriaIl simbolo di sommatoria, le sue proprietà (linearità, additività, cambio di indice) e le somme notevoli di Gauss e geometrica.Sommatorie → annidata: il totale di un nodo è la sua dimensione propria più i totali dei sottoalberi dei figli. Esempio: /(1K) ha docs(1K; file di 3K e 10K) e src(1K; file main.c di 5K e cartella lib(1K) con util.c di 4K). Postorder: a.pdf, b.pdf, docs =1+3+10=14= 1+3+10 = 14, main.c, util.c, lib =1+4=5= 1+4 = 5, src =1+5+5=11= 1+5+5 = 11, / =1+14+11=26= 1+14+11 = 26K. Costo Θ(n)\Theta(n).

L'algoritmo height di AlberiAlbero radicato (definizione per padre e ricorsiva), terminologia (antenati, discendenti, nodi interni ed esterni, sottoalbero, albero ordinato), profondità, livello, altezza; altezza = massima profondità delle foglie; algoritmi depth e height con costo; somma dei figli = n-1; esempio di algoritmo Omega(n^2) (heightBad).Alberi → è un altro postorder: la "visita" calcola l'altezza del nodo dalle altezze dei figli.

Memorizzare profondità e altezza di ogni nodo

  • Profondità v.depth: serve prima il padre, quindi un preorder con la profondità passata come parametro: visita(T, v, d): v.depth <- d, poi per ogni figlio visita(T, w, d+1); chiamata iniziale con d=0d = 0. Costo Θ(n)\Theta(n). Invocare depth da ogni nodo costerebbe invece Θ(∑depth)\Theta(\sum \text{depth}), fino a Θ(n2)\Theta(n^2) (in un albero ridotto a un cammino le profondità sono 0,1,…,n−10,1,\dots,n-1 e la somma è n(n−1)2\frac{n(n-1)}{2}, vedi SommatorieIl simbolo di sommatoria, le sue proprietà (linearità, additività, cambio di indice) e le somme notevoli di Gauss e geometrica.Sommatorie →). Sull'albero di riferimento il preorder con dd assegna r:0r{:}0, poi a:1a{:}1, c:2c{:}2, d:2d{:}2, e:3e{:}3, b:1b{:}1.
  • Altezza v.height: postorder: v.height <- 0; per ogni figlio ww dopo la chiamata ricorsiva v.height <- max(v.height, 1 + w.height). Costo Θ(n)\Theta(n).

Antenato comune più basso (LCA)

Definizione (antenato comune più basso, LCA). Dati v,wv, w in TT, LCA(v,w)\text{LCA}(v, w) è l'antenato comune più profondo. Algoritmo: si calcolano dv=depth(v)d_v = \text{depth}(v) e dw=depth(w)d_w = \text{depth}(w) risalendo; si porta il nodo più profondo a salire di ∣dv−dw∣|d_v - d_w| passi, poi si salgono insieme vv e ww finché coincidono. Costo O(depth(v)+depth(w))=O(h)O(\text{depth}(v) + \text{depth}(w)) = O(h), al caso pessimo O(n)O(n) e senza strutture di appoggio. La differenza di profondità è un valore assolutoIl valore assoluto è la distanza di un numero da zero: definizione, proprietà, disuguaglianza triangolare e disequazioni con i moduli.Valore assoluto (modulo) → perché non si sa quale dei due nodi sia più profondo. Perché funziona: dopo aver portato i due nodi alla stessa profondità, i loro antenati alla stessa profondità sono confrontabili e il primo in comune che si incontra salendo è, per definizione, il più profondo tra quelli comuni.

Traccia su LCA(c,e)\text{LCA}(c,e) nell'albero di riferimento: dc=2d_c=2, de=3d_e=3, differenza 11; ee sale di un passo a dd (profondità 22); ora c≠dc\ne d, salgono insieme: c→ac\to a, d→ad\to a; coincidono, quindi LCA(c,e)=a\text{LCA}(c,e)=a.

Esempio. Nell'albero con radice rr, figli aa, bb e cc figlio di aa: LCA(c,b)=r\text{LCA}(c,b)=r e LCA(c,a)=a\text{LCA}(c,a)=a.

Preorder e postorder insieme

Dato l'ordine di visita in preorder, non tutte le sequenze sono postorder possibili. Se in preorder AA è prima di BB e in postorder AA è dopo BB, allora AA è antenato di BB; se AA precede BB in entrambi, AA sta a sinistra di BB (non è suo antenato). La radice è sempre la prima in preorder e l'ultima in postorder: lo si usa per escludere sequenze, come nell'Esercizio 6 · preorder e postorder compatibili.

Errori comuni

  • Usare il preorder quando il padre ha bisogno dei risultati dei figli (somma, altezza): serve il postorder.
  • Scrivere il costo come Θ(n⋅c)\Theta(n \cdot c) con cc numero massimo di figli: è Θ(n)\Theta(n) perché ∑cu=n−1\sum c_u = n - 1.
  • Calcolare depth da ogni nodo (quadratico) invece di passare la profondità nel preorder.

Versione ripasso

  • Preorder: visita vv, poi ricorsivamente i sottoalberi dei figli. Postorder: prima i sottoalberi dei figli, poi vv. Sono schemi generali da adattare alla visita (vedi AlberiAlbero radicato (definizione per padre e ricorsiva), terminologia (antenati, discendenti, nodi interni ed esterni, sottoalbero, albero ordinato), profondità, livello, altezza; altezza = massima profondità delle foglie; algoritmi depth e height con costo; somma dei figli = n-1; esempio di algoritmo Omega(n^2) (heightBad).Alberi →). Chiamata iniziale preorder(T, T.root()).
  • Esempio: con radice rr, figli aa, bb e cc figlio di aa: preorder r,a,c,br,a,c,b; postorder c,a,b,rc,a,b,r. Sull'albero di riferimento: preorder r,a,c,d,e,br,a,c,d,e,b; postorder c,e,d,a,b,rc,e,d,a,b,r. rr è primo nel preorder e ultimo nel postorder.
  • Costo: la chiamata su uu costa Θ(1+cu)+tu\Theta(1+c_u)+t_u, con cuc_u figli e tut_u costo della visita; con ∑ucu=n−1\sum_u c_u=n-1 il totale è Θ(n+∑utu)\Theta\big(n+\sum_u t_u\big). Con tu=Θ(1)t_u=\Theta(1) è Θ(n)\Theta(n). Esempio: con n=7n=7, ∑u(1+cu)=2n−1=13\sum_u(1+c_u)=2n-1=13.
  • Preorder, indice di un libro: capitoli e sezioni nell'ordine di lettura; è anche il modo in cui i file system mostrano cartelle e file.
  • Postorder, spazio occupato (diskSpace): il padre somma i totali dei figli, che devono essere già calcolati. Esempio: / (1K) con docs (1K, file 3K e 10K) e src (1K, file main.c 5K e cartella lib 1K con util.c 4K): docs =14=14, lib =5=5, src =11=11, / =26=26K. Costo Θ(n)\Theta(n).
  • Altezza v.height: postorder con v.height <- max(v.height, 1 + w.height) per ogni figlio.
  • Profondità v.depth: preorder con la profondità dd passata come parametro: visita(T,v,d) pone v.depth <- d e chiama visita(T,w,d+1) sui figli; Θ(n)\Theta(n). Chiamare depth da ogni nodo costerebbe fino a Θ(n2)\Theta(n^2) su un cammino.
  • LCA (antenato comune più basso): si calcolano dvd_v e dwd_w; il nodo più profondo sale di ∣dv−dw∣|d_v-d_w| passi, poi entrambi salgono insieme finché coincidono. Costo O(h)O(h). Traccia di LCA(c,e)\text{LCA}(c,e): dc=2d_c=2, de=3d_e=3; ee sale a profondità 22, poi c→ac\to a e d→ad\to a: risultato aa.
  • Compatibilità preorder/postorder: se AA precede BB nel preorder e lo segue nel postorder, AA è antenato di BB; se precede BB in entrambi, AA sta a sinistra di BB. Serve per l'Esercizio 6 · preorder e postorder compatibili.
  • Errori tipici: preorder dove serve il postorder (il padre ha bisogno dei figli); costo scritto come Θ(n⋅c)\Theta(n\cdot c) invece di Θ(n)\Theta(n); depth chiamato da ogni nodo.

Esercizi su questo argomento

Teoria collegata