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