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.