Strutture dati: Alberi Operazioni basilari su Alberi Laboratorio di Algoritmi e Strutture Dati M-Z Ingegneria e Scienze Informatiche - Cesena A.A. 2015-2016 Pietro Di Lena [email protected] Formato Newick Strutture dati: Alberi Operazioni basilari su Alberi Formato Newick Definizione I I Definizione di Albero: struttura rappresentata come un grafo completamente connesso in cui ogni coppia di nodi è connessa da un unico percorso. Alcuni esempi di applicazione: I Teoria dei giochi: rappresentazione di tutte le possibili partite (o di un sottoinsieme) di un gioco I Genalogia: alberi genealogici I Biologia: alberi filogenetici (ex. tree of life) Strutture dati: Alberi Operazioni basilari su Alberi Formato Newick Struttura dati Albero: rappresentazione grafica A \ \ A 2 1 3 B C B 2 D C 3 D 1 \ E F G 1 \ 2 2 \ \ \ \ \ 1 2 E I I I F 2 G Vincoli: l’ordine dei figli di un nodo non è rilevante. Come definiamo in C la struttura nodo per alberi generici? La definizione cambia di molto rispetto a quella vista per alberi binari? Strutture dati: Alberi Operazioni basilari su Alberi Formato Newick Definizione della struttura dati nodo per un albero in C 1 # include < stdlib .h > 2 3 4 5 6 7 8 struct node { char nlabel ; int elabel ; struct node * brother ; struct node * children ; }; 9 10 11 struct node * node_alloc ( char nlabel , int elabel ) { struct node * tmp = ( struct node *) malloc ( sizeof ( struct node )); 12 if ( tmp != NULL ) { tmp - > nlabel = nlabel ; tmp - > elabel = elabel ; tmp - > brother = NULL ; tmp - > children = NULL ; } return tmp ; 13 14 15 16 17 18 19 20 I I } Assumiamo che un nodo abbia un identificativo (nome) di tipo char, nlabel. Il campo elabel viene utilizzato per assegnare un peso all’arco che collega il nodo con il rispettivo padre. Strutture dati: Alberi Operazioni basilari su Alberi Generazione di un albero random 1 # define BASE 65 2 3 4 5 6 7 8 9 10 11 /* * G e n e r a z i o n e random di un albero random di altezza massima * height . Ogni nodo ha un numero di figli c o m p r e s o tra 0 e * c hildren . I nodi sono marcati con lettere m a i u s c o l e A - Z * ( i nomi sono ripe t u t i se ci sono piu di 26 nodi ). * I pesi degli archi sono interi random tra 0 e 9. */ struct node * randtree ( int height , int children ) { static int X = 0; 12 if ( height > 0) { int i , n ; struct node *T , * tmp ; 13 14 15 16 T = node_alloc ( BASE +X , rand ()%10); X = ( X +1) % 26; n = random ()%( children +1); for ( i = 0; i < n ; i ++) if (( tmp = randtree ( height -1 , children )) != NULL ) { tmp - > brother = T - > children ; T - > children = tmp ; } return T ; } else return NULL ; 17 18 19 20 21 22 23 24 25 26 27 } Formato Newick Strutture dati: Alberi Operazioni basilari su Alberi Formato Newick Operazioni basilari su Alberi: altezza e distruzione di un Albero 1: function Height(T ) 2: if T = NIL then 3: return 0 4: else 5: n←0 6: for c ∈ Children(T ) do 7: n ← Max(n, Heigth(c)) 8: end for 9: return n + 1 10: end if 11: end function I I 1: function DeleteTree(T ) 2: if T = 6 NIL then 3: for c ∈ Children(T ) do 4: DeleteTree(c) 5: end for 6: Delete(T) 7: end if 8: end function ATTENZIONE: Le istruzioni di visita dei figli di un nodo dipendono dalla struttura utilizzata. L’implementazione in C potrebbe essere un po’ differente da quella presentata nello pseudocodice. I In particolare, nella DeleteTree , se si decide di utilizzare la struttura node suggerita, il ciclo for non è necessario (tutto può essere calcolato in modo ricorsivo). Qual è il costo computazionale di queste funzioni? I Entrambe le procedure visitano ogni nodo dell’albero esattamente una volta: Θ(n), n = numero di nodi Strutture dati: Alberi Operazioni basilari su Alberi Formato Newick Operazioni basilari su Alberi: numero di nodi e numero di foglie 1: function CountNodes(T ) 2: if T = NIL then 3: return 0 4: else 5: n←0 6: for c ∈ Children(T ) do 7: n ← n + CountNodes(c) 8: end for 9: return n + 1 10: end if 11: end function I I 1: function CountLeaves(T ) 2: if T = NIL then 3: return 0 4: else 5: n←0 6: if Children(T ) = NIL then 7: n←1 8: else 9: for c ∈ Children(T ) do 10: n ← n + CountLeaves(c) 11: end for 12: end if 13: return n 14: end if 15: end function Valgono le stesse considerazioni fatte in precedenza per la visita dei figli. Qual è il costo computazionale di queste funzioni? I Entrambe le funzioni visitano ogni nodo dell’albero esattamente una volta: Θ(n), n = numero di nodi Strutture dati: Alberi Operazioni basilari su Alberi Formato Newick Esempio di stampa in ASCII di un Albero Come possiamo stampare su terminale ed in modo graficamente chiaro un Albero? A \ \ A 2 1 3 B C B 2 D C 3 D 1 \ E 1 \ F 2 G 2 \ \ \ \ \ 1 2 E 2 F G 1 2 3 4 5 6 7 A + - B :2 + - C :3 | + - E :1 + - D :1 + - F :2 + - G :2 Strutture dati: Alberi Operazioni basilari su Alberi Formato Newick Stampa di un albero in ascii 1/2 Procedura di stampa principale. Richiede l’implementazione della funzione che calcola l’altezza dell’albero Height(T). 1 2 # include < stdio .h > # include < stdlib .h > 3 4 5 6 7 void print_tree ( struct node * T ) { if ( T != NULL ) { char * indent = ( char *) calloc (2* height ( T ) , sizeof ( char )); struct node * tmp ; 8 printf ( " % c :% d \ n " ,T - > nlabel ,T - > elabel ); for ( tmp = T - > children ; tmp != NULL ; tmp = tmp - > brother ) print_tree_rec ( tmp , indent ,0); free ( indent ); 9 10 11 12 } 13 14 } Strutture dati: Alberi Operazioni basilari su Alberi Stampa di un albero in ascii 2/2 1 2 3 4 5 static void print_indent ( char * indent , int n ) { int i ; for ( i = 0; i < n ; i ++) printf ( " % c " , indent [ i ]); } 6 7 8 9 static void print_tree_rec ( struct node *T , char * indent , int n ) { if ( T != NULL ) { struct node * tmp ; 10 print_indent ( indent , n ); printf ( " + - " ); if (T - > brother == NULL ) // Last c h i l d r e n indent [ n ++] = ’ ’; else // More b r o t h e r s indent [ n ++] = ’| ’; indent [ n ++] = ’ ’; printf ( " % c :% d \ n " ,T - > nlabel ,T - > elabel ); 11 12 13 14 15 16 17 18 19 for ( tmp = T - > children ; tmp != NULL ; tmp = tmp - > brother ) prin t_tree_rec ( tmp , indent , n ); 20 21 } 22 23 } Formato Newick Strutture dati: Alberi Operazioni basilari su Alberi Formato Newick Formato Newick I I I Newick tree: notazione matematica standard e machine-readable per la rappresentazione di alberi. Sfrutta la corrispondenza tra struttura ad albero e parentesi annidate . Regole grammaticali: tree ⇒ descendant list [ root label ] [: root edge label ] ; descendant list ⇒ ( subtree {, subtree } ) subtree ⇒ descendant list [ node label ] [: node edge label ] ⇒ [ leaf label ] [: leaf edge label ] Convenzioni: I Gli elementi in { } possono apparire zero o più volte. I Gi elementi in [ ] sono opzionali, possono apparire una sola volta o mai. Strutture dati: Alberi Operazioni basilari su Alberi Formato Newick Formato Newick: esempi A 2 1 3 B C 1 D 2 E F 2 G I Solo struttura (Type 0): (,(),(,)); I Solo label foglie (Type 1): (B,(E),(F,G)); I Label foglie e label di tutti gli archi (Type 2): (B:2,(E:1):3,(F:2,G:2):1); I Label di tutti i nodi (Type 3): (B,(E)C,(F,G)D)A; I Completo (Type 4): (B:2,(E:1)C:3,(F:2,G:2)D:1)A; Strutture dati: Alberi Operazioni basilari su Alberi Formato Newick Salvataggio in formato Newick: pseudocodice La funzione Tree2Newick salva un albero in formato Newick in una stringa. 1: function Tree2Newick(T, type) 2: S ← ”” . Emptystring 3: if T 6= NIL then 4: S ← S + Subtree2Newick(T , type) 5: end if 6: S ← S+";" 7: return S 8: end function 1: function Subtree2Newick(T , type) 2: S ← ”” 3: if HasChildren(T) then 4: S ← S+"(" 5: for c ∈ Children(T ) do 6: S ← S + Subtree2Newick(c, type) 7: if NotLastChildren(c) then 8: S ← S+"," 9: end if 10: end for 11: S ← S+")" 12: end if 13: return S + Data2Newick(T , type) 14: end function I I I 1: function Data2Newick(T, type) 2: S ← ”” 3: if type = 1 or type = 2 then 4: if IsLeaf(T) then 5: S ← S + NLbl(T ) 6: end if 7: if type = 2 then 8: S ← S+":"+ELbl(T ) 9: end if 10: else if type = 3 then 11: S ← S + NLbl(T ) 12: else if type = 4 then 13: S ← S +NLbl(T )+":"+ELbl(T ) 14: end if 15: return S 16: end function I vari tipi di stampa sono specificati nella slide precedente. NLbl(T) e ELbl(T) indicano le label del nodo e dell’arco, rispettivamente. L’operatore + indica l’operazione di append tra stringhe. Strutture dati: Alberi Operazioni basilari su Alberi Formato Newick Lettura del formato Newick: pseudocodice La funzione Newick2Tree legge un albero da una stringa in formato Newick. 1: function Newick2Tree(S) 2: if S[curr ] = ";" then 3: return NIL 4: else 5: T ← NodeAlloc(DefaultName, DefaultLabel) 6: if S[curr ] = "(" then 7: curr ← curr + 1 8: repeat 9: tmp ← Newick2Tree(S) 10: AddChildren(T , tmp) 11: if A[curr ] = "," then 12: curr ← curr + 1 13: end if 14: until S[curr ] 6= ")" 15: end if 16: Newick2Data(S, T ) 17: return T 18: end if 19: end function I I I I La funzione AddChildren aggiunge un figlio al nodo T. La variabile curr mantiene la posizione attuale da valutare nella stringa. La funzione Newick2Data legge i dati relativi ad un nodo, relativamente al formato adottato .Aggiorna l’indice curr. DefaultName e DefalutLabel sono costanti utilizzate di default nel caso in cui in nome del nodo e la label dell’arco non siano specificate nella stringa newick. Strutture dati: Alberi Operazioni basilari su Alberi Formato Newick Salvataggio e lettura del formato Newick: costo computazionale I I I Quanto costa la funzione Tree2Newick? I Visita ogni nodo dell’albero esattamente una volta: Θ(n) I n = numero di nodi nell’albero. Quanto costa la funzione Newick2Tree? I Valuta ogni carattere della stringa in input: Θ(n) I n = numero di caratteri nella stringa. Entrambe le trasformazioni hanno un costo lineare nella dimensione dell’input.