Teoria dei grafi

annuncio pubblicitario
Capitolo 4: Teoria dei grafi
Teoria dei grafi
Un grafo orientato è una coppia G = (N,A), dove N è un insieme finito di
elementi detti nodi, ed A è un insieme finito di coppie ordinate di nodi, detti
archi. I nodi possono essere rappresentati graficamente con un cerchietto e ogni
arco con una frecce che esce dal nodo i ed entra nel nodo j. Solitamente il
numero dei nodi e quello degli archi vengono indicati rispettivamente con le
lettere n ed m dove 0 ≤ m ≤ n(n-1).
In un grafo orientato G, un cammino è una sequenza di nodi u0 ,u1 ,…,uk tali che
(ui,ui+1) Є A , per i=0,1,2,…,k-1. Il cammino parte dal nodo u0, attraversa tutti i
nodi ,u1 ,…,uk-1 ed arriva al nodo uk ed ha lunghezza k pari al numero di archi.
Un cammino può essere di tre tipi:
1. Cammino semplice: se non ci sono nodi ripetuti
2. Cammino chiuso: se u0=uk
3. Circuito: se è sia chiuso che semplice e l’unica ripetizione è u0=uk .
Esistono anche grafi non orientati dove gli archi non sono orientati e l’arco [i,j]
è uguale all’arco [j,i]. Anche per questo tipo di grafo esistono dei cammini detti
catene:
1. Catena semplice: se non ci sono nodi ripetuti
2. Catena chiusa: se u0=uk
3. Circuito: è una catena sia semplice che chiusa dove tutti gli archi sono
distinti.
Grafo orientato
29
Grafo non orientato
Capitolo 4: Teoria dei grafi
Specifica sintattica
Per il tipo grafo esistono degli operatori che consentono di sfruttare al meglio
questa struttura. Essi sono i seguenti:
• CreaGrafo:
( ) → grafo
• GrafoVuoto:
(grafo) → booleano
• InsNodo:
(nodo,grafo) → grafo
• InsArco:
(nodo,nodo,grafo) → grafo
• CancNodo:
(nodo,grafo) → grafo
• CancArco:
(nodo,nodo,grafo) → grafo
• Adiacenti:
(nodo,grafo) → insieme
Specifica semantica
CreaGrafo = G
Post: G=(N,A), con N=Ø ed A= Ø
GrafoVuoto(G) = b
Post: b=vero, se N= Ø e A= Ø, b=falso, altrimenti
InsNodo(u,G) = G’
Pre: G=(N,A), u ∉ N
Post: G’ = (N,A), N’=N U {U }
InsArco(u,v,G) = G’
Pre: G = (N,A), u Є N, v Є N, e (u,v) ∉ A
Post: G’ =(N,A’), A’=A U {(u,v)}
CancNodo(u,G)=G’
Pre: G=(N,A), u Є N, non esiste alcun nodo v tale che (u,v) Є A o (v,u) ЄA
Post: G’=(N’,A), N’=N-{u}
30
Capitolo 4: Teoria dei grafi
CancArco(u,v,G) =G’
Pre: G=(N,A), u Є N, v Є N, e (u,v) Є A
Post: G’=(N,A’), A’=A-{(u,v)}
Adiacenti(u,G) = A(u)
Pre: G=(N,A), u Є N
Post: A(u)={v: (u,v) Є A}, detto sottoinsieme di adiacenza di u
Realizzazione con matrice di adiacenza
Gestire un grafo semplicemente con dei puntatori risulterebbe alquanto
complicato perciò per semplificare il suo utilizzo è stato studiato un metodo di
rappresentazione detto matrice di adiacenza (è anche possibile una
rappresentazione con matrice di incidenza).
Se il numero di nodi del grafo è pari ad n allora sarà creata una matrice E di
dimensione n x n ch sarà riempita con il seguente metodo.
Consideriamo i nodi i e j :
- Se esiste un arco da i a j allora E[i,j]=1;
- Se esiste un arco da i a j ed esso è pesato, allora E[i,j]=P(i);
- Se non esiste un arco da i a j allora E[i,j]=0;
In particolare se il grafo non è orientato si otterrà una matrice simmetrica.
1 2 3 4 5 6
1 0 1 0 0 1 0
2 1 0 1 0 1 0
3 0 1 0 1 0 0
4 0 0 1 0 1 1
5 1 1 0 1 0 0
6 0 0 0 1 0 0
31
Capitolo 4: Teoria dei grafi
Visite di un grafo
La visita di un grafo consiste nel visitare almeno una volta tutti i suoi nodi senza
mai passare due volte dallo stesso nodo. I due metodi di visita più utilizzati sono
essenzialmente due:
1. Depth-First-Search (DFS)
2. Breadth-First-Search (BFS)
La prima è simile alla visita in ordine anticipato di un albero e viene definite
anche visita in profondità in quanto visitato un nodo u si cerca di visitare il
maggior numero di nodi allontanandosi il più possibile dal nodo di partenza.
La seconda invece, è detta anche visita in ampiezza in quanto visitato un nodo
u si visitano tutti i suoi nodi adiacenti e poi si passa a considerare un nuovo nodo
v. Forniamo le pseudo codifiche di entrambe.
Procedure DFS( var G:grafo; u:nodo );
var v:nodo;
begin
{esamina il nodo u e marcalo “visitato”}
for each v Є A(u) do begin
{esamina l’arco (u,v)};
if v non è marcato “visitato” then DFS(G,v);
end;
end;
Procedure BFS( var G:grafo; u:nodo );
var v:nodo,Q:coda;
begin
CreaCoda(Q); InCoda(u,Q);
while not CodaVuota(Q) do begin
u:=LeggiCoda(Q); FuoriCoda(Q);
{ esamina il nodo u e marcalo “visitato”};
for each v Є A(u) do begin
{esamina l’arco (u,v)};
if v non è marcato “visitato” and v ∉ Q then
InCoda(v,Q);
end;
end;
end;
32
Capitolo 4: Teoria dei grafi
Cammini minimi in un grafo: l’algoritmo di Bellman-Ford-Moore
Un algoritmo interessante legato all’uso dei grafi è quello della ricerca dei
cammini minimi che spieghiamo qui di seguito.
Esempio
Supponiamo che un ente postale della città di Bari debba inviare alcuni pacchi in
diverse città d’Italia e deve fare in modo di minimizzare il tempo di spedizione.
Per assurdo supponiamo che i mezzi di trasporto utilizzati viaggino a velocità
costante ed in condizioni ottimali di traffico, quindi il tempo per il trasporto
dipende unicamente dalla distanza da coprire.
Per la soluzione di questo tipo di problema è facile pensare ad un grafo G
orientato in cui è possibile identificare le N città con un numero N di nodi e i
percorsi autostradali con gli archi di collegamento tra esse. E’ chiaro inoltre che
il peso di ciascun arco sarà pari alla lunghezza in km di ciascun tratto
autostradale.
Per il grafo valgono le seguenti proprietà:
• è orientato
• gli archi sono pesati
• esiste sempre un percorso tra il nodo di partenza R e ciascuna delle N-1 città
che si vuole raggiungere.
Per chiarire meglio la struttura rappresentiamo graficamente il grafo.
33
Capitolo 4: Teoria dei grafi
Il passo successivo sta quindi nel creare un albero di copertura radicato in R dal
quale siamo in grado di tirar fuori il cammino minimo da R a ciascun nodo.
La struttura ottenuta sarà la seguente:
Per cercare il cammino minimo utilizziamo la visita in ampiezza (BFS) del grafo
consistente nel visitare una sola volta tutti i nodi adiacenti a ciascun nodo.
Questa soluzione è ottimale visto che garantisce di coprire i percorsi più brevi.
Un punto cruciale nella ricerca dei cammini minimi sta nel teorema di Bellman
che enunciamo di seguito:
“Una soluzione ammissibile T è ottima se e solo se valgono le seguenti
condizioni:
di + cij = dj per ogni arco (i,j) ∈ T, e di + cij ≥ dj per ogni arco (i,j) ∈ A
Quindi la soluzione T può essere considerata ottima se e solo se la distanza
trovata tra due nodi è minore o al massimo uguale della distanza
precedentemente trovata. Grazie a questa regola è possibile implementare un
algoritmo che sostituisca tutti quegli archi che violano la suddetta condizione.
Forniamo quindi la pseudo codifica dell’algoritmo generale per la ricerca dei
cammini minimi.
procedure CAMMINIMINIMI(var G:grafo; R:nodo);
begin
{ inizializza T ad un albero di copertura di radice R contenente un
cammino da R ad ogni altro nodo u di G }
34
Capitolo 4: Teoria dei grafi
while esiste un arco ( i, j ) tale che di+ci j< dj do begin
dj := di + cij ;
{ sostituisci in T l’attuale arco entrante in j con l’arco ( i, j )}
end
end;
Come si è già detto il passo fondamentale di questo algoritmo sta nel selezionare
gli archi che non rispettano la condizione di Bellman. Per fare questo prendiamo
in considerazione un insieme di soluzioni S in cui inizialmente avremo solo la
radice R. Possiamo quindi verificare la condizione di Bellman su ogni arco ( i, j)
tale che j appartiene all’insieme di adiacenza A(i) del nodo i. Inoltre si genera
l’albero di copertura (rappresentato con un vettore dei padri) del grafo radicato
in R. Per fare questo inizializziamo un albero fittizio in cui consideriamo tutti i
nodi come figli della radice R e man mano che la condizione di Bellman viene
verificata scopriamo qual è il padre di ciascun nodo e quindi l’albero di
copertura prenderà forma.
Da quanto detto è intuitivo ricavare il seguente algoritmo:
procedure CAMMINIMINIMI ( var G:grafo; R:nodo);
var T,dist:vettore; S:insieme; k:integer; i,j:nodo;
begin
CREAINSIEME(S);
T[R] :=0; dist[R] := 0;
For k:=1 to n do
If k ≠ R then begin
T[k] := R; dist[k] := maxint
end;
INSERISCI(R,S);
While not INSIEMEVUOTO(S) do begin
i := LEGGI(S); CANCELLA (i,S);
for each j A(i) do
if dist[i]+cij < d[j] then begin
T[j] := i;
dist[j] := dist[i] + cij;
if not APPARTIENE(j, S) then
INSERISCI(j,S);
end;
end;
35
Capitolo 4: Teoria dei grafi
end;
Come si evince dall’algoritmo mostrato le distanze dal nodo R a ciascun nodo
del grafo sono riportate in un vettore inizializzato con valore maxint per dar
modo alla condizione di Bellman di verificarsi almeno una volta.
Dell’algoritmo generale presentato vi sono diverse varianti che differiscono nel
modo in cui viene rappresentato l’insieme S. Utilizzando una coda si ottiene
l’algoritmo di Bellman-Ford-Moore che risolve il problema anche con pesi
negativi. Di conseguenza le procedure utilizzate nell’algoritmo generale
vengono sostituite con operatori di gestione della coda. Otterremo quindi la
seguente pseudo codifica.
procedure CAMMINIMINIMI (var G:grafo; R:nodo);
var T,dist:vettore; S:insieme; k:integer; I,j:nodo;
begin
CREACODA(S);
T[R] :=0; dist[R] := 0;
For k:=1 to n do
If k ≠ R then begin
T[k] := R; dist[k] := maxint
end;
INCODA(R,S);
While not CODAVUOTA(S) do begin
i := LEGGICODA(S); FUORICODA (i,S);
for each j A(i) do
if dist[i]+cij < d[j] then begin
T[j] := i;
dist[j] := dist[i] + cij;
if not APPARTIENE(j, S) then
INCODA(j,S);
end;
end;
end;
Mostriamo ora graficamente l’utilizzo della coda e dei vettori delle distanze e
dei padri riferendoci al grafo illustrato inizialmente e supponendo di voler
cercare il cammino minimo dal nodo R (indice 1) al nodo numero 2.
36
Capitolo 4: Teoria dei grafi
37
Capitolo 4: Teoria dei grafi
L’algoritmo di Bellman-Ford-Moore è stato applicato per risolvere il seguente
problema delle finali italiane delle olimpiadi di informatica.
Distanze
Antonio e’ in viaggio, e ha una tabella di distanze tra citta’. Su di essa,
purtroppo, non compare la distanza tra la sua citta’ di partenza e quella di arrivo,
per raggiungere la quale bisogna passare per altre citta’ intermedie. Dovete
aiutare Antonio a trovare la distanza relativa al percorso piu’ breve tra la citta’ di
partenza e quella di arrivo desumibile dalla tabella.
File di input
Il file input.txt e’ costituito da una prima riga nella quale sono indicati, separati
da un carattere spazio, il numero totale di citta’ in tabella N e il numero di
distanze mutue tra due citta’ D. Seguono D righe, ciascuna delle quali contiene
gli identificatori numerici delle due citta’ e la loro mutua distanza, separati da un
carattere spazio.
File di output
Il programma, dopo aver letto il file di input, deve calcolare la distanza minima
tra la citta’ di partenza e quella di arrivo, e scriverla su un file di nome
output.txt. Più precisamente, il file output.txt deve essere costituito da una sola
riga, contenente un numero intero che rappresenta la distanza piu’ breve tra la
citta’ di partenza e quella di arrivo.
Assunzioni
1. Il file di input non contiene altri caratteri oltre a quelli precisati.
2. Il numero N di citta’ e' in ogni caso inferiore a 1000, ed il numero di distanze
D inferiore a 10000.
3. Gli identificatori delle citta’ sono interi compresi tra 1 e N. La citta’ di
partenza e la destinazione sono sempre associate agli identificatori 1 e 2,
rispettivamente.
38
Capitolo 4: Teoria dei grafi
program cammini_minimi;
const
N_Nodi=1000;
type
{ Il grafo e' realizzato con una matrice di adiacenza }
grafo=array[1..N_nodi,1..N_Nodi] of integer;
{ L'albero e' realizzato con un vettore di padri }
albero=array[1..N_nodi] of integer;
{ tiponodo e' il tipo di ogni nodo di una struttura dati concatenata.
In questo programma realizziamo code e liste con puntatori.
}
puntnodo=^tiponodo;
tiponodo=record
info:integer;
link:puntnodo;
end;
{ Oltre ai puntatori ad inizio e fine coda, utilizziamo un vettore booleano
per velocizzare la funzione cercacoda():
se presente[i]=true, il nodo i del grafo è presente nella coda
}
coda=record
inizio:puntnodo;
fine:puntnodo;
presente:array[1..N_Nodi] of boolean;
end;
lista=puntnodo;
var
{ G e' il grafo
inp e oup sono i file di input e output
nn e' il numero dei nodi del grafo
}
g:grafo;
inp,oup:text;
nn:integer;
39
Capitolo 4: Teoria dei grafi
procedure acqgrafo(var inp:text; var g:grafo; var nn:integer);
{ Acquisisce il grafo dal file di input:
- il numero dei nodi,
- il numero degli archi
- gli archi pesati del grafo
Il file di input contiene:
•
prima linea: due interi, NN numero nodi e NA numero archi
•
seguono NA linee con tre interi, U1, U2 e PESO:
o
U1 nodo da cui parte l'arco
o
U2 nodo sul quale incide l'arco
o
PESO peso dell'arco
}
var
na,r,c,u1,u2,peso:integer;
begin
reset(inp);
readln(inp,nn,na);
for r:=1 to nn do begin
for c:=1 to nn do begin
g[r,c]:=maxint;
end;
end;
for r:=1 to na do begin
readln(inp,u1,u2,peso);
g[u1,u2]:=peso;
end;
close(inp);
end;
procedure creacoda(var c:coda);
{ Crea la coda "c" vuota }
var
i:integer;
begin
c.inizio:=nil;
c.fine:=nil;
for i:= 1 to N_Nodi do
c.presente[i]:=false;
end;
40
Capitolo 4: Teoria dei grafi
function codavuota(var c:coda):boolean;
{ Restituisce true se la coda "c" e' vuota }
var
b:boolean;
begin
if (c.inizio=nil) then begin
b:=true;
end
else begin
b:=false;
end;
codavuota:=b;
end;
procedure incoda(var c:coda; j:integer);
{ Inserisce il nodo "j" nella coda "c" }
var
temp,elemento:puntnodo;
begin
new(elemento);
elemento^.info:=j;
elemento^.link:=nil;
temp:=c.fine;
if c.inizio=nil then begin
c.inizio:=elemento;
end
else begin
{ questa operazione va fatta solo
se non si tratta del primo inserimento nella coda!
}
temp^.link:=elemento;
end;
c.fine:=elemento;
c.presente[j]:=true;
end;
41
Capitolo 4: Teoria dei grafi
function estraicoda(var c:coda) :integer;
{ Restituisce il nodo estratto dalla coda "c" }
var
v:puntnodo;
temp:integer;
begin
v:=c.inizio;
c.inizio:=v^.link;
temp:=v^.info;
dispose(v);
c.presente[temp]:=false;
estraicoda:=temp;
end;
function cercacoda(c:coda; j:integer):boolean;
{ Restituisce true se il nodo "j" e' presente nella coda "c" }
begin
cercacoda:=c.presente[j];
end;
function listavuota(l:lista):boolean;
{ Restituisce true se la lista l e' vuota }
var
fine:boolean;
begin
if l=nil then begin
fine:=true;
end
else begin
fine:=false;
end;
listavuota:=fine;
end;
procedure inlista(inf:integer;var l:lista);
{ Inserisce il nodo "inf" in testa alla lista "l" }
var
temp:puntnodo;
begin
new(temp);
temp^.link:=l;
temp^.info:=inf;
l:=temp;
end;
42
Capitolo 4: Teoria dei grafi
function adiacenti(var g:grafo; nodo:integer; nn:integer):lista;
{ Restituisce la lista dei nodi adiacenti di "nodo" }
var
c:integer;
l,elemento:lista;
begin
l:=nil;
for c:=1 to nn do begin
if g[nodo,c]<>Maxint then begin
inlista(c, l);
end;
end;
adiacenti:=l;
end;
function estrailista(var l:lista):integer;
{ Restituisce il nodo estratto dalla testa della lista "l" }
var
k:integer;
temp:lista;
begin
k:=l^.info;
temp:=l;
l:=l^.link;
dispose(temp);
estrailista:=k;
end;
procedure percorso(var t:albero; var l:lista; start, finish: integer);
{ Crea la lista "l" dei nodi del percorso minimo tra il nodo "start" e il nodo "finish"
Si parte dalla foglia "finish" e si risale il vettore dei padri fino a raggiungere la
radice "start"
}
var
i:integer;
begin
i:=finish;
while i<>start do begin
inlista(i,l);
i:=t[i];
end;
inlista(start,l);
end;
43
Capitolo 4: Teoria dei grafi
procedure camminiminimi(var g:grafo; start, finish, nn:integer; var
oup:text);
{ Algoritmo di Bellmann-Ford-Moore }
var
l:lista;
t,dist:albero;
c:coda;
v:puntnodo;
k,j,min:integer;
begin
{ inizializzo il vettore dei padri e quello delle distanze }
t[start]:=0; dist[start]:=0;
creacoda(c);
for k:=1 to nn do begin
if k<>start then begin
t[k]:=start;
dist[k]:=maxint;
end;
end;
{ Inseriamo il nodo di partenza in coda }
incoda(c,start);
while (not codavuota(c)) do begin
k:=estraicoda(c);
l:=adiacenti(g,k,nn);
while not listavuota(l) do begin
j:=estrailista(l);
if dist[j]>dist[k]+g[k,j] then begin
dist[j]:=dist[k]+g[k,j];
t[j]:=k;
{ il nodo j va messo in coda solo se fa parte di un
cammino minimo, quindi solo se dist[j] cambia!
}
if not cercacoda(c,j) then begin
incoda(c,j);
end;
end;
end;
end;
rewrite(oup);
{ Scrive sul file di output la distanza del cammino minimo }
writeln(oup,dist[finish]);
writeln(oup);
percorso(t, l, start, finish);
44
Capitolo 4: Teoria dei grafi
{ Scrive sul file di output i nodi attraversati dal cammino minimo
while not listavuota(l) do begin
write(oup,estrailista(l):4);
end;
close(oup);
end;
{ ***** M A I N ***** }
begin
assign(inp,'input.txt');
assign(oup,'output.txt');
acqgrafo(inp,g,nn);
camminiminimi(g,1,2,nn,oup);
end.
45
Capitolo 4: Teoria dei grafi
Cammini minimi: accenni all’algoritmo di Dijkstra.
Lo stesso algoritmo della ricerca dei cammini minimi in grafo ha diverse
varianti tra cui ricordiamo l’algoritmo di Dijkstra. Esso differisce da quello di
Bellman-Ford-Moore in quanto al posto della coda si utilizza un heap (cap. 3)
ovvero una coda con priorità.
La profonda differenza tra i due metodi di ricerca sta nel fatto che l’algoritmo di
BFM funziona anche con grafi pesati con pesi negativi, mentre quello di
Dijkstra è valido solo per grafi con archi pesati aventi pesi positivi. Sebbene
questo algoritmo può sembrare meno completo, in realtà se vi sono le condizioni
per utilizzarlo risulta molto più veloce ed efficiente dell’altro metodo.
A conferma di questo nella gestione della rete Internet i migliori percorsi di
routing per l’instradamento dei pacchetti vengono scelti proprio utilizzando
questo algoritmo.
Sort topologico di un grafo aciclico
Un DAG (Directed Acyclic Graph) è un grafo G = (N, A) orientato privo di
cicli. I DAG rappresentano tipicamente insiemi di azioni tra cui sono definite
delle propedeuticità, cioè alcune azioni devono essere eseguite prima di altre:
come per esempio avviene in una catena di produzione, o in un gruppo di
processi software, o in un insieme di esami universitari.
I nodi del grafo rappresentano le azioni e gli archi orientati (u, v) rappresentano
la propedeuticità di u su v, cioè non si può eseguire v se prima non si è eseguita
u (si può anche dire che v dipende da u). Questa dipendenza è ovviamente
transitiva, cioè se v dipende da u e z dipende da v allora z dipende da u anche se
non esiste l’arco (u, z). Un ordinamento topologico di G è una sequenza ordinata
S dei nodi che rispetti tutte le dipendenze tra azioni: se v dipende da u deve
46
Capitolo 4: Teoria dei grafi
apparire in S dopo u (ma non vale strettamente il contrario: se v appare dopo u
non è detto che dipenda da u).
Se G è una semplice catena di nodi in cui ciascuno è connesso con un arco al
successivo, esiste un unico banale ordinamento topologico per G; altrimenti gli
ordinamenti sono più di uno ed esistono anche se il grafo è costituito da diverse
componenti non connesse tra loro.
Nell’esempio che segue, è descritto:
• un DAG G con due componenti
• tre sorgenti – nodi senza archi entranti – (1, 6, 8)
• tre pozzi – nodi senza archi uscenti – (3, 7, 9)
• un ordinamento topologico S di G
Si noti che in S, per esempio, 5 è preceduto da 4 e 4 è preceduto da 1; 3 è
preceduto da 2 e 5; 9 è preceduto da 6 e 8.
In definitiva, si definisce ordinamento topologico di un DAG non
necessariamente connesso, un ordinamento dei nodi tale che se (u, v) è un
arco, allora u precede v nell’ordinamento.
La strategia solutiva si basa su una visita in profondità DFS ricorsiva nella quale
si calcoli l’istante FINE[u] in cui termina la visita di ciascun nodo u e
ordinando, quindi, i nodi per istanti di fine decrescenti. Poiché il grafo è
aciclico, se (u, v) è un arco, sicuramente FINE[v] < FINE[u].
E’ possibile anche evitare di calcolare gli istanti di fine di ciascun nodo,
memorizzando direttamente il nodo u nel vettore S, che contiene la soluzione
ordinata, non appena si è completata la chiamata ricorsiva su u: per questo
47
Capitolo 4: Teoria dei grafi
motivo il vettore deve essere riempito a ritroso, quindi il nodo la cui visita DFS
termina per
i-esima è memorizzato in S[n – i + 1].
var
S: array[1..n] of integer;
VISITATO: array[1..n] of boolean;
procedure topologicSort (var G: grafo; n:integer);
var
i, u: integer;
begin
for u := 1 to n do
VISITATO[u] := false;
i := 1;
for u := 1 to n do
if not VISITATO[u] then
DFS(G, u);
end;
procedure DFS (var G: grafo; u: integer);
var
v: integer;
begin
VISITATO[u] := true;
{ A(u) è l’insieme dei nodi adiacenti di u }
for each v Є A(u) do
if not VISITATO[v] then
DFS(G, v);
S[n – i + 1] := u;
i := i + 1;
end;
48
Capitolo 4: Teoria dei grafi
Bibliografia
- Alan Bertossi, Algoritmi e strutture di dati
Casa editrice UTET Libreria 2004
- Prof. Fabrizio Luccio, Dipartimento di Informatica, Università di Pisa
Dispensa sui Grafi, Corso di Algoritmica
http://www.di.unipi.it/~luccio/19%20grafi%20nuovo.pdf
49
Scarica