Grafi: Rappresentazioni e Visite Laboratorio di Algoritmi e Strutture Dati Massimo Benerecetti Rappresentazone di grafi Ci sono due tipi di rappresentazione standard per grafi in un computer: • Rappresentazione a matrice di adiacenza • Rappresentazione a liste di adiacenza Rappresentazone di grafi orientati Rappresentazione a matrice di adiacenza questa volta per rapresentare un grafo orientato. Spazio: |V|2 B C A F D E A B C D E F A 0 1 0 1 0 0 B 0 0 1 0 0 0 C 0 0 0 0 1 0 D 1 0 1 0 0 0 E 0 0 0 1 0 0 F 0 0 0 0 0 0 Rappresentazone di grafi orientati Rappresentazione a liste di adiacenza questa volta per rapresentare un grafo orientato. B C A F D E A B B C C E D A E D F D C Rappresentazone di grafi orientati Rappresentazione a liste di adiacenza questa volta per rapresentare un grafo orientato. Spazio: a |V| + b |E| a b B C A F D E A B B C C E D A E D F D C Rappresentazone di grafi • Matrice di adiacenza • Spazio richiesto O(|V|2) • Verificare se i vertici u e v sono adiacenti richiede tempo O(1). • Molti 0 nel caso di grafi sparsi • Liste di adiacenza • Spazio richiesto O(|E|+|V|) • Verificare se i vertici u e v sono adiacenti richiede tempo O(|V|). Visita in ampiezza BFS • Tecnica di visita di un grafo • È una variazione della visita in ampiezza per alberi binari • Viene mantenuta una coda di vertici da visitare, inizialmente contenete solo la sorgente della visita; • La visita di s procede come segue: 1. Ad ogni iterazione viene prelevato un vertice s dalla coda; 2. Si accodano tutti i vertici adiacenti (scoperti) ad s; 3. Si termina la visita del vertice s e si ritorna al passo 1 per procedere con l’iterazione successiva. • La visita termina quando la coda è vuota. • Bisogna evitare di rivisitare vertici già visitati • Attenzione alla presenza di cicli. Algoritmo BFS • Per distinguere tra i vertici non visitati, quelli sciperti, e quelli visitati coloriamo: • ogni vertice scoperto di grigio • ogni vertice non scoperto di bianco • ogni vertice visitato di nero • Vengono accodati solo i vertici che non sono ancora stati scoperti (cioè bianchi) • I vertici in coda saranno i vertici scoperti e non ancora visitati (cioè grigi) • I vertici già scoperti o visitati non vengono più riconsiderati. Algoritmo BFS BSF(G:grafo, s:vertice) for each vertice u V(G) - {s} do colore[u] = Bianco Inizializzazione pred[u] = Nil colore[s] = Grigio pred[s] = Nil Coda = {s} while Coda do u = Testa[Coda] for each v Adiac(u) do if colore[v] = Bianco then colore[v] = Grigio Accodamento dei soli pred[v] = u nodi non visitati Accoda(Coda,v) Decoda(Coda) colore[u] = Nero Algoritmo BFS: complessità BSF(G:grafo, s:vertice) for each vertice u V(G) - {s} do colore[u] = Bianco O(|V|) pred[u] = Nil colore[s] = Grigio pred[s] = Nil Coda = {s} while Coda do u = Testa[Coda] for each v Adiac(u) do if colore[v] = Bianco then colore[v] = Grigio O(|Eu|) pred[v] = u Eu = lunghezza della Accoda(Coda,v) lista di adiacenza Decoda(Coda) di u colore[u] = Nero Algoritmo BFS: complessità BSF(G:grafo, s:vertice) for each vertice u V(G) - {s} do colore[u] = Bianco O(|V|) pred[u] = Nil colore[s] = Grigio pred[s] = Nil Coda = {s} while Coda do u = Testa[Coda] for each v Adiac(u) do if colore[v] = Bianco O(|E|) then colore[v] = Grigio E = dimensione pred[v] = u delle liste di Accoda(Coda,v) Decoda(Coda) colore[u] = Nero adiacenza. Numero di archi Algoritmo BFS: complessità L’algoritmo di visita in Breadth-First impiega tempo proporzionale alla somma del numero di vertici e del numero di archi (dimensione delle liste di adiacenza). T(V,E) = O(|V|+|E|) Stampa del percorso minimo Percorso-minimo(G:grafo,s,v:vertice) BFS(G,s,pred[]) Stampa-percorso(G,s,v,pred) Stampa-percorso(G:grafo,s,v:vertice,pred[]:array) if v = s then stampa s else if pred[v] = NIL then stampa “non esiste alcun cammino tra s e v” else Stampa-percorso(G,s,pred[v],pred) print v Visita in Profondità (DFS) • Tecnica di visita di un grafo • È una variazione della visita in profondità per alberi binari • La visita di s procede come segue: • Si visitano ricorsivamente tutti i vertici adiacenti ad s; • Si termina la visita del vertice s e si ritorna. • Bisogna evitare di rivisitare vertici già visitati • Bisogna anche qui evitare i cicli • Nuovamente, quando un vertice è stato scoperto e (poi) visitato viene marcato opportunamente (colorandolo) Algoritmo DFS Manterremo traccia del momento (tempo) in cui ogni vertice v viene scoperto e del momento in cui viene visitato (o terminato). Useremo inoltre due array d[v] e f[v] che registrano il momento in cui v verrà scoperto e quello in cui verrà visitato. La variabile globale tempo serve a registrare il passaggio del tempo. Il tempo viene usato per studiare le proprietà di DFS DFS: intuizioni I passi dell’algoritmo DFS si parte da un vertice non visitato s e lo si visita si sceglie un vertice non scoperto adiacente ad s. da s si attraversa quindi un percorso di vertici adiacenti (visitandoli) finché possibile (DFS-Visita): • cioè finché non si incontra un vertice già scoperto/visitsto appena si resta “bloccati” (tutti gli archi da un vertice sono stati scoperti), si torna indietro (backtracking) di un passo (vertice) nel percorso attraversato (aggiornando il vertice s al vertice corrente dopo il passo all’indietro). si ripete il processo ripartendo dal passo. DFS: DFS-Visita DFS-Visita: algoritmo principale della DFS sia dato un vertice u di colore bianco in ingresso visitare il vertice u: colorare u di grigio e assegnare il tempo di inizio visita d[u] visitare in DFS ricorsivamente ogni vertice bianco adiacente ad u con DFS-Visita colorare di nero u e assegnare il tempo di fine visita f[u]. Chiamata ricorsiva a b a c c d e f e b d f b f e c a d f Albero di copertura Depth-first e c b d a Archi dell’albero Archi di ritorno Algoritmo DFS DSF(G:grafo) for each vertice u V do colore[u] = Bianco pred[u] = NIL tempo = 0 for each vertice u V do if colore[u] = Bianco then DFS-Visita(G,u) DSF-Visita(G:grafo,u:vertice) colore[u] = Grigio d[u] = tempo = tempo + 1 for each vertice v Adiac[u] do if colore[v] = Bianco then pred[v] = u DFS-Visit(G,v) colore[u] = Nero f[u] = tempo = tempo + 1 Inizializzazione del grafo e della variabile tempo Abbreviazione per: tempo=tempo+1 d[u]=tempo Abbreviazione per: tempo=tempo+1 f[u]=tempo DFS: simulazone DSF(G:grafo) for each vertice u V do colore[u] = Bianco pred[u] = NIL tempo = 0 for each vertice u V do if colore[u] = Bianco then DFS-Visita(G,u) a c DSF-Visita(G:grafo,u:vertice) colore[u] = Grigio d[u] = tempo = tempo + 1 for each vertice v Adiac[u] do if colore[v] = Bianco then pred[v] = u DFS-Visit(G,v) colore[u] = Nero f[u] = tempo = tempo + 1 b e d f Alberi di copertura multipli DSF(G:grafo) for each vertice u V do colore[u] = Bianco pred[u] = NIL tempo = 0 for each vertice u V do if colore[u] = Bianco then DFS-Visita(G,u) f a e c DSF-Visita(G:grafo,u:vertice) colore[u] = Grigio d[u] = tempo = tempo + 1 for each vertice v Adiac[u] do if colore[v] = Bianco then pred[v] = u DFS-Visit(G,v) colore[u] = Nero f[u] = tempo = tempo + 1 b e g d f b a c d g Tempo di esecuzione di DFS DSF(G:grafo) for each vertice u V do colore[u] = Bianco pred[u] = NIL tempo = 0 for each vertice u V do if colore[u] = Bianco then DFS-Visita(G,u) DSF-Visita(G:grafo,u:vertice) colore[u] = Grigio d[u] = tempo = tempo + 1 for each vertice v Adiac[u] do if colore[v] = Bianco then pred[v] = u DFS-Visit(G,v) colore[u] = Nero f[u] = tempo = tempo + 1 (|V|) (|V|) |V | volte ( |Eu| ) Chiamata solo per vertici non ancora visitati Tempo di esecuzione di DFS DSF(G:grafo) for each vertice u V do colore[u] = Bianco pred[u] = NIL tempo = 0 for each vertice u V do if colore[u] = Bianco then DFS-Visita(G,u) (|V|+ |E | ) BNF - Backus-Naur Form Backus-Naur Form (BNF) : notazione standard utilizzata per la definizioni di grammatiche di linguaggi formali. Specifica le regole di produzione delle frasi del linguaggio. Distingue tre insiemi di simboli: • simboli non terminali (<nome>, <cognome>, …) • simboli terminali (a, marco, …) [oppure ‘a’, ‘marco’, …] • simboli speciali ( |, ::= ) Esempio: <number> ::= <digit> | <digit> <number> <digit> ::= 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 BNF - Esempi Alcuni esempi: <number> ::= <digit> | <digit> <number> <digit> ::= 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 <real_number> ::= <number> '.' <number> <log_expr> ::= <log_expr> <bin_op> <log_expr> | '' <log_expr> | <simple_exp> <bin_op> ::= '' | '' EBNF - Extended Backus-Naur Form Extended Backus-Naur Form (EBNF) : estende la notazione Backus-Naur Form (BNF) con simboli aggiuntivi per la definizioni di parti opzionali o ricorrenti: • parentesi tonde [raggruppamento] • parentesi quadre [componenti opzionali] • parentesi graffe [componenti ripetute] La sintassi dei linguaggi di programmazione (C, C++, Pascal, …) viene tipicamente essere espressa tramite grammatiche EBNF. Facilita la definizione di algoritmi di “parsing” per il linguaggi. EBNF - parentesi tonde Parentesi tonde ( ) : indicano una singola occorrenza, tipicamente si usano per alternative annidate. Esempio: <N>::= (a | b) c produce stringhe della forma ac o bc Equivale alla grammatica <N> ::= <M> c <M> ::= a | b EBNF - parentesi quadre Parentesi quadre [ ] : indicano una componente opzionale, 0 o 1 occorrenze. Esempio: <N> ::= [a] c produce stringhe della forma c oppure ac – equivale alla grammatica: <N> ::= <M> c <M> ::= ε | a Possono contenere alternative annidate [ε indica la stringa vuota] <N> ::= [a | b] c produce stringhe della forma c o ac o bc – equivale alla grammatica: <N> ::= <M> c <M>::= ε | a | b EBNF - parentesi graffe Parentesi graffe { } : indicano una ripetizione di 0 o 1 o più occorrenze. Esempio: <N> ::= {a} c produce stringhe della forma c, ac, aac, …, aaaaac, … – equivale alla grammatica: <N> ::= <M> c <M> ::= ε | a <M> Possono contenere alternative annidate <N> ::= {a | b} c produce stringhe della forma c, ac, bc, abc, bac, … – equivale alla grammatica: <N> ::= <M> c <M>::= ε | a <M> | b <M> Parser Ricorsivo Analizzatore sintattico di tipo top-down, tipicamente di tipo LL(1). Relativamente semplice da realizzare, data una grammatica in forma EBNF opportuna (i.e., senza ambiguità). Segue la struttura ricorsiva della grammatica: • una funzione per ogni simbolo non-terminale • ona o più funzioni per il riconoscimento dei simboli terminali La funzione per un non-terminale <X> richiama le funzioni per i simboli terminali e non-terminali, seguendo la regola grammaticale del non-terminal <X> stesso. Parser Ricorsivo: Esempio Data la regola EBNF funzione per <X> è <X> ::= '(' <T> ',' <T> ') ‘ lo schema della void parse_X(…) { // fn. per il non-terminale <X> match(‘(’); // lettura del terminale ‘)’ parse_T(…); // chiamata alla fn. per <T> match(‘,’); parse_T(…); match(‘)’); } Libreria per gestione grafi Definire in linguaggio C una libreria per la gestione di grafi. • Implementazione di grafi con le due rappresentazioni standard: • Rappresentazione tramite matrice di adiacenza; • Rappresentazione tramite liste di adiacenza. • Prevedere la possibilità di associare pesi (numeri reali) agli archi. Funzioni standard gestione file #include <stdio.h> FILE *stream fopen(const char *name, char *mode); int fclose(FILE *stream); #include <stdio.h> Variabile di tipo puntatore a (descrittore di) file. int main(void) { FILE *fd = fopen("myfile.txt","r"); char y[5] = "abcd"; if (fd) { fprintf(fd,"%s\n", y); /* inserisce nel file la stringa contenuta in y */ fclose(fd); } } Funzioni standard gestione file #include <stdio.h> FILE *stream fopen(const char *name, char *mode); int fclose(FILE *stream); #include <stdio.h> int main(void) { FILE *fd = fopen("myfile.txt","r"); Funzioni di apertura e char y[5] = "abcd"; chiusura di file. if (fd) { fprintf(fd,"%s\n", y); /* inserisce nel file la stringa contenuta in y */ fclose(fd); } } Modalità di apertura file I possibili valori per il mode in fopen("myfile.txt", mode) sono: r Apre il file per la lettura (read-only). w Apre il file per la scrittura (write-only). Il file viene creato se non esiste. r+ Apre il file per lettura e scrittura. Il file deve già esistere. w+ Apre il file per scrittura e lettura. Il file viene creato se non esiste. a Apre il file per aggiornamento (append). È come aprire il file per la scrittura, ma ci si posiziona alla fine del file per aggiungere dati alla fine. Il file viene creato se non esiste. a+ O Apre il file per lettura e aggiornamento. Il file viene creato se non esiste. Funzioni standard di output su file #include <stdio.h> int fputc(char c, FILE *stream); int fprintf(FILE *stream, const char *format, ...); #include <stdio.h> int main(void) { FILE *fd = fopen("myfile.txt","r"); Funzioni di scritture su file. char y[5] = "abcd"; if (fd) { fprintf(fd,"%s\n", y); /* inserisce nel file la stringa contenuta in y */ fclose(fd); } } Funzioni standard di output su file #include <stdio.h> int fputc(char c, FILE *stream); int fprintf(FILE *stream, const char *format, ...); #include <stdio.h> int main(void) { FILE *fd = fopen("myfile.txt","r"); char y[5] = "abcd"; Funzioni di scritture su file. if (fd) { for (int i=0; i<4; i++) fputc(y[i], fd); /* inserisce nel file la stringa contenuta in y */ fclose(fd); } } Funzioni standard di input da file #include <stdio.h> int fgetc(FILE *stream); #include <stdio.h> int main(void) { char x[10]; FILE *fd = fopen("myfile.txt","r"); Funzioni di scritture su file. if (fd) { for(i = 0; i < 10; i++) x[i] = fgetc(); /* Legge 10 caratteri che vengono inseriti nell’array x */ fclose(fd); } } Funzioni standard di spostamento in file #include <stdio.h> int fseek(FILE *stream, long offset, int whence); long ftell(FILE *stream); void rewind(FILE *stream); int fgetpos(FILE *stream, fpos_t *pos); int fsetpos(FILE *stream, fpos_t *pos); • fseek: sposta l’indicatore di posizione nel file alla posizione whence + offset whence può valere SEEK_SET, SEEK_CUR o SEEK_END per specificare il riferimento all'inizio, alla posizione corrente o alla fine file. • • • • ftell: ritorna il valore corrente dell'indicatore di posizione del file stream. rewind: imposta l'indicatore di posizione del file stream all'inizio file. fgetpos: scrive in *pos il valore corrente della posizione del file. fsetpos: riposiziona il file associato a stream nella posizione indicata in *pos. Libreria per gestione grafi Le operazioni da implementare sono: • Lettura/scrittura di un grafo da file; • Conversione tre le due le rappresentazioni di grafo (liste di adiacenza ↔ matrice di adiacenza) • Inserimento di un nuovo vertice; • Inserimento di un nuovo arco; • Cancellazione di un arco o di un vertice; • Calcolo del grafo trasposto; • Visita in ampiezza di un grafo; • Visita in profondità di un grafo; • Stampa di un percorso tra due vertici (minimo o non); • Verifica dell’aciclicità di un grafo. BNF per file di specifica grafi Il file che contiene la descrizione di un grafo deve essere conforme alla seguente grammatica in formato Extended-BNF (EBNF): <grafo> ::= '(' <numero_nodi> ')' <adiacenze> '.' <adiacenze> ::= <adiacenza_nodo> { <adiacenze> } <adiacenza_nodo> ::= <nodo> ['->' <lista_archi> ] ';' <lista_archi> ::= <arco> { ',' <arco> } <arco> ::= ['(' <peso> ')' ] <nodo> <nodo> ::= identificativo <peso> ::= numero_reale_con_segno <numero_nodi> ::= numero_intero_senza_segno I simboli tra virgolette identificano i simboli terminali della grammatica. Ad es. “(” indica il simbolo terminale di aperta parentesi (nel file le virgolette non compaiono). • I simboli tra parentesi angolari (ad es., <nodo>) identificano i simboli non terminali della grammatica. • Le parentesi graffe indicano zero o più ripetizioni del simbolo tra esse contenuto.