Università degli Studi di Roma “Tor Vergata” Dispense per il corso di GRAFI E RETI DI FLUSSO Antonio Iovanella Ottobre 2006 I Indirizzo dell’autore: Antonio Iovanella Dipartimento di Ingegneria dell’Impresa, Università di Roma “Tor Vergata” Via del Politecnico 1, 00133 Roma - Italia Email: [email protected] Web Page: http://www.disp.uniroma2.it/Users/iovanella/ Home page della didattica: http://www.uniroma2.it/didattica/grfbis Indice 1 Introduzione 1.1 1.2 1.3 1 Definizioni . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3 1.1.1 Sottografi, ordine, dimensione e densità di un grafo . . . . . . . . . 4 1.1.2 Vicinati, gradi e regolarità . . . . . . . . . . . . . . . . . . . . . . . 7 1.1.3 k-Fattorizzazioni . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8 Grafi e modelli . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9 1.2.1 Relazioni e sottografi . . . . . . . . . . . . . . . . . . . . . . . . . . 9 1.2.2 Assegnamento di lavori . . . . . . . . . . . . . . . . . . . . . . . . . 10 1.2.3 Scheduling di team in una azienda . . . . . . . . . . . . . . . . . . 11 1.2.4 Percorsi su rete . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13 Esercizi . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15 2 Algoritmi 17 2.1 Breve tassonomia dei problemi decisionali . . . . . . . . . . . . . . . . . . 17 2.2 Algoritmi . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18 2.2.1 Algoritmi di ricerca . . . . . . . . . . . . . . . . . . . . . . . . . . . 20 Crescita di funzioni . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22 2.3.1 Notazione Big-O . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23 2.3.2 Crescita di combinazioni di funzioni . . . . . . . . . . . . . . . . . . 24 2.4 Complessità degli algoritmi . . . . . . . . . . . . . . . . . . . . . . . . . . . 26 2.5 Esercizi . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 30 2.3 II INDICE III 3 Proprietà e strutture dei grafi 32 3.1 Strutture dati per la rappresentazione di grafi . . . . . . . . . . . . . . . . 32 3.2 Isomorfismo tra grafi . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 36 3.3 Connessione, cicli e grafi bipartiti . . . . . . . . . . . . . . . . . . . . . . . 39 3.4 Alberi . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 44 3.5 Cammini Euleriani . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 45 3.6 Cammini Hamiltoniani . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 47 3.7 Esercizi . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 47 4 Grafi Planari 50 4.1 Grafi sul piano . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 51 4.2 Grafi duali . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 53 4.2.1 Proprietà dei grafi planari e Formula di Eulero . . . . . . . . . . . . 54 4.2.2 Caratterizzazione dei grafi planari . . . . . . . . . . . . . . . . . . . 58 Esercizi . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 58 4.3 5 Algoritmi di ricerca su grafo 60 5.1 Algoritmi di ricerca su grafo . . . . . . . . . . . . . . . . . . . . . . . . . . 60 5.2 Algoritmo di ordinamento topologico . . . . . . . . . . . . . . . . . . . . . 66 5.3 Algoritmi di ricerca di alberi ricoprenti minimi . . . . . . . . . . . . . . . . 69 5.3.1 Algoritmo di Kruskal . . . . . . . . . . . . . . . . . . . . . . . . . . 73 5.3.2 Algoritmo di Prim . . . . . . . . . . . . . . . . . . . . . . . . . . . 75 Esercizi . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 78 5.4 6 Problemi di flusso su grafo 6.1 6.2 6.3 79 Algoritmi di ricerca del cammino minimo su di un grafo . . . . . . . . . . . 80 6.1.1 L’algoritmo di Dijkstra . . . . . . . . . . . . . . . . . . . . . . . . . 83 Problemi di massimo flusso su grafo . . . . . . . . . . . . . . . . . . . . . . 88 6.2.1 93 L’algoritmo di Ford e Fulkerson . . . . . . . . . . . . . . . . . . . . Esercizi . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 100 Elenco delle figure 1.1 L’attuale mappa dell’area dei ponti del problema di Könisberg [1]. . . . . . 2 1.2 Grafo corrispondente al Problema di Könisberg. . . . . . . . . . . . . . . . 2 1.3 Grafo dell’Esempio 1.1.2 . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5 1.4 Un sottografo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5 1.5 Un sottografo indotto . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5 1.6 Un sottografo ricoprente . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5 1.7 Esempi di grafi 3-regolari . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8 1.8 Le fattorizzazioni di K4 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9 1.9 Grafo rappresentante le relazioni di conoscenza tra i cinque membri del gruppo 10 1.10 Grafo rappresentante le relazioni di non conoscenza tra i cinque membri del gruppo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10 1.11 Grafo bipartito . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11 1.12 Una istanza del problema dei team . . . . . . . . . . . . . . . . . . . . . . 12 1.13 Un cammino . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14 1.14 Un ciclo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14 1.15 Alcuni esempi di path e cicli . . . . . . . . . . . . . . . . . . . . . . . . . . 14 1.16 Grafo per l’Esempio 1.2.1 . . . . . . . . . . . . . . . . . . . . . . . . . . . 15 2.1 Grafico delle funzioni più comunemente usate nella stima Big-O . . . . . . 26 3.1 Grafo non orientato . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33 3.2 Grafo orientato . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33 3.3 Lista di adiacenza per il grafo di Figura 3.2. . . . . . . . . . . . . . . . . . 36 IV V ELENCO DELLE FIGURE 3.4 Esempio di due grafi isomorfi . . . . . . . . . . . . . . . . . . . . . . . . . 37 3.5 Due grafi isomorfi . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 38 3.6 I grafi bipartiti completi K2,3 e K3,3 . . . . . . . . . . . . . . . . . . . . . . 39 3.7 Un grafo con quattro componenti connesse. . . . . . . . . . . . . . . . . . . 40 3.8 Componente connessa di x . . . . . . . . . . . . . . . . . . . . . . . . . . . 41 3.9 Un circuito con un nodo ripetuto. . . . . . . . . . . . . . . . . . . . . . . . 41 3.10 Una foresta . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 44 3.11 Un albero . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 44 3.12 Come spostare i quattro cavalli. . . . . . . . . . . . . . . . . . . . . . . . . 47 4.1 Il problema dei tre nemici. . . . . . . . . . . . . . . . . . . . . . . . . . . . 50 4.2 Connessioni per il problema dei tre nemici. . . . . . . . . . . . . . . . . . . 51 4.3 K5 e K3,3 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 52 4.4 La facce di un grafo planare. . . . . . . . . . . . . . . . . . . . . . . . . . . 53 4.5 Costruzione del duale. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 54 4.6 Un esempio di grafo G e del suo duale G∗ . . . . . . . . . . . . . . . . . . . 54 4.7 Esempio di due embedding planari con duali non isomorfi. . . . . . . . . . 55 4.8 Il ciclo C evidenziato per la dimostrazione del teorema. . . . . . . . . . . . 57 4.9 Contrazione dell’arco e. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 57 5.1 L’algoritmo di ricerca su grafo. . . . . . . . . . . . . . . . . . . . . . . . . 62 5.2 Grafo per gli Esempi 5.1.1 e 5.1.2. . . . . . . . . . . . . . . . . . . . . . . 63 5.3 Albero dei cammini e valori dei vettori pred(i) e ordine(i) nel caso FIFO. . 64 5.4 Albero dei cammini e valori dei vettori pred(i) e ordine(i) nel caso LIFO. . 66 5.5 L’algoritmo di ordinamento topologico. . . . . . . . . . . . . . . . . . . . . 67 5.6 Grafo per l’Esempio 5.2.1 . . . . . . . . . . . . . . . . . . . . . . . . . . . 68 5.7 Contenuto del vettore ordine(i) dopo l’ordinamento topologico. . . . . . . 69 5.8 Tagli e path per le condizioni di ottimalità. . . . . . . . . . . . . . . . . . . 70 5.9 Grafo per l’Esempio 5.3.1. . . . . . . . . . . . . . . . . . . . . . . . . . . . 74 5.10 Evoluzione dell’algoritmo di Kruskal sul grafo di Figura 5.9. . . . . . . . . 75 VI ELENCO DELLE FIGURE 5.11 Evoluzione dell’algoritmo di Prim sul grafo di Figura 5.9. . . . . . . . . . . 77 6.1 Cammini da s a k. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 82 6.2 L’algoritmo di Dijkstra . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 84 6.3 Grafo per l’Esempio 6.1.1 e corrispondente tabella degli archi. . . . . . . . 85 6.4 Evoluzione delle etichette dei nodi e lista di adiacenza per il grafo di Figura 6.3. 87 6.5 Disegno per la correttezza di Dijkstra . . . . . . . . . . . . . . . . . . . . . 88 6.6 Costruzione del grafo residuo. . . . . . . . . . . . . . . . . . . . . . . . . . 90 6.7 Un grafo G con un flusso assegnato x ed il corrispondente grafo residuo G(x). 91 6.8 Un taglio s-t per il grafo di Figura 6.7. . . . . . . . . . . . . . . . . . . . . 91 6.9 L’algoritmo di Ford e Fulkerson . . . . . . . . . . . . . . . . . . . . . . . . 95 6.10 Grafo per l’Esempio 6.2.1. . . . . . . . . . . . . . . . . . . . . . . . . . . . 96 6.11 Il grafo residuo dopo la prima iterazione. . . . . . . . . . . . . . . . . . . . 96 6.12 Il grafo residuo dopo la seconda iterazione. . . . . . . . . . . . . . . . . . . 97 6.13 Il grafo residuo dopo la terza iterazione. . . . . . . . . . . . . . . . . . . . 97 6.14 Il grafo residuo dopo la quarta iterazione. . . . . . . . . . . . . . . . . . . . 98 6.15 Il taglio minimo del grafo di Figura 6.10. . . . . . . . . . . . . . . . . . . . 98 6.16 Istanza patologica per l’algoritmo di Ford e Fulkerson. 99 . . . . . . . . . . . PREFAZIONE VII Prefazione Queste dispense nascono per essere affiancate ai testi adottati per i corsi di Grafi e Reti di Flusso, previsti per gli studenti di Ingegneria Gestionale e per quelli di Ingegneria Online della Facoltà di Ingegneria dell’Università di Roma Tor Vergata. Esse costituiscono inoltre un utile strumento di approfondimento per gli studenti del corso di Teoria dei Grafi e Reti di Flusso del Master di II livello in Ingegneria dei Sistemi a Rete della Facoltà di Ingegneria dell’Università di Roma Tor Vergata. Essendo questa la primissima versione delle dispense, conterrà sicuramente degli errori. Inoltre i miei sforzi non saranno stati di certo sufficienti a creare uno strumento chiaro e completo. Per questo confido nella collaborazione dei lettori nel segnalarmi ogni imprecisione che rileveranno e le parti poco chiare o incomplete. In queste pagine sono presenti tutti gli argomenti che saranno trattati nei moduli didattici. I testi originali dai quali ho tratto ispirazione per la redazione di queste note sono raccolti nella bibliografia, e pertanto invito tutti gli studenti a tenerli in considerazione come riferimento ufficiale in caso di dubbi o imprecisioni. Gli argomenti trattati in queste dispense seguono fedelmente quelli svolti in aula. In dettaglio, nel Capitolo 1 verranno introdotti i concetti e le definizioni di base della teoria dei grafi che possono essere approfonditi su [9], [2], [4], [7] e [14]; nel Capitolo 2 verrà introdotto il concetto di algoritmo e vedremo i metodi di misura della loro efficienza, argomenti che possono essere studiati su [8], [11] e [12]. I testi [10], [2], [7] e [14] possono essere usati come riferimento per il Capitolo 3 nel quale verranno illustrate alcune importanti caratteristiche dei grafi come l’isomorfismo e l’eulerianità, mentre [14], [7], [11] sono stati usati per il Capitolo 4 dove tratteremo i grafi planari. I testi [10], [7] e [3] sono invece alla base dei Capitoli 5 e 6 dove verranno illustrati rispettivamente gli algoritmi di ricerca su grafo e gli algoritmi di flusso. Ottobre 2006 Capitolo 1 Introduzione Ognuno di noi su questo pianeta è separato dagli altri solo da sei persone. Sei gradi di separazione fra noi e tutti gli altri su questo pianeta: il presidente degli Stati Uniti, un gondoliere veneziano... Non solo nomi importanti, chiunque: un indigeno australiano, uno della Terra del Fuoco, un eschimese. Io sono legato a chiunque sul pianeta da una catena di sei persone. E’ un pensiero profondo... ognuno di noi è una porta spalancata su altri mondi. John Guare Per definire il concetto di grafo si fa spesso ricorso ad un episodio che viene associato alla nascita stessa della Teoria dei Grafi, ovvero alla formulazione del famoso Problema dei Ponti di Könisberg da parte di Eulero in un suo lavoro del 1736. Nella città di Könisberg (all’epoca appartenente alla Prussia, attualmente situata in Russia con il nome di Kaliningrad) ci sono, oggi come allora, delle aree abitative insediate su alcune isole del fiume Pregel ed altre che sorgono lungo le sue rive, per un totale di quattro aree. Queste zone sono unite tra di loro da sette ponti, cosı̀ come si può vedere nell’attuale mappa della zona, riportata in Figura 1.1. Il problema posto da Eulero si può formulare nel seguente modo: è possibile trovare un percorso chiuso che, a partire da una qualunque area, permetta di attraversare i ponti una ed una sola volta e ritornare al punto di partenza? La zona presa in esame può essere schematizzata come in Figura 1.2 dove i punti in grassetto rappresentano le aree (le due isole, indicate con le lettere a e d e le due rive 1 2 Introduzione Figura 1.1: L’attuale mappa dell’area dei ponti del problema di Könisberg [1]. del fiume, indicate con le lettere b e c) e gli archi rappresentano i ponti tra le diverse aree. Cercando di risolvere il problema ci si convince abbastanza presto che questo non ha soluzione e che non ha soluzione anche se vogliamo passare per ogni ponte una ed una sola volta, ma accettiamo di non ritornare esattamente nella stessa area. Eulero dimostrò che la forma delle suddette aree non ha alcuna influenza, ma che la soluzione dipende solo dalle proprietà di connessione. La soluzione a questo problema sarà data nel Capitolo 3. b a5 a1 a4 a3 a d a6 a2 a7 c Figura 1.2: Grafo corrispondente al Problema di Könisberg. Introduzione 1.1 3 Definizioni In questa sezione verranno esposte le definizioni di base della Teoria dei Grafi. Il lettore deve però essere allertato del fatto che non esiste una notazione universalmente accettata, ma che spesso per gli stessi concetti possono trovarsi in letteratura notevoli differenze1 . Per quanto possibile, verrà data segnalazione lungo le pagine di queste note. Definizione 1.1.1 Un grafo G è una tripla costituita da un insieme di vertici V (G) (detti anche nodi), un insieme di archi E(G) e una relazione, detta relazione di incidenza, che associa con un arco una coppia di vertici, detti estremi. Un grafo può essere rappresentato graficamente mediante dei punti per indicare i nodi e da linee e curve per gli archi. Esempio 1.1.1 In Figura 1.2 è disegnato il grafo G rappresentante il Problema di Könisberg, dove l’insieme dei vertici è V (G) = {a, b, c, d} (le quattro regioni) e l’insieme degli archi è E(G) = {a1 , a2 , a3 , a4 , a5 , a6 , a7 } (i ponti). L’insieme degli archi può rappresentarsi anche come E(G) = {ab, ac, ad, bd, db, cd, dc} ed in questo modo risulta esplicitata maggiormente la relazione di incidenza. Notare che i nodi b e d (e anche c e d) hanno più di un arco in comune. Gli esempi di problemi che possono essere descritti come un grafo sono molteplici. Per esempio è possibile descrivere la rete stradale come un grafo dove i nodi sono gli incroci e gli archi sono le strade che li congiungono; analogamente, un circuito elettrico può essere visto come un grafo i cui nodi sono le connessioni e gli archi i componenti del circuito stesso. Definizione 1.1.2 Un loop (o laccio) è un arco i cui estremi coincidono. Si chiamano archi multipli gli archi i cui estremi sono gli stessi. Definizione 1.1.3 Si definisce grafo semplice un grafo privo di loop ed archi multipli. 1 Nel luglio del 2006, Douglas West ha proposto un sondaggio su Internet rivolto alla comunità di coloro che si occupano di Teoria dei Grafi per cercare di uniformare almeno alcune notazioni principali. Per quanto possibile, l’esito di tale sondaggio è stato preso in considerazione in queste note Introduzione 4 Individueremo un grafo semplice mediante i suoi insiemi di nodi e di archi, considerando l’insieme degli archi come un insieme di coppie non ordinate di vertici e scrivendo e = ab (o egualmente e = ba) per un arco con estremi a e b. Si possono dare ulteriori definizioni sui nodi e sugli archi: Definizione 1.1.4 Se ab ∈ E(G) allora a e b si dicono adiacenti e si indica con a ∼ b. I vertici a e b si dicono incidenti sull’arco ab. Due archi si dicono adiacenti se hanno un vertice in comune. Definizione 1.1.5 Un grafo G si dice finito se gli insiemi V (G) ed E(G) sono finiti. In gran parte delle applicazioni i grafi sono finiti e privi di loop ed archi multipli. Di conseguenza, in seguito faremo riferimento quasi esclusivamente a grafi semplici e sempre a grafi finiti. Per evitare confusione, inoltre, non considereremo il grafo nullo, ovvero il grafo con l’insieme dei vertici e degli archi vuoti. Per semplicità di notazione, nel seguito indicheremo con V o N l’insieme dei vertici V (G) e con E l’insieme degli archi E(G). 1.1.1 Sottografi, ordine, dimensione e densità di un grafo Definizione 1.1.6 Dato un grafo G = (V, E), G′ = (V ′ , E ′ ) è un sottografo di G se V ′ ⊂ V ed E ′ ⊂ E e posso scrivere G′ ⊂ G. Se il sottografo G′ contiene tutti gli archi di G che congiungono due vertici in G′ , allora G′ è chiamato sottografo indotto e si indica con G[V ′ ]. Se il sottografo G′ ha V ′ ≡ V , allora G′ è chiamato sottografo ricoprente. Per indicare un sottografo scriveremo genericamente G′ ⊆ G e diremo che G contiene G′ . Esempio 1.1.2 Si consideri il grafo G = ({1, 2, 3, 4, 5, 6}, {12, 14, 16, 25, 34, 36, 45, 56}), rappresentato in Figura 1.3. In Figura 1.4 è rappresentato un suo generico sottografo con G′ = ({1, 2, 3, 4, 6}, {12, 14, 16, 36}), in Figura 1.5 il sottografo indotto G′ = ({1, 2, 3, 4, 6}, {12, 14, 16, 34, 36}) ed infine in Figura 1.6 il sottografo ricoprente G′ = ({1, 2, 3, 4, 5, 6}, {12, 14, 16, 36, 56}). 5 Introduzione 1 1 2 6 2 3 5 3 6 4 4 Figura 1.3: Grafo dell’Esempio 1.1.2 1 Figura 1.4: Un sottografo 1 2 6 3 2 6 3 5 4 4 Figura 1.5: Un sottografo indotto Figura 1.6: Un sottografo ricoprente Spesso è possibile costruire nuovi grafi a partire da un grafo assegnato G cancellando oppure aggiungendo vertici ed archi. Se V ′ ⊂ V , allora G − V ′ = G[V \V ′ ] è il sottografo di G ottenuto cancellando tutti i vertici V ′ e tutti gli archi su di loro incidenti. Analogamente, se E ′ ⊂ E, allora G − E ′ = (V, E\E ′ ). Se V ′ = {v} oppure E ′ = {xy}, allora la notazione si semplifica in G − v e G − xy. Infine, se x ed y sono due vertici non adiacenti di G, allora G + xy è ottenuto congiungendo x ed y con un arco. Definizione 1.1.7 Si definisce ordine di un grafo G il numero dei suoi vertici, ovvero |G| = |V (G)|; si definisce dimensione di un grafo il numero dei sui archi, ovvero e(G) = |E(G)|. 6 Introduzione Per indicare l’ordine di un grafo arbitrario si può utilizzare inoltre la notazione Gn mentre attraverso la notazione G(n, m) si indica un grafo arbitrario di ordine n ed dimensione m. Nell’esempio 1.1.2 il grafo ha ordine 6 e dimensione 8 e potremo indicarlo per esempio come G(6, 8). Dato un grafo semplice arbitrario di ordine n, ci chiediamo in quale campo di valori possa variare il numero di archi m. Chiaramente, tali valori possono variare tra 0 (in corrispondenza del grafo nullo) e tra il numero corrispondente al caso in cui tutti i nodi sono mutualmente adiacenti, numero pari a tutte le possibili combinazioni di n oggetti presi due a due. Quindi: n(n − 1) n 0≤m≤ = 2 2 (1.1) Nel caso specifico il coefficiente binomiale si scrive attraverso la formula che compare nell’ultimo membro della Formula 1.1. Quindi un grafo d’ordine 6 potrà, per esempio, avere dimensione compresa tra 0 e 15. Spesso il numero di archi di un grafo viene espresso in modo proporzionale rispetto al massimo numero di archi possibile. A tale misura viene data il nome di densità di un grafo e si calcola come: D(G) = m n(n − 1)/2 (1.2) chiaramente, tale misura varia da 0 a 1. In generale, diremo sparso un grafo che abbia un numero di archi molto basso, ovvero una densità bassa (per esempio D(G) ≈ 0, 1÷0, 3); viceversa, diremo che é denso un grafo che ha un alto numero di archi, quindi una densità alta (per esempio D(G) ≈ 0, 7 ÷ 0, 9). I grafi che hanno tutti i nodi mutualmente adiacenti rivestono una importanza particolare e sono chiamati grafi completi (o clique) e sono indicati con Kn , dove n indica la sua cardinalità. Dato che un grafo può contenere sottografi completi, possiamo dare la definizione più generale: Definizione 1.1.8 Un grafo completo (o clique) in un grafo G è un insieme di nodi 7 Introduzione mutualmente adiacenti. Dato l’insieme di tutte le clique di G, la cardinalità della clique massima viene chiamata numero di clique ed è indicato con ω(G). 1.1.2 Vicinati, gradi e regolarità Passiamo ora a dare altre definizioni sui grafi. Definizione 1.1.9 L’insieme dei vertici adiacenti ad un assegnato vertice x ∈ G è chiamato vicinato di x e si indica con Γ(x). Con il simbolo Γ(x) spesso ci si riferisce al vicinato aperto, mentre per indicare il vicinato chiuso si usa scrivere Γ(x) ∪ {x}. Notare che se y ∈ Γ(x) e x ∈ Γ(y), allora significa che x ∼ y e y ∼ x, ovvero xy è un arco del grafo. Definizione 1.1.10 Si definisce grado d(x) di un nodo x ∈ G la cardinalità del suo vicinato, ovvero, d(x) = |Γ(x)|. Se un vertice ha d(x) = 0, allora si definisce vertice isolato. Anche in questo caso il simbolismo non è univocamente definito, perchè possiamo trovare indicato il vicinato ed il grado rispettivamente con ΓG (x) e dG (x), in modo da non sottintendere il grafo G. Dato un grafo G, si usa indicare il suo grado minimo con δ(G) ed il suo grado massimo con ∆(G) L’introduzione del concetto di grado di un vertice ci permette di introdurre un primo importante risultato generale, che viene chiamato spesso Lemma Handshaking 2 : Lemma 1.1.1 In ogni grafo, il numero di vertici di grado dispari è pari. Dimostrazione: Sommando i gradi di ogni vertice, ogni arco è contato esattamente due volte (una volta per ogni vertice). Quindi avremo che: X d(i) = 2m (1.3) ∀i∈V 2 La ragione del nome del lemma deriva dal fatto che in un ricevimento, se tutti gli invitati si salutano tra loro allora la somma totale delle strette di mano (in inglese, handshaking) è sempre pari. 8 Introduzione Dato che il secondo membro è positivo, anche il primo deve esserlo; quest’ultimo è composto da due contributi, la somma dei gradi pari e la somma dei gradi dispari. È quindi chiaro che il contributo dei vertici di grado dispari deve essere pari e questo accade solo se sono in numero pari. Definizione 1.1.11 Dato un grafo G, se δ(G) = ∆(g) = k allora G si dice k-regolare. Un grafo G si dice regolare se è k-regolare per un certo k. Figura 1.7: Esempi di grafi 3-regolari Per esempio, tutte le clique Kn sono (n − 1)-regolari; ancora, se k = 3 si ha un grafo 3-regolare (vedi Figura 1.7), detto anche cubico. 1.1.3 k-Fattorizzazioni Definizione 1.1.12 Un k-fattore è un sottografo ricoprente k-regolare. Se l’insieme degli archi può essere diviso in k-fattori ho una decomposizione che prende il nome di k-fattorizzazione. Se ho una decomposizione in 1-fattori allora la decomposizione prende il nome di fattorizzazione ed ha ovviamente senso solo se il grafo ha un numero pari di nodi. In Figura 1.8 sono rappresentate (con gli archi in grassetto) le tre possibili fattorizzazioni di K4 . 9 Introduzione 1 2 1 2 1 2 3 4 3 4 3 4 Figura 1.8: Le fattorizzazioni di K4 1.2 Grafi e modelli In questa sezione verranno mostrate alcune applicazioni pratiche che possono essere rappresentate mediante dei grafi. Gli esempi illustrati ci serviranno soprattutto per introdurre nuove definizioni e termini della teoria dei grafi. 1.2.1 Relazioni e sottografi Supponiamo sia assegnato un gruppo di cinque persone; come è possibile rappresentare le relazioni di conoscenza tra i membri del gruppo? Se indichiamo con le prime cinque lettere dell’alfabeto i cinque membri del gruppo, possiamo costruire un grafo nel quale i nodi rappresentano i membri, ed un arco esiste se due membri si conoscono. Supponiamo sia assegnata un’istanza di tale problema come in Figura 1.9. Supponiamo di voler studiare ora il problema complementare: dato ancora lo stesso gruppo di persone, come rappresentare le relazioni di non conoscenza tra i membri? Definizione 1.2.1 Il grafo complemento Ḡ di un grafo semplice G è un grafo semplice avente insieme dei nodi V (Ḡ) ≡ V (G) ed insieme degli archi tale che un arco ab esiste in Ḡ se e solo se ab ∈ / E(G). Definizione 1.2.2 Un insieme indipendente (o insieme stabile) W in un grafo è un insieme di vertici mutuamente non adiacenti, ovvero se e soltanto se il sottografo indotto G[W ] = ∅. 10 Introduzione b b a c e a d Figura 1.9: Grafo rappresentante le relazioni di conoscenza tra i cinque membri del gruppo c e d Figura 1.10: Grafo rappresentante le relazioni di non conoscenza tra i cinque membri del gruppo La definizione di complementarietà ci permette di legare tra di loro i due problemi introdotti e di ottenere il grafo rappresentante il secondo attraverso tale definizione, come si può vedere in Figura 1.10. Possiamo notare inoltre che nel grafo G, il sottografo {a, b, c} è una clique di ordine 3 e {c, e} è un insieme indipendente di cardinalità 2. Corrispondentemente, nel grafo G, i nodi {a, b, c} sono un insieme indipendente di cardinalità 3 e {c, e} è una clique di ordine 2. Quindi gli insiemi indipendenti di un grafo diventano clique nel suo grafo complementare cosı̀ come le clique in un grafo diventano insiemi indipendenti nel suo grafo complemento. L’insieme indipendente, oltre che alla clique, è legato ad un altro concetto, ovvero quello del vertex covering: Definizione 1.2.3 Un vertex covering di un grafo G è un sottoinsieme V ′ di V tale che ogni arco di G è incidente con almeno un vertice in V ′ . Si può dimostrare [14] che, se α è il valore della cardinalità del massimo insieme indipendente di G e β la cardinalità del più piccolo V ′ tale che sia un vertex covering di G, allora α + β = n, dove n è l’ordine di G. Infatti, ogni insieme indipendente di cardinalità massima è il complemento di un vertex cover minimo. 1.2.2 Assegnamento di lavori Si supponga che un’azienda debba effettuare n compiti diversi, da assegnare ognuno ad m dipendenti, che però non sono interscambiabili perchè ognuno di essi possiede delle 11 Introduzione specializzazioni. In che modo assegnare i compiti ai dipendenti? Per risolvere questo problema possiamo costruire un grafo nel quale i vertici sono gli n compiti e gli m dipendenti, mentre un arco ij esiste se il compito i può essere svolto dal dipendente j. In Figura 1.11 è disegnato un grafo corrispondente al caso in cui n = m = 4 e si può notare come in realtà l’insieme dei nodi sia partizionato in due sottoinsiemi (indipendenti) V1 (corrispondenti ai compiti) e V2 (corrispondenti ai dipendenti). V1 V2 Figura 1.11: Grafo bipartito Per questa particolare categoria di grafi possiamo dare la seguente definizione: Definizione 1.2.4 Un grafo G si dice bipartito se l’insieme dei suoi nodi V (G) è l’unione di due insiemi indipendenti disgiunti V1 e V2 chiamati insiemi partizione ed ogni suo arco va da V1 a V2 . Un grafo bipartito è anche chiamato 2-partito, perchè può essere visto come un particolare caso dei grafi r-partiti, ovvero di quei grafi nei quali l’insieme dei nodi è costituito dall’unione di r insiemi partizione e gli archi vanno da una delle partizioni verso una qualunque delle altre. 1.2.3 Scheduling di team in una azienda Una azienda ha la necessità di schedulare3 un certo numero di riunioni dei vari team, ma ha il vincolo che alcuni manager appartengono a team differenti e quindi non è possibile indire due riunioni contemporaneamente se hanno un manager in comune. 3 Il termine inglese scheduling indica un problema decisionale che considera l’allocazione di risorse scarse ad attività, con l’obiettivo di ottimizzare una o più misure di prestazione. Siccome tale parola è intraducibile in italiano, viene di solito adattata, ottenendo i termini schedulare, schedulato, ecc. 12 Introduzione 1 2 1 a b e 1 3 c d Figura 1.12: Una istanza del problema dei team Per rappresentare tale problema posso disegnare un grafo nel quale i nodi sono i team, ed esiste un arco se i due team hanno un membro in comune, come in Figura 1.12. Per risolverlo, invece, posso usare un algoritmo che, partendo da un nodo, assegni ad ognuno di essi una etichetta (colore) in modo che due nodi adiacenti non abbiano stessa etichetta. Associando l’etichetta ad un istante temporale, e considerando che nodi che hanno stessa etichetta non hanno membri in comune, ne consegue che per risolvere il problema è sufficiente schedulare i team nell’ordine dato dalle etichette. Per esempio, considerando le etichette assegnate al grafo in Figura 1.12 una possibile sequenza è {a, c, e}, {b} e {d} (notare come l’ordine delle etichette corrisponda agli istanti di attuazione). Generalizzando il problema, ci si può chiedere quale sia, tra tutte le etichettature ammissibili, quella che implica il minor numero di etichette possibili. Definizione 1.2.5 Si definisce numero cromatico di un grafo G e si indica con χ(G), il minimo numero di colori necessari per etichettare i vertici in modo che vertici adiacenti ricevano colori diversi. Il termine colore deriva dal famoso Problema dei quattro colori, posto da Francis e Frederick Guthrie nel 1852 e da A. Cayley nel 1878, nel quale si ci si chiedeva quale fosse il minimo numero di colori necessari per colorare una mappa geografica politica. Nel 1890 P. J. Heawood congetturò che bastassero quattro colori, ma tale congettura è stata dimostrata definitivamente solo nel 1976 da K. Appel e W. Haken. Per altri aspetti storici sul problema dei quattro colori rimandiamo il lettore a [9]. Introduzione 1.2.4 13 Percorsi su rete Uno dei campi nel quale la teoria dei grafi trova grande applicazione è quello della rappresentazione delle reti stradali. Infatti, possiamo associare un vertice del grafo ad ogni incrocio stradale ed unire tra di loro tali vertici con un arco in corrispondenza del tratto di strada che unisce i due incroci. Inoltre possiamo iniziare ad introdurre dei nuovi elementi che possiamo associare agli archi ed ai nodi, ovvero dei pesi; in questo caso, un peso associato agli archi può rappresentare la lunghezza del tratto stradale, oppure la sua capacità in termini di auto per ora, ecc. Questa rappresentazione fa nascere alcuni quesiti, per esempio, dati due vertici qualunque a e b, è possibile individuare sulla rete il cammino più breve tra questi due punti? Oppure, se i vertici rappresentano delle città e gli archi la rete autostradale, è possibile individuare un percorso che partendo da una generica città visiti tutte le restanti con un percorso di distanza minima? Vedremo nel seguito come risolvere il primo problema (vedi Capitolo 6) mentre per il secondo rimandiamo alla bibliografia, invitando il lettore ad approfondire gli argomenti riguardanti il cosiddetto Problema del commesso viaggiatore o TSP, Travelling Salesman Problem per esempio in [7] o [14]. Definizione 1.2.6 Un cammino (o anche path) è un grafo semplice i cui vertici possono essere ordinati in una lista in modo che due vertici sono consecutivi se e soltanto se sono consecutivi nella lista medesima. Quindi, P = (V (P ) = {x0 , x1 , . . . xl }, E(P ) = {x0 x1 , x1 x2 . . . xl−1 xl }) è un cammino che indicheremo con x0 x1 . . . xl . Chiameremo i vertici x0 e xl terminali del cammino. P Inoltre, se indichiamo con di la lunghezza associata ad ogni arco, allora l = ∀xi xj ∈E(P ) di è la lunghezza del cammino. Definizione 1.2.7 Un ciclo è un cammino con egual numero di vertici ed archi e con i terminali coincidenti. A titolo di esempio, in Figura 1.13 è rappresentato il cammino x0 x1 x3 x2 x4 x5 , mentre in Figura 1.14 è rappresentato il ciclo x0 x1 x3 x2 x4 x5 x0 . 14 Introduzione x2 x1 x0 x2 x1 x0 x3 x4 x5 x3 x4 x5 Figura 1.13: Un cammino Figura 1.14: Un ciclo P3 C4 P4 C5 Figura 1.15: Alcuni esempi di path e cicli Per indicare path e cicli arbitrari si usa aggiungere alla nomenclatura un pedice che indica il numero di archi di cui sono composti. Quindi, per esempio, P4 indica un path di lunghezza 4, mentre C5 indica un ciclo di lunghezza 5 (alcuni esempi sono dati in Figura 1.15). Inoltre, un path con terminali x e y viene spesso indicato come “{x, y}-path”. Un cammino od un ciclo possono essere identificati come sottografi in un grafo arbitrario e questo ci porta a generalizzare le definizioni appena date. Definizione 1.2.8 Dato un grafo arbitrario G = (V, E), allora: • un walk W in un grafo è una sequenza alternata di vertici ed archi, x0 e1 x1 e2 . . . el xl , con ei = xi−1 xi e 0 < i ≤ l; • un trail T è un walk con tutti gli archi distinti; • un path P è un trail con tutti i vertici distinti; • un trail chiuso (o anche circuito) è un trail con x0 ≡ xl ; 15 Introduzione • un ciclo C è un walk composto da più di tre archi, ha x0 ≡ xl ed ogni vertice distinto. Definizione 1.2.9 Dato un grafo G, un insieme di cammini {P 1 , P 2 , . . . P k } si dice indipendente (o internamente disgiunto) se per ogni coppia di cammini, gli unici vertici in comune sono i terminali, ovvero sono dei {x, y}-path se V (P i )∩V (P j ) = {x, y}, ∀i 6= j. Esempio 1.2.1 Dato il grafo in Figura 1.16, un walk è {abcebc}, un trail è {abcebf }, un trail chiuso è {abcebf ea}, un path è {abcdef } ed, infine, un ciclo è {abcdef a}. b a c d f e Figura 1.16: Grafo per l’Esempio 1.2.1 Il concetto di cammino in un grafo ci permette di introdurre un’importante proprietà dei grafi, ovvero la connessione. Definizione 1.2.10 Un grafo G è connesso se ogni coppia di suoi vertici appartiene ad un cammino e sconnesso altrimenti. Riprenderemo questo concetto nelle prossime sezioni. 1.3 Esercizi Es. 1.3.1 Dire se è possibile disegnare un grafo in cui i gradi dei vertici siano i seguenti: d(1) = 1; d(2) = 2; d(3) = 3; d(4) = 4; d(5) = 5. Introduzione 16 Es. 1.3.2 E’ possibile disegnare un grafo 3-regolare con 9 vertici? Se si disegnarlo. E con 8 vertici? Se si disegnarlo. Es. 1.3.3 Dato K6 , quanti cammini ricoprenti posso fare partendo da un vertice assegnato a? Es. 1.3.4 Dato K4,4 , quanti cammini ricoprenti posso fare partendo da un dato vertice a? Es. 1.3.5 Si disegni una 2-fattorizzazione di K5 . Es. 1.3.6 Si mostri se è possibile disegnare una 2-fattorizzazione di K6 . Es. 1.3.7 (Difficile) Determinare il numero di 1-fattori in K2n . Es. 1.3.8 (Difficile) Determinare il numero di 2-fattori in K2n,2n . Capitolo 2 Algoritmi I progressi ottenuti nel campo dell’elaborazione elettronica hanno permesso lo sviluppo e l’applicazione dei metodi matematici per la modellizzazione e la risoluzione di una grande varietà di problemi decisionali, anche di dimensioni ragguardevoli. Tutto l’insieme di metodologie che hanno in comune l’uso del metodo matematico, come per esempio l’ottimizzazione, la programmazione matematica, la teoria dei grafi, la teoria delle code, la teoria delle decisioni, la simulazione, ecc, sono raccolte in una disciplina che prende il nome di Ricerca Operativa. Data la natura applicativa della Ricerca Operativa, lo studio teorico del problema matematico posto viene normalmente affiancato allo studio delle tecniche necessarie per ottenere una soluzione in modo efficiente. 2.1 Breve tassonomia dei problemi decisionali In generale, nella modellazione di un problema decisionale, ci dobbiamo preoccupare di tre componenti fondamentali: il grado di incertezza, il numero di obiettivi ed il numero di decisori. Il grado di incertezza indica se ci si trova in condizioni di informazione completa, come nei problemi deterministici, oppure in condizioni di conoscenza parziale, come nei problemi stocastici. Il numero di obiettivi è un’altra componente da conoscere e, in generale, potremo riconoscere problemi a singolo obiettivo, oppure multiobiettivo. Analogamente, 17 Algoritmi 18 per il numero dei decisori avremo una divisione in due classi, quella dei problemi a singolo decisore e quella dei problemi multidecisore. Continuando con questi accenni di tassonomia dei problemi decisionali, potremo considerare problemi lineari oppure problemi non lineari, a seconda della nostra funzione obiettivo. Inoltre, sulla base dei valori che possono assumere le variabili, avremo problemi continui se i valori apparterranno allo spazio dei numeri reali R, problemi discreti (o combinatori) se i valori apparterranno allo spazio dei numeri interi Z ed infine, problemi misti se le variabili possono assumere valori sia reali che interi. Utilizzando lo schema appena descritto, nella trattazione della teoria dei grafi ci limiteremo in seguito al solo studio dei problemi deterministici, singolo obiettivo, singolo decisore, lineari e discreti. Al lettore è lasciato immaginare la quantità di problemi decisionali che si possono osservare nella realtà al presentarsi ed al combinarsi delle diverse ipotesi sopra dette e, di conseguenza, come la loro analisi sia fondamentale per individuare le tecniche più adeguate per risolverli. 2.2 Algoritmi Per la soluzione di un problema, occorre individuare un metodo generale (procedura) in grado di risolvere ogni generica istanza, fornendoci la soluzione desiderata in un certo numero di passi. Il termine generale usato per definire tali procedure è algoritmo 1 . Definizione 2.2.1 Un algoritmo è una procedura definita usata per risolvere un problema usando un numero finito di passi. Gli algoritmi ricopriranno una grande importanza nel nostro studio della Teoria dei Grafi e, quindi, dedicheremo le prossime pagine alla descrizione di alcuni strumenti per la loro analisi. Esempio 2.2.1 Descrivere un algoritmo per trovare l’elemento più grande in una sequenza (lista) di interi. 1 Il termine algoritmo deriva dal nome del matematico persiano Abu Ja’far Mohammed ibn Musa alKhowarizmi, vissuto nel IX secolo d.C.. 19 Algoritmi Per specificare la procedura di risoluzione di questo semplice problema possiamo utilizzare molti metodi, ma uno dei più semplici è quello di utilizzare il linguaggio naturale per descrivere i singoli passi della procedura. Per risolvere il problema devono essere eseguiti i seguenti passi: Step 1: Poni il massimo temporaneo uguale al primo intero della sequenza; Step 2: Compara il prossimo intero nella sequenza. Se è più grande, poni il massimo temporaneo pari a tale valore; Step 3: Ripetere il passo precedente per ogni altro elemento della lista; Step 4: Stop se non ci sono altri interi. Il valore cercato è contenuto nel massimo temporaneo (che diventa definitivo). Per descrivere più efficacemente un algoritmo si può utilizzare una descrizione mediante pseudocodice, basata su una sintassi molto simile al linguaggio di programmazione PASCAL, di facile comprensione per chiunque abbia dei rudimenti di Fondamenti di Informatica. Essa inoltre ci permette di evitare le specificità di un linguaggio di programmazione. Utilizzando lo pseudocodice, il nostro algoritmo diventa: procedure MAX(a1 , . . . , an ; integers) max := a1 for i := 2 to n if max < ai then max := ai max contiene il massimo Come è facile notare, l’algoritmo in pseudocodice segue fedelmente i passi sopra descritti. Per poter fornire una soluzione significativa, gli algoritmi devono rispettare alcune proprietà: Proprietà 2.2.1 Un algoritmo deve soddisfare le seguenti proprietà: 20 Algoritmi • Input - L’algoritmo deve avere un input contenuto in un insieme definito I. • Output - Da ogni insieme di valori in input, l’algoritmo produce un insieme di valori in uscita che comprende la soluzione. • Determinatezza - I passi dell’algoritmo devono essere definiti precisamente. • Finitezza - Un algoritmo deve produrre la soluzione in un numero di passi finito (eventualmente molto grande) per ogni possibile input definito su I. • Efficacia - Deve essere possibile effettuare ogni passo dell’algoritmo esattamente ed in un tempo finito. • Generalità - L’algoritmo deve essere valido per ogni insieme di dati contenuti in I e non solo per alcuni. Oltre a queste proprietà, un algoritmo deve essere efficiente, ovvero, dato un input di dimensione fissata, deve fornire una soluzione in un tempo ragionevole ed inoltre deve occupare una quantità limitata di memoria di un computer. Problematiche di questo tipo sono trattate dall’analisi della complessità computazionale degli algoritmi; in particolare, se l’oggetto dello studio è il tempo di elaborazione, parleremo di complessità temporale, mentre se l’oggetto è l’occupazione della memoria, allora parleremo di complessità spaziale. È chiaro quindi che nell’analisi di un algoritmo è di fondamentale importanza sapere se risolverà il nostro problema in un microsecondo, in un ora o in un secolo. Analogamente, è importante sapere se l’occupazione di memoria possa eccedere le capacità disponibili. L’analisi della complessità spaziale coinvolge principalmente l’analisi delle strutture dati e, quindi, esula dagli scopi di queste note. Viceversa, l’analisi della complessità temporale è molto importante per gli algoritmi su grafo e sarà approfondita nella Sezione 2.4. 2.2.1 Algoritmi di ricerca In questa sezione vedremo alcuni esempi di algoritmi ed in particolare ci concentriamo sugli algoritmi di ricerca su stringa che abbiamo già introdotto nell’Esempio 2.2.1. Algoritmi 21 Gli algoritmi di ricerca rivestono un particolare interesse nella pratica; basti pensare alla necessità di trovare una parola in un dizionario, un dato in un database o anche alla ricerca di pagine web attraverso i motori di ricerca. In quest’ultimo caso, l’algoritmo di ricerca presenta, ovviamente, complessità ben diverse. Il primo algoritmo che introduciamo è l’algoritmo di ricerca lineare (o sequenziale): data una lista di elementi distinti a1 , a2 . . . , an , localizzare l’elemento x o affermare che non c’è. procedure LINEAR SEARCH (x, integer; a1 , . . . , an , distinct integers) i := 1 while (i ≤ n AND x 6= ai ) i := i + 1 if i ≤ n then posizione := i else posizione := 0 L’algoritmo inizia confrontando x con a1 . Se l’elemento non è stato individuato, si incrementa il contatore i e quindi si continua fino a che una delle due condizioni risulta falsa (cioè o sono arrivato alla fine della lista, o ho trovato l’elemento) ed il ciclo while termina. L’istruzione condizionale if ha il compito di inserire nella variabile di output il valore della posizione o il valore 0 se tale valore non è nella lista. Il secondo algoritmo di ricerca che descriviamo è l’algoritmo di ricerca binaria: data una lista di elementi distinti a1 , a2 . . . , an , ordinati in modo che a1 ≤ a2 ≤ . . . ≤ an , localizzare l’elemento x o affermare che non c’è. La differenza in questo caso è che la sequenza è ordinata in modo crescente, come per esempio può accadere in un vocabolario se il criterio adottato è quello lessicografico. Supponiamo allora che sia assegnata la sequenza {1, 2, 4, 5, 6, 9, 10, 12, 15, 18, 20, 24} e che si voglia trovare se il numero 18 appartiene a tale lista. L’idea che sta alla base dell’algoritmo è quella di dividere ad ogni passo la lista in due parti (nel nostro caso {1, 2, 4, 5, 6, 9} e {10, 12, 15, 18, 20, 24}) e confrontare l’elemento da cercare rispettivamente con l’ultimo elemento della prima metà e con il primo elemento della seconda metà. Nel nostro caso l’elemento è più grande del primo elemento della Algoritmi 22 seconda metà e quindi possiamo concentrarci solo in tale stringa per la ricerca. L’algoritmo continua dividendo tale stringa ulteriormente, ottenendo pertanto {10, 12, 15} e {18, 20, 24} ed eseguendo di nuovo i confronti. Cosı̀ facendo l’algoritmo genera ancora le stringhe {18} e {20, 24} arrestandosi al valore cercato oppure affermando che non appartiene a tale lista. Lo pseudocodice dell’algoritmo proposto è il seguente: procedure BINARY SEARCH (x, integer; a1 , . . . , an , increasing integers) i := 1 j := n while (i < j ) begin ⌋ m := ⌊ i+j 2 if x > am then i := m + 1 else j := m end if x = ai then posizione := i else posizione := 0 L’apparente complessità dell’algoritmo di ricerca binaria, rispetto a quello lineare, nasconde dei benefici che saranno mostrati nel paragrafo 2.4. 2.3 Crescita di funzioni Nell’analisi di un algoritmo è di particolare interesse comprendere la sua applicabilità pratica e, principalmente, capire il tempo necessario per ottenere un risultato utile, ovvero la sua efficienza. Osservando gli algoritmi presentati nella sezione precedente, si può notare come l’input sia sempre legato al numero n di oggetti in ingresso sui quali eseguire l’elaborazione. Ci potremmo quindi chiedere quanto cresce il tempo di elaborazione al crescere di n e se è possibile trovare una funzione f (n) che sia in grado di trasferire questa informazione. Inoltre, sarebbe utile disporre di un criterio in grado di paragonare due algoritmi confrontando la crescita delle rispettive funzioni dell’input. 23 Algoritmi 2.3.1 Notazione Big-O Per analizzare il comportamento degli algoritmi dobbiamo prima introdurre la notazione Big-O, necessaria per lo studio della crescita di una funzione generica, di cui segue la definizione: Definizione 2.3.1 Siano f e g due funzioni tali che f, g : N → R (o anche f, g : R → R). Diremo che f (n) = O(g(n)) se esistono due costanti, C e k, tali che ∀n > k si ha: f (n) ≤ C|g(n)| (2.1) La 2.1 si legge come “f (n) è un big o di g(n)”. È importante notare che basta trovare una sola coppia C, k tale che sia vera la 2.1. In realtà, una coppia che soddisfa la definizione data non è mai unica, anzi, basta prendere una qualunque coppia C ′ , k ′ tale che C < C ′ e k < k ′ per soddisfare la definizione e questo ci porta a dire che se una coppia esiste, allora ne esistono infinite. Esempio 2.3.1 Mostrare che f (n) = n2 + 2n + 1 è un O(n2 ). Per risolvere questo esercizio basta osservare che 0 ≤ n2 + 2n + 1 ≤ n2 + 2n2 + n2 = 4n2 , avendo considerato C = 4 e k = 1. Notare che in questo caso si ha che f (n) ≤ C|g(n)| e g(n) ≤ C|f (n)|. Quando ciò accade diremo che le funzioni sono dello stesso ordine. Occorre notare che il segno di uguale nella 2.1 non è realmente un uguale, ma, piuttosto, indica che in questa notazione, quando si hanno dei valori di n sufficientemente grandi nei dominii di f e g, la disuguaglianza è verificata. Se f (n) ≤ C|g(n)| e h(n) è una ulteriore funzione che assume valori assoluti maggiori di g(n) a partire da valori di n sufficientemente grandi, allora si ha ovviamente che f (n) ≤ C|h(n)|. Di norma, però, si sceglie la funzione g più piccola possibile; quindi nell’esempio precedente è corretto, ma privo di senso, dire che f (n) = O(n3 ). Esempio 2.3.2 Dare una stima Big-O della somma dei primi numeri n interi positivi. 24 Algoritmi Dato che 1 + 2 + . . . + n ≤ n + n + . . . + n = n2 , allora 1 + 2 + . . . + n = O(n2 ), con C = 1 e k = 1. Esempio 2.3.3 Dare una stima Big-O di f (n) = log n!. Per quanto riguarda il fattoriale si ha che 1 · 2 · . . . · n ≤ n · n · . . . · n = nn . Quindi log n! ≤ log nn = n log n, che implica log n! = O(n log n). 2.3.2 Crescita di combinazioni di funzioni Gli algoritmi sono tipicamente composti da diverse operazioni concatenate ed annidate in sottoprocedure, quindi la notazione introdotta nel paragrafo precedente deve essere estesa in modo da tenere conto del peso delle singole sottoprocedure. Supponiamo allora di avere assegnate due funzioni f1 (n) = O(g1 (n)) e f2 (n) = O(g2 (n)). Per la definizione data nel paragrafo precedente sappiamo che esistono delle costanti C1 , C2 , k1 e k2 tali che f1 (n) ≤ C1 |g1 (n)|, ∀n > k1 e f2 (n) ≤ C2 |g2 (n)|, ∀n > k2 . Teorema 2.3.1 Si supponga che f1 (n) = O(g1 (n)) e f2 (n) = O(g2 (n)). Allora la somma delle due funzioni è: (f1 + f2 )(n) = O(max{g1 (n), g2 (n)}) (2.2) Dimostrazione: Si noti che |(f1 +f2 )(n)| = |f1 (n)+f2 (n)| ≤ |f1 (n)|+|f2 (n)| (quest’ultima relazione è vera per la disuguaglianza triangolare, |x + y| ≤ |x| + |y|). Se si considera g(n) = max{g1 (n), g2 (n)} e C = C1 + C2 , allora |f1 (n)| + |f2 (n)| ≤ C1 |g1 (n)| + C2 |g2 (n)| ≤ C|g(n)|. Corollario 2.3.2 Se entrambe le funzioni f1 (n) e f2 (n) sono entrambe O(g(n)), allora (f1 + f2 )(n) = O(g(n)). Per quanto riguarda il prodotto di funzioni, vale il seguente teorema: 25 Algoritmi Teorema 2.3.3 Si supponga che f1 (n) = O(g1 (n)) e f2 (n) = O(g2 (n)). Allora il prodotto delle due funzioni è: (f1 · f2 )(n) = O(g1 (n) · g2 (n)) (2.3) Dimostrazione: Considerando C = C1 · C2 , allora (f1 · f2 )(n) = |f1 (n)| · |f2 (n)| ≤ C1 |g1 (n)| · C2 |g2 (n)| ≤ C|(g1 · g2 )(n)|. Esempio 2.3.4 Dare una stima Big-O della funzione f (n) = 3n log(n!) + (n2 + 3) log n. Considerando ogni termine singolarmente abbiamo: f (n) = 3n → n log n! → n log n → log n +(n2 + 3) → n2 + 3 ≤ 4n2 log n Quindi f (n) = O(n2 log n). L’ultimo risultato di questa sezione riguarda la crescita di funzioni polinomiali (in x, per comodità di notazione): Teorema 2.3.4 Sia f (x) = an xn + an−1 xn−1 + . . . + a0 , con an , an−1 . . . , a0 numeri reali. Allora f (x) = O(xn ). Dimostrazione: Utilizzando la disuguaglianza triangolare, e per x > 1, |f (x)| = |an xn + an−1 xn−1 + . . . + a0 | ≤ |an |xn + |an−1 |xn−1 + . . . + |a0 | = xn (|an | + |a0 | |an−1 | + ... + n ) x x ≤ xn (|an | + |an−1 | + . . . + |a0 |) Questo dimostra che |f (x)| < Cxn , dove C = |an | + |an−1 | + . . . + |a0 |, se x > 1. Quindi f (x) = O(xn ). 26 Algoritmi Come abbiamo detto, la notazione Big-O viene usata per la stima del numero di operazioni necessarie affinchè un algoritmo risolva un dato problema. Le funzioni che normalmente si usano sono 1, log n, n, n log n, n2 , 2n , n!. La sequenza presentata non è casuale, ma rispetta l’ordinamento tale per cui la funzione successiva è sempre più grande di quella che la precede. In Figura 2.1 sono riportati i grafici delle funzioni indicate, dove n sia l’ascisse e in ordinata siano i valori della funzione, in scala esponenzialmente crescente. 2048 n! 1024 2n 512 256 128 n2 64 32 16 8 n log n n 4 log n 2 1 1 2 3 4 5 6 7 8 Figura 2.1: Grafico delle funzioni più comunemente usate nella stima Big-O 2.4 Complessità degli algoritmi L’analisi della complessità temporale degli algoritmi può essere espressa in termini di numero di operazioni eseguite dallo specifico algoritmo quando l’input ha un data dimensione. Questo tipo di analisi risulta essere più efficiente della semplice misura del tempo impiegato da un computer per completare la sua elaborazione, perché, nel caso, la velocità di elaborazione può variare molto da computer a computer ed è inoltre difficile da misurare e valutare. Algoritmi 27 Per illustrare come analizzare la complessità di un algoritmo, consideriamo il primo esempio della Sezione 2.2 per trovare l’elemento più grande in una lista. Le operazioni che sono eseguite sono i due confronti all’interno del ciclo for, uno per verificare se si è giunti alla fine della lista, l’altro per aggiornare, eventualmente, il massimo temporaneo. Dato che i due confronti vengono ripetuti dal passo due al passo n ed è poi eseguito un’ulteriore confronto per uscire dal ciclo quando il contatore i = n + 1, si ha che sono eseguiti esattamente 2(n − 1) + 1 = 2n − 1 confronti. Quindi, dato un input di lunghezza n, se si misura la complessità in termini di confronti, si ha che l’algoritmo trova il massimo in una lista di lunghezza n in O(n) passi. Questo ragionamento ci ha portato ad usare la notazione Big-O introdotta nella sezione precedente per dare una misura della complessità computazionale temporale dell’algoritmo. Questa procedura può essere generalizzata allo studio dell’efficienza di qualunque algoritmo e, negli esempi che seguono, mostreremo come usare tale misura e come sia possibile servirsi della composizione delle funzioni per valutare la complessità generata da più procedure o da procedure annidate. Prendendo ad esempio l’algoritmo di ricerca lineare, all’interno del ciclo while vengono effettuati due confronti: uno per verificare se si è arrivati alla fine della lista e l’altro per confrontare x con un termine della lista. Successivamente viene eseguito un confronto fuori dal ciclo. Considerando il caso peggiore, ovvero quello in cui l’elemento non è contenuto nella lista, sono eseguiti 2n + 2 confronti e quindi la ricerca lineare richiede almeno O(n) confronti. Il tipo di analisi eseguita sull’algoritmo di ricerca lineare è del tipo worst case, ovvero viene contato il massimo numero di operazioni necessarie per risolvere il nostro problema dato un input fissato. Ovviamente, quest’analisi mostra quante operazioni sono necessarie all’algoritmo, nel caso peggiore, per garantire che verrà prodotta una soluzione, ma nella realtà possono esserne effettuate molte di meno. A titolo di esempio, si può notare che l’algoritmo di ricerca lineare ha complessità proporzionale a n, ma se l’elemento da ricercare è tra i primi nella lista, l’algoritmo termina con un numero di passi minore. Analizziamo ora l’algoritmo di ricerca binaria e, per semplicità, supponiamo che la lista 28 Algoritmi Complessità O(1) O(log n) O(n) O(n log n) O(na ) O(an ), con a > 1 O(n!) Terminologia Complessità costante Complessità logaritmica Complessità lineare Complessità n log n Complessità polinomiale Complessità esponenziale Complessità fattoriale Tabella 2.1: Terminologia comunemente usata per indicare la complessità degli algoritmi sia composta da n = 2k elementi (e, quindi, k = log n). Notare che con questa ipotesi non c’è perdita di generalità, perchè potremmo considerare la nostra lista originale come parte di una lista più grande di 2k+1 elementi, dove 2k ≤ n ≤ 2k+1 . Ad ogni passo dell’algoritmo, le variabili i e j sono confrontate per vedere se la lista ristretta ha più di un termine e, se i < j, viene eseguito un confronto per determinare se x è maggiore del termine mediano della lista in considerazione. Al primo passo, la ricerca è limitata a 2k−1 termini e vengono effettuati due confronti; ad ogni passo successivo vengono eseguiti due confronti su di una lista che è grande la metà di quella del passo precedente. Alla fine del ciclo while vengono eseguiti due ulteriori confronti e quindi complessivamente saranno stati eseguiti al più 2k+2 confronti, ovvero 2 ⌊log n⌋ + 2 confronti. Quindi, l’algoritmo di ricerca binaria richiede al più O(log n) confronti, e da ciò segue che tale algoritmo, a parità di input, è molto più efficiente dell’algoritmo di ricerca lineare. In Tabella 2.1 è riportata la terminologia comunemente usata per indicare la complessità temporale degli algoritmi. La stima Big-O permette di valutare come il tempo necessario per risolvere un problema cambi in funzione della dimensione dell’input. Tale stima però non fornisce indicazioni sul tempo realmente necessario ad un computer per completare l’elaborazione perchè non possiamo individuare un valore limite senza aver ricavato le costanti C e k nell’equazione 2.1 ed inoltre perchè è difficile stimare il tempo richiesto per completare una singola operazione. Comunque, possiamo tentare di fornire una misura riconducendoci a delle stime sui tempi di operazione sui bit2 . Cosı̀ facendo, possiamo 2 Nel nostro caso si è assunto che il tempo di elaborazione di una operazione base su bit, eseguita su un computer ad alte prestazioni, sia di 10−9 secondi. 29 Algoritmi Dimensione del problema n 10 102 103 104 105 106 Numero di operazioni su bit eseguite log n 3 · 10−9 sec 7 · 10−9 sec 1 · 10−8 sec 1.3 · 10−8 sec 1.7 · 10−8 sec 7 · 10−8 sec n 10 10−7 10−6 10−5 10−4 10−3 −8 sec sec sec sec sec sec n log n 3 · 10−8 sec 7 · 10−7 sec 1 · 10−5 sec 1 · 10−4 sec 2 · 10−2 sec 3 · 10−2 sec n2 2n 10 sec 10−5 sec 10−3 sec 10−1 sec 10 sec 17 min 10 sec 4 · 1013 anni ∗ ∗ ∗ ∗ −7 −6 n! 3 · 10−3 sec ∗ ∗ ∗ ∗ ∗ Tabella 2.2: Tempo di calcolo usato dagli algoritmi ottenere la Tabella 2.2 che riporta i tempi computazionali necessari a problemi con diverse dimensioni di input, fornendo inoltre una indicazione sul numero di operazioni su bit. Gli asterischi indicano tempi maggiori di 10100 anni. La tabella riporta tempi computazionali che possono risultare impraticabili anche per istanze piccole. Ci si potrebbe chiedere quale vantaggio si avrebbe con l’aumento delle prestazioni degli elaboratori. Dall’esempio seguente è facile convincersi che questa possibilità non ha riscontro nella realtà. Supponiamo di considerare due algoritmi, uno di complessità polinomiale O(n2 ) e l’altro di complessità esponenziale O(2n ). Consideriamo ora un elaboratore che abbia velocità v1 e che ci permetta di risolvere in una data unità di tempo una istanza di dimensione n1 . Immaginiamo ora di poter disporre di un elaboratore 100 volte più veloce (v2 = 100 · v1 ); considerando la complessità dell’algoritmo, si può affermare che esiste una proporzionalità pari a n22 /n21 = v2 /v1 = 100 e quindi, con un rapido conto, si ottiene che è possibile risolvere nello stesso tempo istanze con n2 = 10 · n1 , cioè 10 volte più grandi. Applicando lo stesso ragionamento per l’algoritmo di complessità O(2n ), si ottiene, con un elaboratore 100 volte più veloce, che 2n2 /2n1 = v2 /v1 = 100, ovvero che n2 = n1 + log 100 ≈ n1 + 7, cioè posso risolvere istanze con solo 7 nodi in più! Questo esempio ci mostra come il miglioramento delle capacità di elaborazione ha purtroppo solo un impatto marginale nella efficienza degli algoritmi con complessità esponenziale. 30 Algoritmi 2.5 Esercizi Es. 2.5.1 Mostrare che l’algoritmo dell’Esempio 2.2.1 rispetta le Proprietà 2.2.1 Es. 2.5.2 Fornire una stima Big-O della seguente funzione: f (n) = n2 + n log n log n!. Es. 2.5.3 Fornire una stima Big-O della seguente funzione: f (n) = 13 + 23 + . . . + n3 . Es. 2.5.4 Fornire una stima Big-O della seguente funzione: f (n) = √ 1+ √ 2+...+ √ n. Es. 2.5.5 Fornire una stima Big-O della seguente funzione: f (n) = log 2 + log 3 + . . . + log n. Es. 2.5.6 Dare una stima Big-O per f (n) = n2 (n log(n!) + n log n). Es. 2.5.7 Dare una stima Big-O per f (n) = (log n)2 + log(n2 ). Es. 2.5.8 Determinare la complessità computazionale associabile al seguente segmento di codice (n << m): while (j < m) do begin for i := 1 to m do if a[i] < j then a[i] = j; for i := 1 to n do if a[i] < j then a[i] = j; j = j + 1; end Es. 2.5.9 Determinare la complessità computazionale associabile al seguente segmento di codice (m << n): for i := 1 to m do begin Algoritmi if a[i] < j then a[i] = j; for i := 1 to n do if a[i] < j then a[i] = j; j = j + 1; end Es. 2.5.10 31 Capitolo 3 Proprietà e strutture dei grafi 3.1 Strutture dati per la rappresentazione di grafi La performance di un algoritmo su grafo dipende non solo dall’algoritmo stesso, cosı̀ come è stato descritto nella sezione precedente, ma anche dal modo con il quale il grafo viene memorizzato e da come vengono gestiti gli aggiornamenti ed i risultati parziali. Tipicamente, nella rappresentazione di un grafo occorre memorizzare due tipi di informazioni: la topologia dei nodi e degli archi, quindi eventuali grandezze da associare ai nodi e agli archi stessi(queste informazioni saranno necessarie soprattutto per gli algoritmi che approfondiremo nelle prossime sezioni). Prima di introdurre le diverse tipologie di rappresentazione, si vuole estendere e generalizzare il concetto di grafo finora esposto al caso in cui gli archi siano orientati. La definizione è simile alla Definizione 1.1.1: Definizione 3.1.1 Un grafo orientato G è una tripla costituita da un insieme di vertici V (G) (detti anche nodi), un insieme di archi orientati E(G) e una relazione, detta relazione di incidenza, che associa un vertice ad un altro. Risulta chiaro allora che nel caso di grafi orientati non è più corretto affermare che e = ab è la stessa cosa di scrivere e = ba, perché la relazione di incidenza è vera solo in un verso. In ogni caso però, tutte le definizioni di walk, trail, path, ecc che abbiamo 32 33 Proprietà e strutture dei grafi dato nel Capitolo 1, si estendono immediatamente al caso dei grafi orientati avendo cura di considerare in modo stringente l’orientamento nelle varie tipologie di cammini (e cicli). 2 2 4 d a a c 1 4 d g e h b c 1 g e h b 3 f 5 Figura 3.1: Grafo non orientato 3 f 5 Figura 3.2: Grafo orientato Le rappresentazioni dei grafi che studieremo sono la matrice di incidenza, la matrice di adiacenza e la lista di adiacenza. Matrice di incidenza La rappresentazione attraverso la matrice di incidenza di un grafo non orientato G di n nodi ed m archi avviene attraverso una matrice A di dimensione n × m che contiene una riga per ogni nodo del grafo ed una colonna per ogni arco. La colonna corrispondente all’arco (i, j) ha soltanto due elementi diversi da zero in corrispondenza dei nodi i e j. Prendendo il grafo disegnato in Figura 3.1, la sua matrice di incidenza corrispondente è: 1 1 A = 0 0 0 1 0 0 0 0 0 0 0 1 1 0 0 0 0 1 1 0 1 1 0 0 0 0 1 1 0 1 1 0 0 0 0 1 1 1 La matrice di incidenza ha una struttura tipica per cui solo 2m componenti, delle n×m, sono diverse da zero ed uguali ad uno. È da notare che la somma degli elementi diversi da zero in una riga eguaglia il grado del nodo corrispondente, ovvero: 34 Proprietà e strutture dei grafi d(vi ) = m X aij j=1 dove chiaramente aij è l’elemento della riga i-esima e colonna j-esima. Nel caso di un grafo orientato la matrice di incidenza si modifica considerando che per l’h-esimo arco (i, j), nella colonna h corrispondente, l’elemento aih è uguale ad 1 mentre l’elemento ajh è uguale a −1. Di conseguenza, per il grafo in Figura 3.2 la matrice è: 1 1 0 0 0 0 0 0 −1 0 −1 1 0 0 0 0 A = 0 −1 1 0 1 1 0 0 0 0 0 −1 −1 0 −1 1 0 0 0 0 0 −1 1 −1 La rappresentazione di un grafo tramite la matrice di incidenza è chiaramente non efficiente a causa dello spazio necessario per la sua memorizzazione su un elaboratore, soprattutto al crescere di n e della densità. Nonostante ciò, questa rappresentazione è di grande importanza perchè come vedremo nelle prossime sezioni, oltre a possedere interessanti proprietà teoriche, essa coincide con la matrice dei coefficienti della formulazione dei problemi di flusso a costo minimo. Matrice di adiacenza La rappresentazione attraverso la matrice di adiacenza di un grafo non orientato G di n nodi ed m archi avviene attraverso una matrice A di dimensione n × n dove ogni riga ed ogni colonna corrispondono ai nodi e gli elementi della matrice aij sono o 0, se non esiste l’arco (i, j) corrispondente, oppure sono uguali alla somma del numero di archi che uniscono i nodi (i, j). Quindi, se riprendiamo l’esempio in Figura 3.1, la sua matrice di adiacenza è: 35 Proprietà e strutture dei grafi 0 1 A = 1 0 0 1 1 0 0 1 1 1 0 1 1 1 0 0 1 2 0 0 1 2 0 La matrice di adiacenza per i grafi non orientati è simmetrica ed ha n2 elementi. Conseguentemente, questa rappresentazione è efficiente solo se il grafo è significativamente denso, mentre per grafi sparsi si ha un notevole spreco di memoria. Analizzando la matrice di adiacenza, si può contare il numero di archi incidenti sul nodo i semplicemente sommando gli elementi della riga i-esima (quindi d(v5 ) = 3, pari alla somma del contributo dell’arco adiacente al nodo 3 e dei due archi adiacenti al nodo 4). Analogamente, il numero di archi incidenti su un nodo j si ottiene sommando gli elementi della colonna j-esima. Da questo ne consegue che la somma di tutti gli elementi diversi da zero della matrice di adiacenza è uguale a 2m. Nel caso di grafi orientati, invece, la matrice di adiacenza perdela sua caratteristica di simmetria e, per il grafo in Figura 3.2, diventa: 0 0 A = 0 0 0 1 1 0 0 0 1 1 0 1 0 0 0 0 0 1 0 0 1 1 0 Nella matrice di adiacenza nel caso di grafi orientati è possibile contare il numero di archi uscenti da un nodo i (tale insieme è chiamato anche stella uscente) sommando gli elementi della riga corrispondente, mentre il numero di archi entranti in un nodo i (tale insieme è chiamato anche stella entrante) si ottiene sommando gli elementi della colonna corrispondente. Da questo ne consegue che gli elementi diversi da zero sono uguali a m. Proprietà e strutture dei grafi 36 Lista di adiacenza La lista di adiacenza dei nodi A(i) di un grafo G con n nodi, è un vettore di dimensione n nel quale ogni cella i punta una lista che contiene l’insieme dei nodi j tali per cui (i, j) ∈ E(G). In Figura 3.3 è rappresentata la lista di adiacenza del grafo in Figura 3.2. La lista di adiacenza di un grafo è molto utile quando, agli archi ed ai nodi, assoceremo dei numeri che ne rappresenteranno il costo, la lunghezza, ecc, come illustreremo nei prossimi Capitoli. Infatti, mediante questa rappresentazione vedremo che sarà sufficiente aumentare, per ogni cella puntata dal nodo, i campi della lista corrispondente per memorizzare tutte le informazioni necessarie. Figura 3.3: Lista di adiacenza per il grafo di Figura 3.2. 3.2 Isomorfismo tra grafi Osservando la matrice di adiacenza per i grafi non orientati, si può notare come si introduca implicitamente un ordinamento dei vertici per riga. In realtà, quello che si vuole è ottenere delle proprietà che non siano funzione di tale ordinamento. Intuitivamente, si può dire che si vorrebbero trovare due diverse rappresentazioni matriciali del grafo G tali per cui siano preservate le relazioni di adiacenza. Definizione 3.2.1 Due grafi G = (V, E) e G′ = (V ′ , E ′ ) si dicono isomorfi, e si scrive G∼ = G′ , se esiste una corrispondenza biunivoca tra i due insiemi di vertici, cioè se esiste una biiezione Φ : V → V ′ tale che xy ∈ E ⇔ φ(x)φ(y) ∈ E ′ . 37 Proprietà e strutture dei grafi Per esempio, se prendiamo le matrici di adiacenza dei due grafi G e G′ , rappresentati in Figura 3.4, possiamo notare che è facile individuare una funzione biiettiva che mette in corrispondenza i vertici dei due grafi e che le due matrici si trasformano l’una nell’altra mediante permutazioni delle righe e delle colonne. Al lettore è lasciato per esercizio di trascrivere le due matrici e di trovare la funzione Φ che permette di passare da una matrice all’altra. a 1 2 3 4 G′ G d b c Figura 3.4: Esempio di due grafi isomorfi Dati due grafi generici G e H ci possiamo chiedere ora come verificare che siano tra di loro isomorfi. Alcune condizioni necessarie possono essere facilmente individuate; per esempio i due grafi devono avere stessi ordine e dimensione, oppure l’immagine di un vertice i ∈ G deve avere in H lo stesso grado dell’immagine i ∈ H in G. In generale però lo studio esaustivo delle permutazioni delle matrici di adiacenza di due grafi di ordine n non costituisce un algoritmo molto efficiente, perchè questo avrebbe una complessità computazionale di O(n!). Dato il grande interesse che il problema dell’isomorfismo tra grafi riveste nelle applicazioni pratiche, sono stati proposti molti algoritmi per tentare di risolverlo, ma esso rimane ancora aperto, nel senso che non è ancora stato proposto un algoritmo efficiente ne si è riuscito a dimostrare che tale algoritmo possa esistere oppure no [6]. Esempio 3.2.1 Siano assegnati i due grafi in Figura 3.5. Dimostrare che sono tra loro isomorfi. Sia definita la funzione Φ : V (G) → V (H) tale che φ(w) = a, φ(x) = c, φ(y) = b e φ(z) = d. Per mostrare che Φ è un isomorfismo occorre analizzare se tali funzioni 38 Proprietà e strutture dei grafi w z G c d b a H x y Figura 3.5: Due grafi isomorfi preservino le adiacenze, e questo può essere testato riscrivendo la matrice A(G) in modo che permutando righe e colonne si ottenga la matrice A(H). Nel nostro caso si può notare come, permutando tra di loro la seconda e la terza colonna di A(G) e poi la terza e la quarta (e la stessa cosa si fa per le righe), si ottiene la matrice del grafo H, dimostrando quindi l’isomorfismo tra i due grafi. Per esteso: 0 1 0 0 0 1 0 1 0 0 A(G) = → 0 1 0 1 0 0 0 1 0 1 0 0 1 0 0 0 1 1 → 0 1 0 0 1 0 0 1 0 0 1 0 1 1 = A(H) 1 0 0 1 0 0 Classi di isomorfismo Il concetto di isomorfismo visto come “G e H sono isomorfi” ha un valore limitato se non lo estendiamo all’applicazione su insiemi di grafi che siano tra loro isomorfi. Ricordando che una relazione su un insieme I è una collezione di coppie ordinate di suoi elementi e una relazione di equivalenza è una relazione riflessiva (a ∼ = a), simmetrica (a ∼ =b⇔b∼ = a) e transitiva (a ∼ = b, b ∼ = c, ⇒ a ∼ = c), possiamo affermare che: Proposizione 3.2.1 La relazione di isomorfismo è una relazione di equivalenza. Dimostrazione: Lasciata per esercizio al lettore. 39 Proprietà e strutture dei grafi Una relazione di equivalenza partiziona un insieme in classi di equivalenza e quindi due elementi appartengono alla stessa classe se e solo se ricadono nella stessa classe di equivalenza. Definizione 3.2.2 Una classe di isomorfismo di grafi è una classe di equivalenza di grafi sotto una relazione di isomorfismo. Per esempio, i grafi completi Kn costituiscono delle classi di isomorfismo di grafi perchè, prese due a due, rispettano le proprietà delle relazioni di equivalenza. Un’altra importante classe di isomorfismo è costituita dai grafi bipartiti completi , ovvero quei grafi semplici bipartiti tali che due vertici sono adiacenti se e soltanto se appartengono a partizioni diverse. Se gli insiemi partizioni hanno dimensione p e q, il grafo bipartito completo è indicato come Kp,q . K2,3 K3,3 Figura 3.6: I grafi bipartiti completi K2,3 e K3,3 . 3.3 Connessione, cicli e grafi bipartiti Nelle prossime definizioni ci dirigeremo verso le classi di equivalenza delle relazioni di connessione limitandoci al caso dei grafi non orientati. Riprendendo i concetti definiti alla fine della Sezione 1.2.4, diremo che un sottografo connesso di G è massimale se è connesso e non è contenuto in nessun altro sottografo connesso di G. Definizione 3.3.1 Dato un grafo G, le sue componenti connesse sono i suoi sottografi connessi massimali. In Figura 3.7 è rappresentato un grafo con quattro componenti connesse, {a, b}, {c, d, e, f }, {g} e {h, i, j, }. Come si può notare, la componente connessa {g} è costituita da un solo nodo e viene chiamata anche componente triviale. 40 Proprietà e strutture dei grafi a c b e d f g i j h Figura 3.7: Un grafo con quattro componenti connesse. Definizione 3.3.2 Un cut-vertex è un vertice la cui rimozione aumenta il numero di componenti connesse. Un bridge è un arco la cui rimozione aumenta il numero di componenti connesse. Quindi riprendendo la Figura 3.7, un cut-vertex è per esempio il nodo i, mentre l’arco ij è un bridge (infatti, in entrambi i casi passo da 4 a 5 componenti connesse). Proposizione 3.3.1 Ogni grafo G con n vertici ed m archi ha almeno n − m componenti connesse. Dimostrazione: Supponiamo sia assegnato un grafo con n nodi e nessun arco. In questo caso ho esattamente n componenti connesse. Per la Definizione 3.3.2, aggiungendo un arco diminuisco di uno il numero di componenti connesse, quindi dopo aver aggiunto m archi il numero di componenti è diminuito a n − m. Esempio 3.3.1 Sia G un grafo con n vertici tale che ogni suo vertice abbia grado almeno (n − 1)/2. Mostrare che G è connesso. Per dimostrare la connessione, ipotizziamo per assurdo che ci siano due nodi x ed y di G appartenenti a due diverse componente connesse ed aventi grado ciascuno almeno (n−1)/2. Sia la componente di x quella in Figura 3.8. Allora il numero di nodi di tale componente connessa deve essere 1 + (n − 1)/2 = (n + 1)/2. Tale numero di nodi si ottiene ovviamente anche nell’altra componente connessa. Ora, per ottenere il numero di nodi del grafo di partenza basta sommare il numero di nodi delle due componenti connesse, ma cosı̀ facendo ci accorgiamo che otteniamo (n + 41 Proprietà e strutture dei grafi 1)/2+(n+1)/2 = n+1, che eccede il numero di nodi di partenza di G, mostrando l’assurdo. x n−1 2 Figura 3.8: Componente connessa di x Definizione 3.3.3 Se a e b sono due vertici appartenti alla stessa componente connessa, allora esiste un path di lunghezza minima d(a, b) chiamato distanza. Lemma 3.3.2 Ogni walk chiuso W di lunghezza dispari contiene un ciclo dispari. Dimostrazione: La dimostrazione è per induzione. Per l = 1 il lemma è ovvio; per l > 1 si ponga l’ipotesi induttiva affermando che il lemma è vero per walk chiusi più corti di W . In questo caso si possono presentare due possibilità: se non ho walk chiusi ripetuti allora ho già cicli dispari ed il lemma è dimostrato; altrimenti, se ho nodi ripetuti (per esempio il nodo i, come in Figura 3.9) allora posso spezzare W in due walk chiusi, uno di lunghezza pari e l’altro di lunghezza dispari. Ma per il walk chiuso di lunghezza dispari vale l’ipotesi induttiva e quindi deve contenere un ciclo di lunghezza dispari. Pari Dispari i Figura 3.9: Un circuito con un nodo ripetuto. Le definizioni ed i teoremi che seguono caratterizzano ed esaminano la connessione nei grafi. Proprietà e strutture dei grafi 42 Esempio 3.3.2 Sia G un grafo con n ≥ 2 vertici, n − 1 archi e senza vertici isolati. Mostrare che G contiene almeno due vertici di grado 1. Per il Lemma 1.1.1, con d(vi ) = 1. P v d(vi ) = 2m = 2n − 2, quindi ci devono essere almeno due vertici Definizione 3.3.4 Un grafo G si dice aciclico se non contiene cicli. Lemma 3.3.3 Ogni grafo connesso G di ordine n contiene almeno n − 1 archi. Dimostrazione: Dimostriamo il lemma per induzione. Per n = 1 il lemma è ovvio. Per n ≥ 2, considero un vertice arbitrario v ∈ G e considero il grafo H = G \ v, non necessariamente connesso, con Fi componenti connesse aventi ciascuna ni vertici, ovvero n1 + n2 + . . . + nk = ni − 1. Per ipotesi induttiva, affermo che il sottografo di H indotto su Fi ha almeno n − 1 archi. Ora, osservo che il vertice v deve essere connesso ad ognuna delle k componenti Fi con almeno un arco; quindi G contiene almeno (n1 − 1) + (n2 − 1) + . . . + (nk − 1) + k = n − 1 − k + k = n − 1 archi. Lemma 3.3.4 Ogni grafo aciclico ha, al più, n − 1 archi. Dimostrazione: Dimostriamo il lemma per induzione. Per n = 1 il lemma è ovvio. Per n ≥ 2, prendo un arco xy ∈ G. Quindi il grafo H = G \ xy ha esattamente una componente connessa in più di G (questo perché, per ipotesi, il grafo è aciclico e quindi tra x e y non può esserci un altro path), ovvero, H può essere decomposto in grafi connessi aciclici H1 , H2 , . . . , Hk . Per ipotesi induttiva, affermo che ogni grafo Hi contiene al più ni − 1 archi, dove ni è il numero di nodi di ogni componente connessa, i = 1, . . . , k. Allora G ha al più (n1 −1)+(n2 −1)+. . .+(nk −1)+1 = (n1 +n2 +. . .+nk )−(k−1) ≤ n−1 archi. Teorema 3.3.5 Sia G un grafo di ordine n. Allora due qualunque delle seguenti affermazioni implicano la terza: Proprietà e strutture dei grafi 43 a) G è connesso; b) G è aciclico; c) G ha n − 1 archi. Dimostrazione: a, b ⇒ c Se G è connesso ed aciclico, allora per il Lemma 3.3.3 e per il Lemma 3.3.4 il grafo ha esattamente n − 1 archi; a, c ⇒ b Supponiamo G connesso e con n−1 archi e che, per assurdo, abbia un ciclo C. Allora H = G \ xy, con xy ∈ C, è un grafo connesso di n vertici e n − 2 archi e questo contraddice il Lemma 3.3.3; b, c ⇒ a Supponiamo che G sia aciclico ed abbia n−1 archi. Per il Lemma 3.3.4 non ho vertici isolati (altrimenti avrei n − 1 nodi e n − 1 archi), quindi per il Lemma 1.1.1 il grafo G ha almeno un vertice di grado uno ed abbiamo che G \ v è un grafo aciclico con n − 1 vertici e n − 2 archi. Per induzione, G \ v è connesso e quindi anche G. Grafi bipartiti L’enunciato del Lemma 3.3.2 ci tornerà utile nella dimostrazione del prossimo teorema che ci permetterà di riconoscere la bipartizione di un grafo. Ricordiamo dalla Sezione 1.2.2 che un grafo G si dice bipartito se l’insieme dei suoi nodi V (G) è l’unione di due insiemi indipendenti disgiunti V1 e V2 chiamati insiemi partizione ed ogni suo arco va da V1 a V2 . Teorema 3.3.6 Un grafo G è bipartito se e soltanto se non contiene cicli dispari. Dimostrazione: Necessità. Sia G un grafo bipartito. Ogni walk alterna i propri archi tra le due partizioni di nodi per cui ogni circuito deve avere un numero di archi pari, quindi G non ha cicli dispari. 44 Proprietà e strutture dei grafi Sufficienza. Sia G un grafo senza cicli dispari. Poiché un grafo è bipartito solo e soltanto se lo sono le sue componenti, possiamo assumere allora il grafo G connesso. Si prenda un vertice x ∈ V e poniamo V1 = {y : d(x, y) sia pari} e V2 = V \ V1 . Si può notare allora che non ci sono archi che uniscono due nodi in V1 , altrimenti G dovrebbe contenere un ciclo dispari. Quindi G è bipartito. 3.4 Alberi I grafi che soddisfano le condizioni del Teorema 3.3.5 costituiscono una classe di grafi molto particolari chiamati alberi . Definizione 3.4.1 Un albero è un grafo connesso che non contiene cicli. Ogni vertice di grado uno di un albero è chiamata foglia. Una foresta è un grafo le cui componenti connesse sono alberi. Proposizione 3.4.1 Per un albero sono vere le seguenti affermazioni: 1. Un albero con n nodi contiene n−1 archi; una foresta di ordine n e con k componenti contiene n − k archi. 2. Un albero ha almeno due foglie. 3. Ogni coppia di nodi di un albero è connessa da un solo path. Figura 3.10: Una foresta Figura 3.11: Un albero Le definizioni e le preposizioni date possono essere riassunte nel seguente teorema: 45 Proprietà e strutture dei grafi Teorema 3.4.2 Dato un grafo G, le seguenti affermazioni sono equivalenti: 1. G è un albero. 2. G è connesso e se xy ∈ G, allora G − xy è sconnesso, ovvero ogni arco è un bridge. 3. G è aciclico e se x e y sono due vertici non adiacenti di G, allora G + xy contiene un ciclo. Il seguente corollario ci fornisce una proprietà che useremo nel seguito: Corollario 3.4.3 Ogni grafo connesso G contiene un albero ricoprente, ovvero un albero che contiene tutti i nodi del grafo (denominato anche spanning tree). Dimostrazione: È sufficiente costruire un sottografo di G connesso e aciclico di n − 1 archi e che ricopre ogni nodo. Riprenderemo lo studio di questa classe di grafi nella Sezione 5.3 dove verranno proposti degli algoritmi per la ricerca di alberi ricoprenti. 3.5 Cammini Euleriani In questo paragrafo risolveremo il Problema dei Ponti di Könisberg introdotto nel Capitolo 1 nel caso di grafi arbitrari. Quanto diremo sui cammini euleriani si estende anche ai grafi non semplici, ovvero ai grafi che possiedono archi paralleli o cappi e che in letteratura vengono chiamati anche multigrafi. Definizione 3.5.1 Un trail euleriano (tour di Eulero) è un trail (trail chiuso) che contiene ogni arco di G esattamente una volta. Definizione 3.5.2 Un grafo è chiamato Euleriano se contiene un tour di Eulero. Enunciamo ora il Teorema di Eulero (1736) che caratterizza i grafi Euleriani. Teorema 3.5.1 Sia G un multigrafo connesso. equivalenti: Allora le seguenti affermazioni sono Proprietà e strutture dei grafi 46 1. Il grafo G è euleriano. 2. Ogni vertice di G ha grado pari. 3. Gli archi di G possono essere partizionati in cicli. Dimostrazione: (1) ⇒ (2) Se G è euleriano, allora ogni vertice aggiunge un contributo di 2 al ciclo. Dato che ogni arco compare esattamente una volta, ogni vertice v deve avere grado pari (2) ⇒ (3) Supponiamo che G abbia n vertici. Dato che il grafo è connesso, ha almeno n−1 archi per il Lemma 3.3.3, mentre per l’Esempio 3.3.2 deve avere almeno n archi; quindi per il Lemma 3.3.4 c’è un ciclo C in G. Rimuoviamo ora C, ottenendo un nuovo grafo H nel quale ogni vertice ha ancora grado pari. Considerando separatamente le componenti connesse di H, possiamo ripetere il ragionamento per ognuna delle componenti e quindi, proseguendo, ci arrestiamo quando abbiamo terminato la decomposizione. Quindi gli archi di G possono essere partizionati in cicli. (3) ⇒ (1) Sia C uno dei cicli della partizione di E(G) in cicli. Se C è già euleriano, abbiamo la tesi. Altrimenti esiste un altro ciclo C ′ che ha un vertice v in comune con C. Possiamo allora, senza perdita di generalità, considerare v come vertice di partenza e di fine di entrambi i cicli C e C ′ in modo che CC ′ sia ancora un tour. Continuando con la stessa procedura otteniamo un tour d’Eulero Esempio 3.5.1 Supponiamo sia assegnata la porzione di scacchiera cosı̀ come disegnato nell’immagine a sinistra della Figura 3.12. Considerando che nelle quattro caselle bianche agli angoli ci sono i quattro cavalli, sopra i due bianchi (chiamati CB) e sotto i due neri (chiamati CN), è possibile, usando le mosse ammesse per i cavalli negli scacchi, invertire le posizioni in modo che dopo un certo numero di mosse i due cavalli bianchi siano nelle caselle inferiori e quelli neri nelle caselle superiori? 47 Proprietà e strutture dei grafi Per risolvere questo problema basta modellare la porzione di scacchiera con un grafo dove i nodi sono le caselle ed un arco esiste tra due nodi se esiste un trasferimento lecito tra le due caselle. CB CB CN CN Figura 3.12: Come spostare i quattro cavalli. Allora, dato il grafo (rappresentato a destra in Figura 3.12), è sufficiente individuare un cammino lungo il grafo in modo che i quattro cavalli, spostandosi, arrivino nella posizione finale richiesta. È facile convincersi che tale percorso esiste se c’è un ciclo euleriano sul grafo, cosa immediatamente verificabile osservando che i nodi hanno tutti grado pari. 3.6 Cammini Hamiltoniani 3.7 Esercizi Es. 3.7.1 Date la seguente matrice di incidenza, disegnare i grafi corrispondenti: Es. 3.7.2 Date la seguente matrice di adiacenza, disegnare i grafi corrispondenti: 0 0 0 0 A1 = 0 0 0 0 1 1 1 0 0 0 0 0 1 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 0 1 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 1 0 A2 = 0 0 1 0 1 0 0 0 1 0 0 0 0 0 1 1 1 0 0 0 0 0 1 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 1 1 1 0 0 0 1 0 0 0 0 0 0 1 Es. 3.7.3 Mostrare che P4 è isomorfo al suo complemento. 0 0 0 0 0 0 0 0 0 1 1 0 0 1 0 0 Proprietà e strutture dei grafi 48 Es. 3.7.4 Disegnare tutte le classi di isomorfismo costituite da grafi con n = 5 e m = 5, anche non connessi. Es. 3.7.5 Si disegnino tutte le classi di isomorfismo formate da spanning tree di K3,3 . Quante sono in tutto? Es. 3.7.6 Si disegnino tutti le possibili classi di grafi di 4 nodi non isomorfi. Es. 3.7.7 Si disegnino tutti le possibili classi di grafi di 6 nodi non isomorfi. Es. 3.7.8 Quanti sono gli alberi che posso ottenere, a meno di isomorfismi, legando con due archi i due vertici n − 1 e n al path 1, 2, . . . , n − 2 ? Es. 3.7.9 A partire da K4 , togliendo quattro archi, quanti grafi connessi posso ottenere? E quanti sconnessi? Es. 3.7.10 Dire quanti sono gli alberi con 4 vertici. Es. 3.7.11 Sia assegnato un albero G con n vertici aventi grado pari a: d(v1 ) = d(v2 ) = d(v3 ) = 1, d(vn ) = 3 e d(v4) = d(v5) = . . . = d(vn−1 ) = 2 e con l’arco (2, n) ∈ E(G). Quanti alberi diversi si possono ottenere legando gli archi in modo da soddisfare i dati imposti? Es. 3.7.12 Sia assegnato il grafo in figura, composto da un ciclo di n1 nodi, un ciclo di n2 nodi e un terzo ciclo da n3 nodi, tutti e tre uniti in un solo vertice. In quanti modi posso trovare un albero togliendo contemporaneamente tre archi, uno per ogni ciclo? (N.B. I nodi dei cicli nel disegno sono solo indicativi, si deve considerare il caso generale). Proprietà e strutture dei grafi 49 Capitolo 4 Grafi Planari Per facilitare la trattazione dei grafi planari iniziamo dal seguente problema: tre acerrimi nemici hanno tre case vicine ed hanno contemporaneamente la necessità di doversi allacciare le loro abitazioni alle forniture di energia elettrica, gas ed acqua. Per evitare qualsiasi discussione esigono che nessuna delle connessioni di ognuno di loro si intersechi in qualunque punto con quelle degli altri due. La situazione è rappresentata in Figura 4.1. Il problema posto può essere modellato mediante un grafo bipartito completo K3,3 (vedi Figura 4.2) e quindi la domanda che ci vogliamo porre è la seguente: è possibile disegnare K3,3 senza intersezioni tra gli archi? In questa sezione ci occuperemo della trattazione dei grafi che possono essere disegnati su di un piano senza intersezioni tra archi e dimostreremo che il problema dei tre nemici non ha soluzione. elettricità gas acqua Figura 4.1: Il problema dei tre nemici. 50 51 Grafi Planari elettricità gas acqua Figura 4.2: Connessioni per il problema dei tre nemici. 4.1 Grafi sul piano I concetti che verranno esposti si basano sulla semplice osservazione che ogni curva chiusa disposta su di un piano divide il piano stesso in due regioni distinte, ovvero la regione interna alla curva e la regione esterna alla curva. Questa osservazione elementare ha senso se ci limitiamo ad una trattazione semplice della Teoria dei Grafi, mentre, se volessimo fornire maggiori dettagli, dovremmo introdurre concetti di topologia che esulano dagli scopi di queste note. Il lettore che volesse approfondire questi argomenti può trovare una trattazione più approfondita in [5] e [13]. Proposizione 4.1.1 K5 e K3,3 non possono essere disegnate sul piano senza intersezioni tra archi. Dimostrazione: Consideriamo un disegno di K5 sul piano come in Figura 4.3 e consideriamo un ciclo ricoprente C. Se non ci sono intersezioni, allora C è disegnato come una curva chiusa e le corde di C possono essere disegnate fuori e dentro tale curva. Due corde sono in conflitto se i loro estremi in C sono alternati e se ciò accade è possibile disegnarne una internamente a C ed una esternamente a C. Se il ciclo è di lunghezza cinque, due corde possono essere tracciate internamente e due esternamente, ma dato che ci sono cinque corde se ne deduce che è impossibile completare il disegno. 52 Grafi Planari K5 K3,3 Figura 4.3: K5 e K3,3 . Analogamente, nel disegno di K3,3 della Figura 4.3 ci sono tre corde in mutuo conflitto nel ciclo esterno di lunghezza sei e quindi la corda rimanente rende impossibile il completamento del disegno. Definizione 4.1.1 Il disegno di un grafo G(V, E) è una funzione f definita su V ∪ E tale che f : v ∈ V → f (v), e ∈ E → f (e). Un punto f (e) ∩ f (e′ ) è chiamato intersezione. Quindi, per disegno di un grafo intendiamo una funzione che assegna ad ogni vertice v un punto f (v) del piano ed assegna ad ogni arco e di vertici u e v una curva f (e). Si può osservare che possiamo utilizzare G come nomenclatura sia per un grafo che per il suo disegno; questo perché le relazioni di adiacenza sono chiaramente rispettate e, quindi, un disegno del grafo G può essere visto come un membro della classe di isomorfismo contente G. Dato un disegno di un grafo G, possiamo pensare di spostare gli archi sul piano in modo da assicurarci che non esistano tre archi che abbiano un punto interno in comune, che un arco contiene solo i vertici che costituiscono i suoi estremi e che non ci sono archi tra di loro tangenti. Nel seguito considereremo solo disegni con tali proprietà. Definizione 4.1.2 Un grafo G si definisce planare se ha un disegno senza intersezioni. Tale disegno è un embedding planare di G. Un grafo piano è un particolare embedding planare di un grafo planare. 53 Grafi Planari Il termine embedding1 planare ribadisce il fatto che il disegno di un grafo G sul piano è composto da punti del piano stesso, definiti dalla funzione f . Di conseguenza, allo stesso grafo G possono corrispondere differenti funzioni che definiscono embedding diversi. Un embedding planare divide il piano in parti che saranno ora oggetto del nostro studio. Definizione 4.1.3 Un insieme aperto nel piano è un insieme A ∈ R2 tale che ∀p ∈ A, ∃r : ||p − r|| < ǫ, ǫ > 0, ⇒ r ∈ A. Definizione 4.1.4 Una regione è un insieme aperto A che contiene una poligonale di estremi u e v, ∀u, v ∈ A. Definizione 4.1.5 Le facce Fi di un grafo planare sono le regioni massimali del piano che contengono punti del piano che non sono interessati dall’embedding planare. Sulla base di questa definizione, un grafo planare partiziona il piano in un numero di regioni mutuamente disgiunte che sono quelle racchiuse all’interno delle poligonali dell’embedding, più la faccia esterna, ovvero la faccia illimitata costituita dalla restante parte di piano. F2 F3 F4 F1 Figura 4.4: La facce di un grafo planare. Per esempio, considerando l’embedding planare di Figura 4.4, questo divide il piano in quattro regioni costituite dall’insiemi aperti con frontiera gli archi di G e che definiscono le facce triangolari F1 e F2 , la faccia quadrata F3 ed infine la faccia esterna F4 . 4.2 Grafi duali Da ogni grafo planare G è possibile costruire un grafo planare duale G∗ ad esso legato. 1 In inglese il termine embedding significa incastrato, incastonato. 54 Grafi Planari Definizione 4.2.1 Il grafo duale G∗ (V ∗ , E ∗ ) di un grafo piano G(V, E) è un grafo piano che ha i vertici corrispondenti con le facce di G, mentre i suoi archi corrispondono a quelli di G nel seguente modo: se e = (u, v) è un arco di G con facce X da un lato e Y dall’altro, allora i vertici dell’arco duale e∗ ∈ E ∗ sono x ed y che rappresentano le facce X ed Y di G. Il grado del vertice x ∈ V ∗ è uguale al numero di archi frontiera della faccia X di G. x e∗ v e u y Figura 4.5: Costruzione del duale. La Figura 4.5 schematizza questo processo, mostrando la costruzione degli archi nel grafo duale. Utilizzando questo metodo, si può costruire per esempio il duale di K4 e si può notare come le sue quattro facce portino ad ottenere un duale che è ancora K4 . 4.2.1 Proprietà dei grafi planari e Formula di Eulero Nel processo di costruzione dei grafi duali può verificarsi la comparsa di loop ed archi multipli. Per esempio, la costruzione del duale (rappresentato con i vertici bianchi e archi tratteggiati) per l’embedding in Figura 4.6 porta ad avere due vertici, uno per la faccia interna triangolare ed uno per la faccia esterna, ed archi corrispondenti alle frontiere tra le diverse facce. Figura 4.6: Un esempio di grafo G e del suo duale G∗ . Grafi Planari 55 È da notare che i loop si formano in corrispondenza di un bridge dato che le facce sui due lati sono le stesse, mentre archi multipli si hanno in corrispondenza di facce che hanno più di un arco frontiera in comune. Basandoci su ragionamenti simili possiamo provare che il duale del duale (G∗ )∗ è isomorfo a G se e soltanto se G é connesso. Figura 4.7: Esempio di due embedding planari con duali non isomorfi. Definizione 4.2.2 La lunghezza l(Fi ) di una faccia Fi in un grafo G planare é pari alla lunghezza del walk chiuso che é frontiera della faccia. Per esempio nel grafo in alto nella Figura 4.7, le lunghezze delle facce sono rispettivamente 3, 6, 7, mentre nel grafo in basso le lunghezze sono 3, 4, 9. In entrambe i casi si può notare che la somma fa 16, cioè il doppio del numero degli archi. Questo suggerisce la seguente proposizione: Proposizione 4.2.1 Se l(Fi ) denota la lunghezza della faccia Fi del grafo piano G, allora P 2m(G) = l(Fi ). Dimostrazione: La lunghezza delle facce corrisponde al grado dei vertici duali relativi e, P dato che |m(G)| = |m(G∗ )|, la formula 2m(G) = l(Fi ) altro non è che una diversa forma del Lemma 1.1.1 (lemma handshaking). Grafi Planari 56 Questa proposizione evidenzia che le relazioni relative a grafi piani connessi diventano relazioni per il loro duale quando scambiamo il ruolo dei vertici e delle facce. Infatti, archi incidenti su vertici diventano archi frontiera di una faccia e viceversa; analoga cosa si verifica per il ruolo della lunghezza delle facce e grado dei vertici. Un’ulteriore relazione si può presentare nella colorazione dei vertici di G∗ in termini di G. Di fatto, gli archi di G∗ rappresentano frontiere condivise tra facce di G, quindi il numero cromatico di G∗ eguaglia il numero di colori necessari per colorare le facce di G. Dato però che il duale del duale di un grafo piano connesso è il grafo originale, allora 4 colori sono sufficienti per colorare correttamente le regioni di ogni grafo planare se e soltanto se ogni grafo planare ha numero cromatico al più 4. Continuando nella nostra caratterizzazione dei grafi planari, dimostriamo il seguente teorema. Teorema 4.2.2 Le seguenti affermazioni sono equivalenti per un grafo piano G: 1. G é bipartito; 2. ogni faccia di G ha lunghezza pari; 3. il duale G∗ è euleriano. Dimostrazione: (1) ⇒ (2) Se il grafo é bipartito, allora per il Teorema 3.3.6 non ho cicli dispari e quindi il contributo alla misura della lunghezza di una faccia è sempre pari. (2) ⇒)(1) Consideriamo il ciclo C nel grafo G di Figura 4.8. Dato che non ho intersezioni tra archi, allora C é una curva chiusa che racchiude la regione F ed inoltre tutte le regioni di G o sono completamente all’interno della regione F o completamente all’esterno; se sommo la lunghezza delle regioni contenute in F ottengo un numero pari, per ipotesi. Questa somma contiene sia archi di C, che contano una sola volta, sia ogni arco contenuto in F , contati due volte (perché insistono su due facce). Sottraendo a tale somma i contributi degli archi contenuti in F deve rimanere ancora una quantità pari; quindi C ha lunghezza pari e da ciò consegue la bipartibilità di G. 57 Grafi Planari C Figura 4.8: Il ciclo C evidenziato per la dimostrazione del teorema. (2) ⇔ (3) Il grafo duale G∗ è connesso ed il grado dei sui vertici corrisponde alla lunghezza delle facce che è pari. La tesi segue subito dal Teorema 3.5.1. Dimostriamo ora la Formula di Eulero che rappresenta uno dei più importanti risultati per i grafi planari e che mette in relazione vertici, archi e facce. Teorema 4.2.3 (Eulero, 1758) Se un grafo piano G connesso ha esattamente n vertici, m archi e f facce, allora n−m+f =2 (4.1) Dimostrazione: Dimostriamo il teorema per induzione. Per n = 1, se m = 0 allora la formula vale (1 − 0 + 1 = 2), mentre qualunque inserimento di un arco aggiunge un loop che porta ad avere un arco in più ed una faccia in più; dato che questi ultimi nella formula si elidono, questa vale qualunque sia il numero di archi. e → Figura 4.9: Contrazione dell’arco e. Per ipotesi induttiva, affermiamo che il teorema sia valido per m − 1, ∀G connesso. Allora, se il grafo è connesso, posso trovare un arco e che non è un loop. Se contraggo tale arco come in Figura 4.9 ottengo un nuovo grafo G′ con n′ = n − 1 vertici, m′ = m − 1 archi 58 Grafi Planari e f ′ = f facce. Per l’ipotesi induttiva posso scrivere che: n − m + f = (n − 1) − (m − 1) + f ′ = n′ − m′ + f ′ = 2 Il teorema di Eulero ha notevoli implicazioni nello studio dei grafi planari e ci porta a dire, per esempio, che tutti gli embedding planari di un grafo connesso G hanno lo stesso numero di facce o che, sebbene il duale dipenda dal particolare embedding scelto, il numero dei vertici del duale è invece indipendente. La formula di Eulero può essere generalizzata al caso di grafi k-connessi attraverso la formula n − m + f = k + 1. 4.2.2 Caratterizzazione dei grafi planari Un problema molto importante è, dato un grafo G qualunque, capire se questo possiede un embedding in un piano, ovvero se è planare. Data l’importanza pratica dei grafi planari, che compaiono in molte modellizzazioni nella produzione industriale e nella progettazione dei circuiti VLSI, occorre trovare un modo efficace per certificare la planarità. Un importante risultato che fornisce un test di planarità è il Teorema di Kuratowski, che in forma semplificata ha il seguente enunciato. Teorema 4.2.4 (Kuratowski, 1930) Un grafo è planare se e solo se non contiene K5 o K3,3 . La dimostrazione di questo importante risultato esula dagli scopi di queste note. Il lettore interessato potrà trovare una completa trattazione su [5]. 4.3 Esercizi Es. 4.3.1 Dato K2,4 dire se esiste un possibile embedding planare. Se si, disegnarlo e costruire il suo grafo duale. Grafi Planari 59 Es. 4.3.2 Dato il grafo in figura, disegnare il suo grafo duale. Dire quante sono le facce e validarlo con la legge di Eulero. Es. 4.3.3 A partire dal grafo dell’esercizio precedente, connettere il grafo con un singolo bridge e quindi costruire il duale. Dire quante sono le facce e applicare la legge di Eulero. Es. 4.3.4 Fornire un esempio di un grafo in grado di dimostrare che G∗ è non isomorfo a G. Es. 4.3.5 E’ possibile disegnare un grafo planare semplice con 3 vertici e 4 facce? Perché? Se è possibile, disegnarlo. Es. 4.3.6 E’ possibile disegnare un grafo planare semplice con 4 vertici e 4 facce? Perché? Se è possibile, disegnarlo. Es. 4.3.7 E’ possibile disegnare un grafo planare semplice con 6 facce e 5 vertici? Se si, disegnarlo. Es. 4.3.8 E’ sempre possibile disegnare un grafo planare con n > 2 vertici e 3n − 6 archi? Es. 4.3.9 Dato un grafo semplice e connesso, una condizione necessaria per la planarità è che sia m ≤ 3n − 6. É possibile disegnare un grafo planare di 11 nodi in cui ogni nodo ha grado almeno pari a 5? Perché? Capitolo 5 Algoritmi di ricerca su grafo Gli algoritmi di ricerca su grafo, oggetto dei prossimi paragrafi, rappresentano tecniche fondamentali per determinare nodi che soddisfino particolari proprietà sia su grafi non orientati che orientati. Inoltre, le varianti a tali tecniche sono alla base di numerosi importanti algoritmi, come per esempio: trovare tutti i nodi che a partire da un nodo specifico siano raggiungibili da un path orientato; individuare tutti i path orientati che da tutti i nodi raggiungano un nodo assegnato; individuare tutte le componenti connesse di un grafo; determinare se un grafo è bipartito, ecc. Un’ulteriore applicazione di grande riscontro nella pratica consiste nella ricerca di cicli in grafi orientati e, se il grafo è aciclico, nell’ordinarne i nodi secondo un dato criterio, cosı̀ come vedremo nella Sezione 5.2. In merito ai grafi non orientati vedremo, specificamente nella nella Sezione 5.3, gli algoritmi di ricerca del minimo albero ricoprente di un grafo, all’analisi dei quali faremo precedere la dimostrazione delle condizioni che garantiscono l’ottimalità della soluzione trovata. 5.1 Algoritmi di ricerca su grafo Il problema che ci poniamo di risolvere è il seguente: determinare tutti i nodi in un grafo orientato G(V, E) raggiungibili lungo cammini orientati a partire da un nodo assegnato s, chiamato sorgente. 60 Algoritmi su grafo 61 Partire dalla sorgente ed identificare un numero via via crescente di nodi raggiungibili dalla sorgente rappresenta un criterio che possiamo seguire per risolvere il problema. Immaginiamo che ad ogni passo intermedio dell’algoritmo i nodi del grafo possono trovarsi in due stati differenti: marcato, se è possibile raggiungerli a partire dalla sorgente, o non marcato se non è stato ancora possibile. È evidente che, se un nodo i è marcato (e quindi raggiungibile dalla sorgente) ed il nodo j non è ancora marcato ed esiste l’arco (i, j), allora possiamo marcare j perché allora questo è raggiungibile da un cammino dalla sorgente attraverso l’arco (i, j). Chiameremo l’arco (i, j) ammissibile se i è marcato e j non è marcato, non ammissibile viceversa. Successivamente, esaminando archi ammissibili, potremo marcare altri nodi e, ogni volta che ci riusciremo, affermeremo che il nodo i è un predecessore di j. L’algoritmo termina quando il grafo non contiene più archi ammissibili. L’algoritmo visita i nodi marcati in un certo ordine di cui possiamo tenere traccia utilizzando un vettore che chiameremo ordine e nel quale l’elemento ordine(i) contiene il passo nel quale i è stato marcato. In Figura 5.1 viene riportato il listato in pseudocodice dell’algoritmo di ricerca. Nell’algoritmo in questione, il vettore LISTA contiene l’insieme dei nodi marcati che devono ancora essere considerati per individuare altri archi dai quali poi marcare altri nodi, mentre il vettore pred(i) definisce un albero di nodi marcati che prende il nome di albero di ricerca. Quando l’algoritmo sarà terminato, tutti i nodi raggiungibili dalla sorgente attraverso un cammino orientato risulteranno marcati. Notare che in talune istanze di grafo l’algoritmo potrebbe terminare senza aver marcato tutti i nodi del grafo stesso, il che equivale a dire che i nodi non marcati sono tutti quelli non raggiungibili dalla sorgente tramite un cammino orientato. Si può dimostrare facilmente che l’algoritmo ha una complessità computazionale di O(m + n) = O(m). Infatti, ad ogni iterazione del ciclo while o viene trovato un arco ammissibile oppure no. Nel primo caso, l’algoritmo marca un nuovo nodo e lo aggiunge a LISTA, mentre nel secondo caso cancella un nodo marcato da LISTA. Pertanto, dato che ogni nodo viene marcato al più una volta, il ciclo while è eseguito 2n volte. Per quanto riguarda la ricerca di archi ammissibili, per ogni nodo i viene percorsa la lista di adiacenza 62 Algoritmi su grafo algorithm SEARCH; begin poni tutti nodi come non marcati; marca la sorgente s; pred(s) = 0; next:=1; ordine(s) = 1; LISTA := {s}; while LISTA 6= ∅ do begin seleziona un nodo i in LISTA; if il nodo i è incidente ad un arco ammissibile (i, j) then begin marca il nodo j; pred(j) := i; next:= next + 1; ordine(j) := next; aggiungi il nodo j a LISTA; end else cancella il nodo i da LISTA; end end Figura 5.1: L’algoritmo di ricerca su grafo. A(i) al più una volta; quindi l’algoritmo esamina un totale di quindi termina al più in O(m) volte. P i∈V |A(i)| = m archi e Un aspetto centrale dell’algoritmo è la disciplina con la quale selezioniamo i nodi del grafo contenuti nel vettore LISTA per ricercare archi ammissibili e, quindi, marcare nuovi nodi. Per ottenere ciò possiamo fare uso della lista di adiacenza A(i) introdotta nella Sezione 3.1 alla quale aggiungiamo la seguente regola d’ordinamento: gli archi in A(i) sono disposti in ordine crescente rispetto al loro nodo successore, ovvero se (i, j) e (i, k) sono due nodi consecutivi in A(i), allora j < k. Tra tutte le discipline possibili di selezione di una lista, le due più note sono la coda e la pila che definiscono, rispettivamente, le tecniche di ricerca in ampiezza e di ricerca in profondità. 63 Algoritmi su grafo 2 5 1 6 3 4 Figura 5.2: Grafo per gli Esempi 5.1.1 e 5.1.2. Ricerca in ampiezza Nel caso della ricerca in ampiezza viene implementata una disciplina di tipo first in, first out (FIFO), ovvero ogni nuovo elemento viene inserito in coda e l’estrazione avviene considerando il primo elemento della lista. Se definiamo la distanza di un nodo i dalla sorgente come la somma degli archi di cui è composto il cammino diretto da s a i, questa disciplina porta a marcare prima i nodi a distanza 1, poi quelli a distanza 2 e cosı̀ via. Proprietà 5.1.1 Nella ricerca in ampiezza, l’albero dei cammini dalla sorgente s ad ogni nodo i è composto dai cammini più corti. Esempio 5.1.1 Considerando il grafo di Figura 5.2, si determini l’albero dei cammini utilizzando la disciplina di ricerca in ampiezza. Per risolvere il nostro problema, analizziamo passo per passo l’evoluzione dell’algoritmo. Chiaramente, si devono inizializzare le variabili secondo i valori che seguono: LISTA = {1}, pred(1) = 0; next = 1; ordine(1) = 1. Step 1 Al primo passo, selezionando il nodo 1 preso da LISTA, possiamo marcare il nodo 2; quindi, pred(2) = 1; next = 2; ordine(2) = 2. Dopo l’ultimo aggiornamento, LISTA = {1, 2} (notare che il nodo 2 è stato aggiunto in coda a LISTA). Algoritmi su grafo 64 Step 2 Al secondo passo, selezionando il nodo 1 preso da LISTA, possiamo marcare il nodo 3; pred(3) = 1; next = 3; ordine(3) = 3. Dopo l’ultimo aggiornamento, LISTA = {1, 2, 3}. Step 3 Al terzo passo, selezionando il nodo 2 preso da LISTA (notare che il nodo 1 è stato cancellato dalla lista in quanto non presentava più archi ammissibbili ed è quindi stato selezionato il primo nodo che era stato immesso, cioè il nodo 2) possiamo marcare il nodo 4; pred(4) = 2; next = 4; ordine(4) = 4. Dopo l’ultimo aggiornamento, LISTA = {2, 3, 4}. Step 4 Al quarto passo, selezionando il nodo 2 preso da LISTA, possiamo marcare il nodo 5; pred(5) = 2; next = 5; ordine(5) = 5. Dopo l’ultimo aggiornamento, LISTA = {2, 3, 4, 5}. Step 5 Al quinto ed ultimo passo, selezionando il nodo 4 preso da LISTA, possiamo marcare il nodo 6; pred(6) = 4; next = 6; ordine(6) = 6. Dopo l’ultimo aggiornamento, LISTA = {4, 5, 6}. A questo punto l’algoritmo non trova più archi ammissibili e si arresta. In Figura 5.3 è disegnato l’albero dei cammini definito dall’algoritmo ed i valori assunti dai due vettori pred(i) e ordine(i). È da notare che l’applicazione della disciplina FIFO ha originato, come ci dovevamo aspettare, un albero con le minime distanze tra la sorgente s ed ogni nodo di G. s Figura 5.3: Albero dei cammini e valori dei vettori pred(i) e ordine(i) nel caso FIFO. Algoritmi su grafo 65 Ricerca in profondità Nel caso della ricerca in profondità viene implementata una disciplina del tipo last in, first out (LIFO), ovvero ogni elemento nuovo viene inserito in coda e l’estrazione avviene considerando l’ultimo elemento della lista. Utilizzando questa disciplina si crea un cammino orientato il più lungo possibile e quando non si riescono ad individuare altri archi ammissibili, la lista viene scorsa ritornando verso i nodi che erano stati inseriti nei passi precedenti. Proprietà 5.1.2 Se il nodo j è un successore del nodo i e i 6= j, allora ordine(j) > ordine(i). Inoltre, tutti i successori dei nodi nella sequenza sono ordinati consecutivamente. Esempio 5.1.2 Considerando il grafo di Figura 5.2, si determini l’albero dei cammini utilizzando la disciplina di ricerca in profondità. Anche in questo caso analizziamo passo per passo l’evoluzione dell’algoritmo. Chiaramente, si devono inizializzare le variabili secondo i valori che seguono: LISTA = {1}, pred(1) = 0; next = 1; ordine(1) = 1. Step 1 Al primo passo, selezionando il nodo 1 preso da LISTA, possiamo marcare il nodo 2; pred(2) = 1; ordine(2) = 2. Dopo l’ultimo aggiornamento, LISTA = {1, 2}. Step 2 Al secondo passo, selezionando il nodo 2 preso da LISTA, possiamo marcare il nodo 3 (notare che adesso viene selezionato l’ultimo nodo che era entrato in LISTA, appunto il nodo 2); pred(3) = 2; ordine(3) = 3. Dopo l’ultimo aggiornamento, LISTA = {1, 2, 3}. Step 3 Al terzo passo, selezionando il nodo 3 preso da LISTA, possiamo marcare il nodo 4; pred(4) = 3; ordine(4) = 4. Dopo l’ultimo aggiornamento, LISTA = {1, 2, 3, 4}. Step 4 Al quarto passo, selezionando il nodo 4 preso da LISTA, possiamo marcare il nodo 6; pred(6) = 4; ordine(6) = 5. Dopo l’ultimo aggiornamento, LISTA = {1, 2, 3, 4, 6}. Algoritmi su grafo 66 Step 5 Al quinto ed ultimo passo, selezionando il nodo 2 preso da LISTA, possiamo marcare il nodo 5; pred(5) = 2; ordine(5) = 6. Dopo l’ultimo aggiornamento, LISTA = {1, 2, 5}. A questo punto l’algoritmo non trova più archi ammissibili e si arresta. In Figura 5.3 è tracciato l’albero dei cammini definito dall’algoritmo ed i valori assunti dai due vettori pred(i) e ordine(i). Osservando l’albero dei cammini individuato, possiamo notare come questa volta il cammino dalla sorgente ai nodi sia il più lungo possibile. s Figura 5.4: Albero dei cammini e valori dei vettori pred(i) e ordine(i) nel caso LIFO. 5.2 Algoritmo di ordinamento topologico Il prossimo problema che affronteremo è quello di cercare, se esistono, cicli diretti in un grafo orientato, altrimenti, se il grafo è aciclico, di etichettare i nodi 1, 2, . . . , n in modo tale che per ogni arco (i, j) ∈ E(G), l’etichetta del nodo i sia minore dell’etichetta di j. Questa numerazione, se esiste, prende il nome di ordinamento topologico. Gli algoritmi di ordinamento topologico sono essenziali in molte applicazioni come per esempio nel project management. Per etichettare i nodi di un grafo G con n numeri distinti possiamo usare un vettore ordine in modo che ordine(i) fornisca l’etichetta del nodo i. 67 Algoritmi su grafo Definizione 5.2.1 Dato un grafo orientato G, diremo che una etichettatura è un ordinamento topologico se ∀(i, j) ∈ E, si ha che ordine(i) < ordine(j). Come si è detto, non tutti i grafi possono essere ordinati topologicamente; basta infatti che il grafo sia ciclico e la condizione ordine(i) < ordine(j) non può essere dimostrata vera per ogni arco del grafo. Analogamente, un grafo che non contiene cicli può essere ordinato topologicamente. Questo ci porta a dire che un grafo orientato è aciclico se e soltanto se ammette un ordinamento topologico. algorithm ORDINAMENTO TOPOLOGICO; begin for tutti i nodi i ∈ V do indegree(i):=0; for tutti gli archi (i, j) ∈ E do indegree(j):=indegree(j) + 1; LISTA= 0; next:=0; for tutti i nodi i ∈ V do if indegree(i) = 0 then LISTA = LISTA∪{i}; while LISTA 6= ∅ do begin seleziona un nodo i in LISTA e rimuovilo; next:= next + 1; ordine(i) := next; for tutti gli archi (i, j) ∈ E do begin indegree(j):= indegree(j) − 1; if indegree(j) = 0 then LISTA = LISTA∪{j}; end end if next < n then il grafo contiene un ciclo diretto; else il grafo è aciclico ed il vettore ordine contiene il suo ordinamento topologico; end Figura 5.5: L’algoritmo di ordinamento topologico. Vediamo ora una implementazione dell’algoritmo di ordinamento topologico, di cui forniamo lo pseudocodice in Figura 5.5. La variabile indegree(i) considera il numero di archi entranti nel nodo i, che viene chiamato anche grado entrante di un nodo. In analogia, 68 Algoritmi su grafo 2 1 5 7 6 8 3 4 Figura 5.6: Grafo per l’Esempio 5.2.1 si può contare anche il numero di archi uscenti da un nodo che prende il nome di grado uscente. La complessità computazionale di questo algoritmo è O(m). Infatti, per prima cosa, vengono conteggiati i gradi in ingresso di tutti i nodi, formando una lista che comprende tutti i nodi con grado 0. Ad ogni iterazione selezioniamo un nodo i da LISTA, per ogni arco (i, j) ∈ A(i) riduciamo il grado in ingresso di 1 del nodo j e se il grado in ingresso del nodo j diviene 0 lo aggiungiamo a LISTA. Dato che l’algoritmo esamina ogni nodo ed ogni arco del grafo O(1) volte, allora la sua complessità totale e O(m). Esempio 5.2.1 Dato il grafo in Figura 5.6, individuare, se esiste, un ordinamento topologico. L’algoritmo comincia inserendo al primo passo il nodo 1 nella LISTA e quindi ordine(1) = 1. Al successivo passo, in LISTA ci sono i nodi 3 e 4, scelgo il primo e aggiorno il vettore ordine(3) = 2. Continuando, in LISTA ci sono i nodi 2 e 4, scelgo ancora il primo ed ho ordine(2) = 3. Continuando in questo modo, ottengo la sequenza ordine(4) = 4, ordine(6) = 5, ordine(5) = 6, ordine(7) = 7 ed infine ordine(8) = 8. In Figura 5.7 è rappresentato il vettore ordine(i). Si può notare che tale ordinamento trovato non è unico. Infatti, in alcuni passi si possono presentare più di un nodo con grado in ingresso nullo e la scelta di uno e di un altro non pregiudica la soluzione perché la relazione ordine(i) < ordine(j) rimane valida, ∀(i, j) ∈ E. Algoritmi su grafo 69 Figura 5.7: Contenuto del vettore ordine(i) dopo l’ordinamento topologico. Infine, siccome l’algoritmo termina con una etichettatura ammissibile per ogni nodo, si può affermare che il grafo assegnato è aciclico. È importante ribadire che nel caso dell’ordinamento topologico la selezione dell’elemento dalla lista può essere effettuato arbitrariamente. Questo porta a determinare differenti etichettature, cosa del tutto lecita visto che il nostro obiettivo era quello di rispettare una semplice condizione, ovvero che ∀(i, j) ∈ E, si abbia che ordine(i) < ordine(j). 5.3 Algoritmi di ricerca di alberi ricoprenti minimi Nella Sezione 3.4 abbiamo dato la definizione di albero ricoprente, definendolo come l’albero che comprende tutti i nodi di un dato grafo connesso G. Chiaramente un albero ricoprente non è, in generale, unico, ma dipende dalla scelta degli archi dell’insieme E. In questa sezione vogliamo considerare il Problema del minimo albero ricoprente. Supponiamo sia assegnato un grafo connesso non orientato G = (V, E) e che ad ogni arco (i, j) ∈ E sia assegnato un costo cij , dove cij è un numero intero. Si vuole trovare un albero ricoprente T chiamato minimo albero ricoprente (in inglese: minimum spanning tree, MST) tale che il costo totale, dato dalla somma dei singoli costi degli archi che lo costituiscono, sia minimo. Il problema del minimo albero ricoprente ha una importanza rilevante dato il vasto campo di applicazioni che vanno dal disegno di sistemi fisici, alla cluster analysis fino ai metodi di riduzione della memorizzazione dei dati. 70 Algoritmi su grafo Condizioni di ottimalità Per risolvere il problema forniremo due condizioni di ottimalità che ci porteranno a presentare due algoritmi in grado di fornirci la soluzione ottima. Chiaramente le due condizioni sono equivalenti, cosı̀ come i due algoritmi forniscono la stessa soluzione sullo stesso grafo. Prima di definire tali condizioni esponiamo alcune osservazioni preliminari: Definizione 5.3.1 Dato un grafo G = (V ; E), si definisce taglio di G una sua partizione in due insiemi, S ed S = V − S. Ogni taglio definisce un insieme di archi che hanno un estremo in S e l’altro in S. Indicheremo il taglio con la notazione [S, S]. Nell’esposizione successiva faremo spesso riferimento alle due osservazioni seguenti: 1. Per ogni arco (p, q) che non appartiene all’albero ricoprente T , allora T contiene un unico path da p a q. L’arco (p, q) assieme al path definisce un ciclo. 2. Se si cancella un arco (i, j) ∈ T , il grafo risultante partiziona l’insieme V in due sottoinsiemi. Gli archi del grafo G sottostante i cui estremi ricadono uno in un sottoinsieme e uno nell’altro definiscono un taglio. S S T∗ i p j q Figura 5.8: Tagli e path per le condizioni di ottimalità. In Figura 5.8 sono illustrate le due osservazioni che abbiamo esposto, ovvero l’arco (p, q) genera con il path pq un ciclo, mentre la rimozione dell’arco (i, j) genera il taglio [S, S]. Risulterà molto utile al lettore fare riferimento a tale figura per le dimostrazioni che seguono. Algoritmi su grafo 71 Enunciamo la prima condizione di ottimalità, che prende il nome di condizione di ottimalità sul taglio: Teorema 5.3.1 Un albero ricoprente è un minimo albero ricoprente T ⋆ se e soltanto se è soddisfatta la seguente condizione di ottimalità sul taglio: per ogni arco (i, j) ∈ T ∗ allora cij ≤ cpq , per ogni arco (p, q) contenuto nel taglio formato dalla rimozione dell’arco (i, j) da T ∗ . Dimostrazione: Per dimostrare che ogni albero minimo ricoprente T ⋆ deve soddisfare la condizione di ottimalità del taglio basta notare che, se esistesse un arco (p, q) ∈ [S, S] con cpq < cij , basterebbe sostituire tale arco all’arco (i, j) per ottenere un nuovo albero T ◦ con un costo minore di T ⋆ , contraddicendo la sua ottimalità. Per dimostrare la sufficienza, dobbiamo far vedere che ogni albero T ⋆ che soddisfa la condizione di ottimalità deve essere ottimo. Supponiamo allora che T ◦ 6= T ⋆ sia un minimo albero ricoprente. T ⋆ contiene almeno un arco (i, j), che non appartiene a T ◦ che, se cancellato, crea un taglio [S, S]. Se ora aggiungiamo tale arco a T ◦ creiamo un ciclo W che conterrà un arco (p, q) ∈ [S, S]. Dato che T ⋆ soddisfa la condizione di ottimalità, allora avremo che cij ≤ cpq , ma contemporaneamente T ◦ è un albero minimo ricoprente, quindi cpq ≤ cij ; di conseguenza deve essere che cij = cpq . Chiaramente, sostituire l’arco (p, q) all’arco (i, j) in T ⋆ non comporta alcun aumento del costo di tale albero, ma otteniamo un nuovo T ⋆ che ha un ulteriore arco in comune con T ◦ . Ripetendo tale procedura per tutti i suoi archi, otterremo un nuovo T ⋆ che coincide con il minimo albero ricoprente T ◦ , ovvero anche T ⋆ è un minimo albero ricoprente. La condizione di ottimalità sul taglio implica che ogni arco di un minimo albero ricoprente ha il valore del costo più piccolo tra tutti i costi degli archi che appartengono al taglio generato dalla sua rimozione. Questo implica che noi possiamo includere nell’albero ricoprente minimo qualunque arco a costo minimo appartente a qualunque taglio. Per dimostrare questo ci è utile la seguente proposizione: Proposizione 5.3.2 Sia F un sottoinsieme di archi appartenenti ad un albero ricoprente minimo e sia S un insieme di nodi di certe componenti di F . Si supponga che (i, j) sia un Algoritmi su grafo 72 arco a costo minimo nel taglio [S, S]. Allora un minimo albero ricoprente contiene tutti gli archi di F e l’arco (i, j). Dimostrazione: Supponiamo che F ⊆ T ⋆ . Se (i, j) ∈ T ⋆ allora la proposizione è banalmente vera. Se (i, j) ∈ / T ⋆ , aggiungendolo a T ⋆ si crea un ciclo W che contiene almeno un arco (p, q) 6= (i, j), con (p, q) ∈ [S, S]. Per assunzione, cij ≤ cpq , ma T ⋆ soddisfa la condizione di ottimalità e quindi cij ≥ cpq , quindi cij = cpq . Di conseguenza, aggiungere l’arco (i, j) e rimuovere l’arco (p, q) produce un albero minimo ricoprente che contiene tutti gli archi di F e l’arco (i, j). Passiamo ora a dimostrare la seconda condizione di ottimalità che chiameremo condizione di ottimalità sul path: Teorema 5.3.3 Un albero ricoprente è un minimo albero ricoprente T ⋆ se e soltanto se è soddisfatta la seguente condizione di ottimalità sul path: per ogni arco (p, q) ∈ G che non appartiene a T ⋆ si ha che cij ≤ cpq , ∀(i, j) contenuto nel path in T ⋆ che connette i nodi p e q. Dimostrazione: Per dimostrare la necessità, supponiamo che T ⋆ sia un albero minimo ricoprente e che (i, j) sia un arco contenuto nel path tra i nodi p e q. Se cij > cpq , allora la sostituzione di tale arco in T ⋆ al posto dell’arco (i, j) creerebbe un nuovo albero T ◦ con un costo minore di T ⋆ , contraddicendo la sua ottimalità. Per dimostrare che se T ⋆ è ottimo allora deve verificare la condizione di ottimalità sul path, supponiamo che (i, j) sia un arco in T ⋆ e siano S e S i due insiemi di nodi connessi generati dalla cancellazione dell’arco (i, j) da T ⋆ , con i ∈ S e j ∈ S. Consideriamo ora un arco (p, q) ∈ [S, S]. Dato che T ⋆ contiene un unico path tra p e q e dato che (i, j) è l’unico arco tra S ed S, allora (i, j) deve appartenere a tale path. La condizione di ottimalità implica che cij ≤ cpq e dato che tale condizione deve essere vera per ogni arco (p, q) ∈ / T⋆ nel taglio [S, S] generato dalla cancellazione dell’arco (i, j), allora T ⋆ soddisfa la condizione di ottimalità sul taglio e quindi deve essere ottimo. Si può notare come nella dimostrazione di quest’ultimo teorema si sia usata quella della sufficienza del Teorema 5.3.1. Questo evidenzia come le due condizioni siano completamen- Algoritmi su grafo 73 te equivalenti e che quindi un minimo albero ricoprente T ⋆ che soddisfa la condizione di ottimalità sul taglio deve contemporaneamente soddisfare anche la condizione di ottimalità sul path. Un’altra osservazione è che la condizione di ottimalità sul taglio è una condizione che potremmo indicare come una caratteristica esterna del minimo albero ricoprente, nel senso che coglie una relazione tra un suo arco e gli altri archi fuori dall’albero; viceversa, la condizione di ottimalità sul path coglie una caratterizzazione interna del minimo albero ricoprente che considera la relazione tra un singolo arco, che non appartiene all’albero, ed i diversi archi che appartengono al path, nell’albero, che si forma aggiungendo tale arco. 5.3.1 Algoritmo di Kruskal La condizione di ottimalità sul path suggerisce immediatamente un algoritmo per la ricerca del minimo albero ricoprente su un grafo. Infatti, si può partire con un generico albero ricoprente T e si testa la condizione di ottimalità sul path; se T soddisfa la condizione per ogni arco allora è un albero ottimo, altrimenti ci sarà un qualche arco (p, q) che chiude l’unico path tra p e q in T con cij > cpq . Sara sufficiente sostituire l’arco (p, q) all’arco (i, j) per ottenere un albero di costo più basso; quindi, ripetendo la procedura, in un numero finito di iterazioni avremo l’albero ottimo. La semplicità di questo algoritmo ha però una complessità computazionale che non può essere limitata in un numero polinomiale di iterazioni. Per ottenere un algoritmo più efficiente, noto con il nome di Algoritmo di Kruskal , si può iniziare considerando un ordinamento non decrescente degli archi secondo il loro peso e, quindi, una procedura che a partire da un albero vuoto, rispettando l’ordinamento posto, aggiunga archi uno alla volta. L’algoritmo può essere descritto nel seguente modo: Algoritmo di Kruskal Step 1 - Ordina tutti gli archi del grafo G per valori non decrescenti del costo degli archi; Step 2 - Prendi una foresta ricoprente H = (V (H), F ) di G, con all’inizio F = ∅; 74 Algoritmi su grafo Step i - Aggiungi ad F un arco (i, j) ∈ / F a minimo costo, tale che H rimanga una foresta. Stop quando H è un albero ricoprente. L’algoritmo esegue la procedura corretta perché, ad ogni passo, vengono scartati archi che formerebbero un ciclo con quelli che invece appartengono alla foresta corrente. Inoltre, si può notare che gli archi che formerebbero il ciclo hanno un costo che è maggiore o al più uguale al costo degli altri archi, appartenenti alla foresta, nel ciclo; questo perché i costi sono ordinati in ordine non decrescente. Pertanto, l’albero trovato soddisfa la condizione di ottimalità sul path e quindi è ottimo. Esempio 5.3.1 Dato il grafo G = (V, E) riportato in Figura 5.9 ed assegnati i costi cij , ∀(i, j) ∈ E (trascritti accanto ad ogni arco) trovare l’albero ricoprente ottimo mediante l’algoritmo di Kruskal e fornire il suo valore ottimo z ⋆ . 1 c12 = 5 2 18 15 10 12 5 16 8 3 4 Figura 5.9: Grafo per l’Esempio 5.3.1. Il primo passo è quello di ordinare gli archi in ordine non decrescente, attraverso il quale otteniamo la lista {(1, 2), (3, 4), (2, 3), (2, 4), (1, 3), (4, 5), (2, 5)}. Il passo successivo (Step 1 in Figura 5.10) parte dalla foresta F = ∅ e poi seleziona l’arco (1, 2) che è quello a costo minimo e lo aggiunge ad F . Al passo successivo (Step 2 in Figura 5.10) viene selezionato (3, 4) e dopo aver controllato che non crei un ciclo, è aggiunto ad F . L’algoritmo procede selezionando l’arco (2, 3) (Step 3 in Figura 5.10) e, dopo aver controllato che non si crei un ciclo, lo aggiunge ad F. Il test sul ciclo fallisce nei due passi successivi (Step 4 e 5 in Figura 5.10) quando selezionando prima l’arco (2, 4) e poi l’arco (1, 3) ci si accorge che 75 Algoritmi su grafo generano un ciclo. L’ultimo passo (Step 6 in Figura 5.10) seleziona l’arco (4, 5) e con quest’ultimo inserimento la foresta F diventa l’albero ricoprente ottimo T ⋆ , avente costo z ⋆ = 39. Step 1 Step 2 Step 3 Step 4 Step 5 Step 6 Figura 5.10: Evoluzione dell’algoritmo di Kruskal sul grafo di Figura 5.9. Per determinare la complessità computazionale dell’algoritmo1 occorre osservare che, per individuare un ciclo, si può, tra l’altro, creare ad ogni passo delle liste di nodi in numero pari alle componenti della foresta ricoprente corrente (nell’esempio precedente al passo 3 avremmo le due liste {1, 2, 3, 4} e {5}) e poi, per ogni arco (i, j) che si vuole introdurre, controllare se i nodi i e j appartengono ad una stessa lista. Se il test ha esito positivo, l’arco crea un ciclo (come l’arco (2, 4) al passo 4 dell’esempio) e viene scartato, altrimenti può essere aggiunto alla foresta corrente (come l’arco (4, 5) all’ultimo passo dell’esempio). Per eseguire questi passi occorre effettuare O(n) iterazioni per ogni arco del grafo e, quindi, la complessità computazionale dell’algoritmo di Kruskal è O(nm). 5.3.2 Algoritmo di Prim Il secondo algoritmo per la ricerca del minimo albero ricoprente segue dalla applicazione della condizione di ottimalità sul taglio. L’algoritmo, noto con il nome di Algoritmo di Prim, costruisce un albero ricoprente a partire da un nodo ed aggiunge un arco per 1 Il calcolo è eseguito a meno dell’ordinamento dei costi degli archi che, in un grafo di ordine n, dimensione m e costi di valore arbitrario, è uguale a O(m log m) = O(m log n2 ) = O(m log n). Algoritmi su grafo 76 volta. L’arco viene selezionato tra quelli a costo minimo appartenenti al taglio [S, S] generato dall’inserimento in S dei nodi estremi degli archi aggiunti all’albero nel passo precedente. L’algoritmo termina quando S = V . La correttezza dell’algoritmo segue dalla Proposizione 5.3.2 dove è stato dimostrato che ogni arco che aggiungiamo ad un albero è contenuto in un certo albero ricoprente assieme agli archi che sono stati selezionati nei passi successivi. La descrizione dell’algoritmo è la seguente: Algoritmo di Prim Step 1 - Considerare un albero H = (V (H), T ), con inizialmente V (H) = {r} e T = ∅, con r ∈ V (G); Step i - Ad ogni passo, aggiungi all’albero connesso H un arco a minimo costo (i, j) ∈ / H scegliendolo tra quelli che a minimo costo aggiungono un nuovo nodo e mantengono l’albero connesso. Stop se H è il minimo albero ricoprente. In pratica, l’algoritmo evolve attraverso la selezione di archi a minimo costo che abbiano gli estremi uno nell’insieme dei nodi degli archi già selezionati (cioè in S) e l’altro nell’insieme dei nodi degli archi che non sono stati ancora selezionati (cioè in S). Quindi, l’algoritmo mantiene un albero connesso che cresce di un arco ad ogni passo e che, all’ultimo, coincide con l’albero ottimo. Osservando l’algoritmo di Kruskal si nota invece che l’algoritmo aggiunge, ad ogni passo, archi ad una foresta che diventa connessa solo alla fine. Per analizzare la complessità computazionale dell’algoritmo, basta notare che l’algoritmo stesso esegue n − 1 iterazioni per definire gli n − 1 archi dell’albero ed in ogni iterazione viene selezionato da un taglio un arco a costo minimo. Dato che tale selezione può essere eseguita anche sull’intera lista degli archi, ne segue che la complessità dell’algoritmo di Prim è O(nm). Esempio 5.3.2 Considerando il grafo di Figura 5.9, trovare l’albero ricoprente ottimo mediante l’algoritmo di Prim e fornire il suo valore ottimo z ⋆ . 77 Algoritmi su grafo Supponiamo che l’algoritmo parta da V (H) = {1}. In questo caso, l’algoritmo può scegliere tra gli archi del taglio [S, S] = [{1}, {2, 3, 4, 5}]; l’arco a costo minimo selezionato e che mantiene l’albero connesso è (1, 2) (vedi Step 1 in Figura 5.11, dove la linea tratteggiata indica il taglio); aggiungo l’arco ad H (indicato in figura dagli archi in grassetto) ed il nodo 2 ad S. Al passo successivo (Step 2 in Figura 5.11) l’algoritmo deve scegliere gli archi a costo minimo tra quelli del taglio [{1, 2}, {3, 4, 5}] e, tra tutti, viene scelto l’arco (2, 3). L’algoritmo continua scegliendo (3, 4) in [{1, 2, 3}, {4, 5}] (Step 3 in Figura 5.11) ed infine termina aggiungendo l’arco (4, 5) dal taglio [(1, 2, 3, 4}, {5}] (Step 4 in Figura 5.11), restituendoci l’albero ottimo di costo minimo, pari a z ⋆ = 39. Come si può notare, l’algoritmo termina in n − 1 = 4 passi, avendo ad ogni passo aggiunto un arco ad un albero connesso. Step 1 Step 2 Step 3 Step 4 Figura 5.11: Evoluzione dell’algoritmo di Prim sul grafo di Figura 5.9. Osservazione. Si vuole ribadire che negli esempi che abbiamo esposto, cosı̀ come ci dovevamo aspettare, gli algoritmi forniscono lo stesso valore ottimo e, inoltre, i due alberi ottenuti sono identici. Quest’ultima affermazione non nasconde però una proprietà di unicità sugli archi che compongono l’albero ricoprente ottimo, perché, in generale, si possono ottenere alberi a costo minimo composti da archi differenti. Questo non ci deve sorprendere poiché gli algoritmi si possono trovare nella condizione di poter selezionare archi nella lista (per Kruskal) o nel taglio (per Prim) che abbiano stesso costo. Dato che selezionare un arco piuttosto che un altro è solo funzione delle condizioni di ottimalità, possiamo ottenere alberi minimi ottimi composti da archi differenti. Algoritmi su grafo 5.4 78 Esercizi Da inserire. Es. 5.4.1 Data le matrice di adiacenza dell’Esercizio 3.7.2, disegnare il grafo corrispondente e individuare su questo, se esiste, un ordinamento topologico. In caso negativo, motivare perché esso non esiste e quindi rimuovere il minimo numero di archi in modo che sia possibile trovare un ordinamento topologico. Capitolo 6 Problemi di flusso su grafo In questo capitolo introdurremo ed analizzeremo il seguente problema: si vuole trasferire, a minimo costo, un flusso di beni attraverso una rete in modo da soddisfare la domanda di certi nodi sulla base di quanto procurato da parte di altri nodi. Sia G = (V, E) un grafo orientato al quale considereremo associato ad ogni arco e ad ogni nodo un insieme di grandezze che li caratterizzano. In particolare, assoceremo ad ogni arco (i, j) ∈ E un costo cij che definisce il costo per unità di flusso nell’arco stesso e che varia linearmente con il flusso; una capacità uij che indichi la massima quantità di flusso che può scorrere lungo l’arco; infine, un limite inferiore lij che indica la minima quantità di flusso che deve scorrere nell’arco medesimo. Ad ogni nodo i ∈ V , invece, assoceremo una grandezza b(i) che indicherà se il nodo è un punto di rifornimento del flusso oppure è un punto di domanda del flusso; se b(i) > 0 allora il nodo i sarà un nodo di rifornimento del flusso, se b(i) < 0 allora il nodo i sarà un nodo di domanda del flusso e se b(i) = 0 allora il P nodo i sarà un nodo di trasferimento del flusso. In generale, considereremo ∀i∈V b(i) = 0. Per formulare il nostro problema in termini di modello di ottimizzazione, introduciamo la variabile decisionale xij che rappresenta la quantità di flusso che scorre nell’arco (i, j). Il Problema di flusso a costo minimo si può formulare nel seguente modo: min X (i,j)∈E 79 cij xij 80 Problemi di flusso su grafo soggetto ai vincoli: X j:(i,j)∈E xij − X xji = b(i) j:(j,i)∈E lij ≤ xij ≤ uij ∀i ∈ V (6.1) ∀(i, j) ∈ E (6.2) Il vincolo 6.1 impone sia soddisfatto il bilancio di massa ad ogni nodo. Infatti, esso indica che in un nodo la quantità di flusso entrante meno la quantità di flusso uscente deve eguagliare la quantità di flusso rifornita o richiesta. Il vincolo 6.2 impone invece che siano rispettati i limiti inferiori e superiori della capacità di ogni arco, cioè che il flusso che scorre rispetti lo spettro di valori di capacità ammessi. In molte applicazioni si ha per ogni arco che lij = 0, quindi nel seguito considereremo tale valore, se non altrimenti specificato. 6.1 Algoritmi di ricerca del cammino minimo su di un grafo Gli algoritmi di ricerca del cammino minimo su un grafo costituiscono uno degli argomenti principali dello studio delle reti di flusso. Le ragioni risiedono nel gran numero di applicazioni reali che possono essere risolte mediante questo modello, nella grande semplicità nell’ottenere soluzioni in modo molto efficiente, nel fatto che, nonostante la loro semplicità, catturano alcuni aspetti teorici che sono alla base delle reti di flusso e della teoria dei grafi e nel fatto che compaiono come sottoproblemi in molte applicazioni. Consideriamo un grafo orientato G = (V, E) al quale associamo ad ogni arco (i, j) ∈ E una lunghezza cij . Il grafo ha un nodo particolare che chiameremo sorgente s. Definiamo la lunghezza di un cammino orientato nel grafo come la somma delle lunghezze degli archi nel cammino. Il problema del cammino minimo in un grafo G = (V, E) (in inglese, shortest path problem, SPP) consiste nel determinare i cammini di lunghezza minima dalla sorgente s verso ogni altro nodo in V . In termini di flusso, possiamo modellare il problema 81 Problemi di flusso su grafo supponendo di inviare, a costo minimo, una unità di flusso verso ogni nodo i ∈ V − {s} in un grafo senza vincoli di capacità. In termini di formulazione matematica, il modello è il seguente: min X cij xij (i,j)∈E soggetto ai vincoli: X xij − X xij − j:(i,j)∈E j:(i,j)∈E X xji = n − 1 i=s (6.3) X xji = −1 ∀i ∈ V − {s} (6.4) xij ≥ 0 ∀(i, j) ∈ E (6.5) j:(j,i)∈E j:(j,i)∈E Nello studio del cammino minimo dobbiamo fare alcune assunzioni: • Gli archi devono avere lunghezza intera. Questa assunzione non è restrittiva perché nella rappresentazione nei computer i numeri irrazionali sono convertiti in numeri razionali e qualunque numero razionale può essere trasformato in un numero intero moltiplicandolo per un numero sufficientemente grande. • Il grafo contiene un cammino orientato dalla sorgente s verso qualunque altro nodo nel grafo. • Il grafo non contiene cicli di lunghezza negativa. Per la formulazione vista, l’esistenza di un ciclo W a costo negativo implicherebbe una soluzione non limitata inferiormente perché potremmo inviare una quantità infinita di flusso lungo W . Dato che esiste un cammino orientato dalla sorgente ad ogni altro nodo del grafo, allora possiamo dire che esiste un albero dei cammini minimi che dalla sorgente si emana verso tutti gli altri nodi i cui archi sono gli archi dei diversi cammini. L’esistenza di tale albero è sostenuta dalla seguente proprietà: 82 Problemi di flusso su grafo P1 P3 p s k P2 Figura 6.1: Cammini da s a k. Proprietà 6.1.1 Se il cammino s = n1 , n2 , . . . , nk = k è un cammino minimo dalla sorgente s al nodo k, allora per ogni p = 2, 3, . . . , k − 1, il sottocammino s = n1 , n2 , . . . , np è il cammino minimo dalla sorgente s al nodo p. Dimostrazione: Questa proprietà è facile da dimostrare. Infatti, se prendiamo in Figura 6.1 il cammino minimo P1 -P3 da s a k che passa attraverso un certo nodo p, allora il cammino P2 non è il cammino minimo da s a p. Se P2 fosse il cammino minimo, allora sarebbe sufficiente prendere il cammino P2 -P3 da s a k per avere un cammino più corto di P1 -P3 , contraddicendo la sua ottimalità. Indichiamo ora con d(i) la distanza del nodo i ∈ V da s. La Proprietà 6.1.1 implica che se P è un cammino minimo da s a k, allora d(j) = d(i) + cij , ∀(i, j) ∈ P ; anche il viceversa è vero, infatti: Proprietà 6.1.2 Se d(j) = d(i)+cij , ∀(i, j) ∈ P , allora P deve essere il cammino minimo tra s e k. Dimostrazione: Per dimostrare questa affermazione, supponiamo che s = n1 , n2 , . . . , nk = k sia la successione di nodi nel cammino P . Allora, prendendo la distanza d(k) ed aggiungendo e togliendo la distanza di ogni nodo in P otteniamo, manipolando algebricamente l’espressione, che: d(k) = d(nk ) = (d(nk ) − d(nk−1 )) + (d(nk−1 ) − d(nk−2 )) + . . . + (d(n2 ) − d(n1 )) Considerando che d(n1 ) = d(s) = 0 e che per assunzione d(j) − d(i) = cij , abbiamo che: d(k) = cnk−1 nk + cnk−1 nk−2 + . . . + cn1 n2 = X ∀(i,j)∈P cij Problemi di flusso su grafo 83 Di conseguenza, P è un cammino diretto dalla sorgente s a k di lunghezza d(k) e dato che per assunzione d(k) è la distanza del cammino minimo del nodo k, allora P deve essere il cammino minimo del nodo k. Quanto detto si può riassumere nel seguente risultato: Proprietà 6.1.3 Un cammino diretto P dalla sorgente s al nodo k è un cammino di lunghezza minima se e solo se d(j) = d(i) + cij , ∀(i, j) ∈ P . 6.1.1 L’algoritmo di Dijkstra L’algoritmo di Dijkstra è un algoritmo in grado di risolvere il problema della ricerca del cammino minimo dalla sorgente s a tutti i nodi. L’algoritmo mantiene una etichetta d(i) ai nodi che rappresentano un upper bound sulla lunghezza del cammino minimo del nodo i. Ad ogni passo l’algoritmo partiziona i nodi in V in due insiemi: l’insieme dei nodi etichettati permanentemente e l’insieme dei nodi che sono ancora etichettati temporaneamente. La distanza dei nodi etichettati permanentemente rappresenta la distanza del cammino minimo dalla sorgente a tali nodi, mentre le etichette temporanee contengono un valore che può essere maggiore o uguale alla lunghezza del cammino minimo. L’idea di base dell’algoritmo è quella di partire dalla sorgente e cercare di etichettare permanentemente i nodi successori. All’inizio, l’algoritmo pone il valore della distanza della sorgente a zero ed inizializza le altre distanze ad un valore arbitrariamente alto (per convenzione, porremo come valore iniziale delle distanze d(i) = +∞, ∀i ∈ V ). Ad ogni iterazione, l’etichetta del nodo i è il valore della distanza minima lungo un cammino dalla sorgente che contiene, a parte i, solo nodi etichettati permanentemente. L’algoritmo seleziona il nodo la cui etichetta ha il valore più basso tra quelli etichettati temporaneamente, lo etichetta permanentemente ed aggiorna tutte le etichette dei nodi a lui adiacenti. L’algoritmo termina quando tutti i nodi sono stati etichettati permanentemente. In Figura 6.2 è riportata la descrizione dell’Algoritmo di Dijkstra. Le operazioni compiute dall’algoritmo sono fondamentalmente due: una operazione di selezione del nodo ed una operazione di aggiornamento delle distanze. La prima seleziona ad ogni passo il nodo 84 Problemi di flusso su grafo con il valore dell’etichetta più basso, l’altra verifica la condizione d(j) > d(i) + cij e, in caso positivo, aggiorna il valore dell’etichetta ponendo d(j) = d(i) + cij . algorithm DIJKSTRA; begin S = ∅; S =V; ∀i ∈ V , d(i) = ∞; d(s) = 0; pred(s) = 0; while |S| < n do begin sia i ∈ S un nodo per cui d(i) = min{d(j) : j ∈ S}; S = S ∪ {i}; S = S − {i}; for ∀(i, j) ∈ A(i) do if d(j) > d(i) + cij then begin d(j) = d(i) + cij ; pred(j) = i; end; end; end; Figura 6.2: L’algoritmo di Dijkstra Vediamo un esempio per analizzare i passi effettuati dall’algoritmo. Esempio 6.1.1 Dato il grafo G in Figura 6.3, calcolare i cammini minimi del nodo sorgente {1} verso gli altri nodi utilizzando l’algoritmo di Dijkstra. Usare le tabelle di Etichetta nodo, Archi e Predecessore per indicare rispettivamente l’aggiornamento delle etichette dei nodi, gli archi candidati per ogni nodo ed i predecessori lungo il cammino minimo. All’inizio, l’algoritmo partiziona i nodi in due insiemi S (nodi etichettati permanentemente) e S (nodi etichettati temporaneamente), pone l’etichetta della sorgente uguale a zero e le etichette degli altri nodi uguali a ∞. Successivamente entra nel ciclo while ed esegue i seguenti passi: • seleziona il primo nodo ad etichetta minima, cioè la sorgente s; quindi, per ogni suo nodo adiacente ad esso, verifica se d(2) > d(1) + c12 , d(3) > d(1) + c13 e d(4) > 85 Problemi di flusso su grafo 2 2 2 4 1 2 4 3 1 3 1 4 5 2 6 1 4 2 5 8 3 7 Figura 6.3: Grafo per l’Esempio 6.1.1 e corrispondente tabella degli archi. d(1) + c14 ; dato che ∞ > 0 + 2 = 2, ∞ > 0 + 4 = 4 e ∞ > 0 + 2 = 2, tutte e tre le condizioni sono verificate e quindi aggiorno le etichette ed i predecessori nel seguente modo: d(2) = d(1) + c12 = 0 + 2 = 2 e pred(2) = 1, d(3) = d(1) + c13 = 0 + 4 = 4 e pred(3) = 1 ed infine d(4) = d(1) + c14 = 0 + 2 = 2 e pred(4) = 1. • al secondo passo, l’etichetta con il valore più basso è d(2) e quindi seleziono il nodo 2 che diventa etichettato permanentemente. Per i suoi nodi adiacenti si deve verificare se d(3) > d(2) + c23 e d(5) > d(2) + c25 ; dato che solo la seconda viene verificata (perché d(3) = 4 > d(2) + c23 = 2 + 2 = 4 non è verificata, mentre è vero che d(5) = ∞ > d(2) + c25 = 2 + 4 = 6), aggiorno la sua etichetta ponendo d(5) = d(2) + c25 = 2 + 4 = 6 e pred(5) = 2. • al terzo passo, l’etichetta di valore più basso è d(4) e quindi seleziono il nodo 4 che diventa etichettato permanentemente. Per i suoi nodi adiacenti si deve verificare se d(3) > d(4) + c43 e d(7) > d(4) + c47 ; dato che entrambe sono verificate (perché d(3) = 4 > d(4) + c43 = 2 + 1 = 3 e d(7) = ∞ > d(4) + c47 = 2 + 5 = 7), aggiorno le etichette ed ottengo d(3) = 3 e pred(3) = 4, e d(7) = 7 e pred(7) = 4. • al quarto passo, l’etichetta di valore più basso è d(3) e quindi seleziono il nodo 3 che diventa etichettato permanentemente. Per i suoi nodi adiacenti si deve verificare se d(5) > d(3) + c35 e d(6) > d(3) + c36 ; dato che è verificata solo la seconda, aggiorno l’etichetta corrispondente ed ottengo d(6) = 4 e pred(6) = 3. Problemi di flusso su grafo 86 • al quinto passo, l’etichetta di valore più basso è d(6) e quindi seleziono il nodo 6 che diventa etichettato permanentemente. Per i suoi nodi adiacenti si deve verificare se d(7) > d(6) + c68 e d(8) > d(6) + c68 ; dato che entrambe sono verificate, aggiorno le due etichette ed ottengo d(7) = 6 e pred(7) = 6, e d(8) = 8 e pred(8) = 6. • al sesto passo, l’etichetta di valore più basso è d(5) e quindi seleziono il nodo 5 che diventa etichettato permanentemente. Per i suoi nodi adiacenti si deve verificare se d(6) > d(5) + c56 e d(8) > d(5) + c58 ; dato che solo la seconda è verificata, aggiorno l’etichetta corrispondente ed ottengo d(8) = 7 e pred(8) = 5. • al settimo passo, l’etichetta di valore più basso è d(7) e quindi seleziono il nodo 7 che diventa etichettato permanentemente. Per i suoi nodi adiacenti si deve verificare se d(8) > d(7) + c78 ; dato che la relazione non è verificata non effettuo alcun aggiornamento. • all’ottavo ed ultimo passo, l’etichetta di valore più basso è d(8) e quindi seleziono il nodo 8 che diventa etichettato permanentemente. Il nodo non ha nessun adiacente, quindi l’algoritmo termina. Nelle due tabelle in Figura 6.4 sono riassunti rispettivamente l’andamento delle etichette lungo i singoli passi dell’algoritmo e i predecessori dei singoli nodi per ricostruire i cammini minimi. Correttezza dell’algoritmo di Dijkstra Per dimostrare la correttezza dell’algoritmo di Dijkstra facciamo due ipotesi induttive. La prima è che le etichette di distanza dei nodi in S sono ottime, la seconda che le etichette di distanza dei nodi sono la lunghezza del cammino minimo dalla sorgente s che contengono solo nodi in S. Per dimostrare la prima ipotesi, ricordiamoci che ad ogni passo l’algoritmo trasferisce un nodo i con l’etichetta più piccola da S a S; quindi, occorre dimostrare che d(i) è ottima. Ma per le ipotesi induttive poste, d(i) è la lunghezza del cammino minimo del nodo i tra tutti i cammini che non contengono un nodo di S. Supponiamo allora, per Problemi di flusso su grafo 87 Figura 6.4: Evoluzione delle etichette dei nodi e lista di adiacenza per il grafo di Figura 6.3. assurdo, di considerare un cammino P che contiene almeno un nodo k ∈ S, come mostrato in Figura 6.5 e di decomporre il cammino P nei due sottocammini P1 -P2 : il sottocammino P1 non contiene un nodo di S, ma termina in un nodo in S; quindi, per ipotesi induttiva la lunghezza è d(k) e dato che i ha la distanza minima in S, deve risultare che d(k) ≥ d(i) e quindi il sottocammino P1 deve avere lunghezza almeno d(i). Dato che ogni arco ha lunghezza non negativa, la lunghezza del sottocammino P2 è non negativa e quindi la lunghezza del cammino P è almeno d(i). Per dimostrare che l’algoritmo verifica la seconda ipotesi induttiva, osserviamo che dopo che è stata effettuata l’etichettatura permanente del nodo i, le etichette di alcuni nodi in S − {i} decrescono, perché questo è interno al cammino minimo che si sta cercando per tali nodi. Ma dopo aver permanentemente etichettato un nodo i, l’algoritmo esamina ogni arco (i, j) ∈ A(i) e se d(j) > d(i) + cij , allora avviene l’aggiornamento d(j) = d(i) + cij , pred(j) = i. Quindi, dopo l’aggiornamento, per le ipotesi induttive, il cammino dalla sorgente al nodo j definito dal vettore dei predecessori soddisfa la Proprietà 6.1.3 e quindi le etichette di distanza di ogni nodo in S − {i} sono la lunghezza del cammino minimo soggette alla restrizione che ogni nodo del cammino deve appartenere ad S ∪ {i}. Per quanto riguarda la complessità computazionale dell’algoritmo di Dijkstra partiamo dalle due operazioni di base che l’algoritmo esegue. In particolare, l’operazione di selezione dei nodi viene eseguita n volte ed ad ogni passo scandisce ogni nodo etichettato tempora- 88 Problemi di flusso su grafo S S i pred(i) P2 s k P1 Figura 6.5: Disegno per la correttezza di Dijkstra neamente, quindi esegue n + (n − 1) + (n − 2) + . . . + 1 = O(n2 ) iterazioni. L’operazione di aggiornamento delle etichette viene eseguita |A(i)| volte per ogni nodo i, quindi lungo P tutta l’esecuzione dell’algoritmo viene eseguita ∀i∈V |A(i)| = m; dato che ogni operazione di aggiornamento richiede O(1), ne segue che l’algoritmo impiega O(m) per aggior- nare le etichette. Riassumendo, dato che O(m) < O(n2 ), la complessità computazionale dell’algoritmo di Dijkstra è O(n2 ). Il valore di complessità trovato è il migliore che si può ottenere per grafi molto densi, mentre può essere migliorato nel caso di grafi sparsi. Infatti, la complessità della operazione di selezione dei nodi pesa considerevolmente di più rispetto alla complessità dell’aggiornamento delle etichette. Di conseguenza sono stati effettuati molti sforzi diretti al miglioramento dell’efficienza di tale operazione e, in particolare, implementando strutture dati più elaborate, si è riuscito a diminuire la complessità sino a O(m + n log n) nel caso di liste di nodi memorizzate come liste di Fibonacci. Rimandiamo il lettore a [10] per una trattazione completa sulle diverse implementazioni. 6.2 Problemi di massimo flusso su grafo Il problema della ricerca del massimo flusso su di un grafo è un problema complementare a quello della ricerca del cammino minimo. Infatti, alcuni aspetti del problema del flusso a costo minimo sono catturati dal problema del cammino minimo nel quale sono considerati i costi ma non le capacità; il problema del massimo flusso, invece, considera le capacità, ma 89 Problemi di flusso su grafo non i costi. L’unione dei problemi del cammino minimo e del flusso massimo rappresenta la combinazione di tutti gli ingredienti di base dell’ottimizzazione delle reti di flusso. Consideriamo un grafo orientato G = (V, E) nel quale ad ogni arco (i, j) ∈ E sia assegnata una capacità uij ≥ 0 e sia U = max{uij : uij < ∞, ∀(i, j) ∈ E}. Il Problema del massimo flusso su di un grafo si può enunciare nel seguente modo: in un grafo orientato con capacità, determinare il massimo flusso che può essere inviato da un nodo s chiamato sorgente ad un nodo t chiamato pozzo, senza eccedere le capacità dei singoli archi. Il problema può essere formulato matematicamente nel seguente modo: max v soggetto ai vincoli: X xij − X xij − X xij − j:(i,j)∈E j:(i,j)∈E j:(i,j)∈E X xji = v i=s (6.6) X xji = 0 ∀i ∈ V − {s} − {t} (6.7) X xji = −v i=t (6.8) ∀(i, j) ∈ E (6.9) j:(j,i)∈E j:(j,i)∈E j:(j,i)∈E 0 ≤ xij ≤ uij Il vettore x = {xij } che soddisfa le equazioni scritte conterrà il valore del flusso per ogni arco (i, j) e la variabile v conterrà il valore totale del flusso. Nell’analisi del problema del massimo flusso poniamo le seguenti assunzioni: • Tutte le capacità sono interi non negativi. • Il grafo non contiene un cammino diretto dal nodo s al nodo t composto solo da archi di capacità infinita. Questa assunzione impedisce che una quantità di flusso infinita possa scorrere lungo un cammino, rendendo il problema illimitato. • Se il grafo contiene l’arco (i, j) allora contiene anche l’arco (j, i). Questa assunzione non è restrittiva perché possiamo sempre aggiungere archi a capacità nulla. 90 Problemi di flusso su grafo • Il grafo non contiene archi paralleli. Per poter sviluppare l’algoritmo necessario per risolvere il nostro problema, abbiamo bisogno di definire il concetto di grafo residuo G(x) corrispondente ad un flusso assegnato x. Supponiamo che un arco (i, j) trasporti xij quantità di flusso. Quindi possiamo ancora inviare sull’arco uij − xij unità di flusso dal nodo i al nodo j; analogamente, potremmo inviare xij unità di flusso da j ad i per annullare il flusso che scorre in (i, j). Sulla base di questa osservazione possiamo definire un grafo residuo, rispetto ad un dato flusso x, sostituendo ogni arco (i, j) con una coppia di archi: un arco (i, j) con capacità residua rij = uij − xij e un arco (j, i) con capacità residua rji = xji , come si può notare in Figura 6.6. Il grafo residuo consiste solo negli archi con capacità residua positiva. Dalla (xij , uij ) i rij j i uij − xij j ⇒ j i xji Figura 6.6: Costruzione del grafo residuo. definizione di grafo residuo che abbiamo dato, ne segue che la capacità residua è composta da due componenti: una componente che indica la capacità non utilizzata uij − xij ed una componente xji che indica il flusso corrente. Quindi, possiamo scrivere che rij = uij − xij + xji . Nella Sezione 5.3 abbiamo dato la definizione di taglio in un grafo come la partizione dei suoi nodi in due insiemi, S e S = V − S, usando la notazione [S, S]. Nel seguito di questo capitolo faremo riferimento ai tagli di tipo s-t, ovvero quei tagli nei quali s ∈ S e t ∈ S. Inoltre, definiremo come archi diretti del taglio [S, S] gli archi (i, j) tali per cui i ∈ S e j ∈ S e con archi inversi del taglio [S, S] gli archi (i, j) tali per cui i ∈ S e j ∈ S; indicheremo con (S, S) l’insieme degli archi diretti del taglio [S, S] e con (S, S) l’insieme degli archi inversi del taglio [S, S]. Per esempio, in Figura 6.8 è riportato un taglio s-t per il grafo G di Figura 6.7, dove [S, S] = {(1, 3), (3, 4), (4, 6)}, (S, S) = {(1, 3), (4, 6)} e (S, S) = {(3, 4)}. 91 Problemi di flusso su grafo G 2 4 (1,2) 6 (1,2) (1,2) (2,2) 3 2 1 (2,2) 1 1 G(x) (1,2) 4 2 1 1 1 1 6 1 1 2 2 5 (1,3) 1 5 3 1 Figura 6.7: Un grafo G con un flusso assegnato x ed il corrispondente grafo residuo G(x). 2 4 6 1 3 5 Figura 6.8: Un taglio s-t per il grafo di Figura 6.7. 92 Problemi di flusso su grafo Definizione 6.2.1 Si definisce capacità u[S, S] di un taglio s-t la somma delle capacità degli archi diretti del taglio, ovvero: X u[S, S] = uij (6.10) (i,j)∈(S,S) Si definisce taglio minimo il taglio s-t che tra tutti i possibili tagli s-t ha capacità minima. Chiaramente, la capacità di un taglio è un limite superiore della quantità massima di flusso che possiamo inviare dai nodi in S ai nodi in S rispettando i vincoli di capacità imposti. Definizione 6.2.2 Si definisce capacità residua r[S, S] di un taglio s-t la somma delle capacità residue degli archi diretti del taglio, ovvero: r[S, S] = X rij (6.11) (i,j)∈(S,S) Dato un flusso x su di un grafo, per calcolare il flusso attraverso un taglio s-t possiamo utilizzare il vincolo di bilancio di massa della formulazione, per cui: v= X X ∀i∈S j:(i,j)∈E xij − X xji j:(j,i)∈E Questa espressione si può semplificare notando che se (p, q) ∈ E e p ∈ S e q ∈ S, allora comparirà un xpq nella prima sommatoria all’interno delle parentesi quadre (quando i = p), ed un −xpq nella seconda (quando j = q). Considerando che nella sommatoria non compaiono neanche le componenti di x che fanno riferimento ad archi che hanno solo nodi in S, possiamo scrivere che: v= X (i,j)∈(S,S) xij − X xij (6.12) (i,j)∈(S,S) Questa relazione indica che il flusso dai nodi in S ai nodi in S è uguale al flusso che da S va in S, meno il flusso che da S va a S e, dato che il primo membro dell’equazione è 93 Problemi di flusso su grafo esattamente il valore del flusso, abbiamo che esso eguaglia esattamente il valore del flusso nel taglio. Considerando che xij ≤ uij e che xij ≥ 0, possiamo scrivere che: v≤ X uij (6.13) (i,j)∈(S,S) Questo risultato ci indica che il valore di un qualunque flusso sul grafo è minore o al più uguale alla capacità di un su qualunque taglio s-t. Tale risultato è abbastanza intuitivo perché ogni flusso che scorre da s a t deve attraversare ogni taglio s-t e, quindi, non ne può eccedere la capacità. 6.2.1 L’algoritmo di Ford e Fulkerson Per risolvere il problema del massimo flusso in un grafo attraverso l’algoritmo che stiamo per introdurre abbiamo bisogno di alcune definizioni sul grafo residuo: Definizione 6.2.3 Si definisce cammino aumentante nel grafo residuo un cammino diretto dalla sorgente al pozzo e capacità residua δ la minima capacità residua di ogni arco nel cammino aumentante. Nel grafo in Figura 6.7, un cammino aumentante nel grafo G(x) è costituito dagli archi {(1, 2), (2, 4), (4, 3), (3, 5), (5, 6)} e la capacità residua δ = min{r12 , r24 , r43 , r35 , r56 } = min{1, 1, 1, 2, 1} = 1. Come si può notare, la capacità residua è sempre maggiore di zero; quindi, non appena un grafo contiene un cammino aumentante, possiamo inviare ulteriore flusso dalla sorgente al pozzo. Quest’ultima osservazione ci suggerisce l’algoritmo per risolvere il problema della ricerca del massimo flusso. Infatti, potremmo iniziare utilizzando le tecniche di ricerca viste nella Sezione 5.1 per identificare un cammino da s a t nel grafo G(x) partizionando i nodi del grafo in due insiemi: nodi etichettati (quelli raggiungibili da s) e nodi non etichettati (quelli non raggiungibili da s). Se alla fine del processo t è etichettato, allora invio la massima quantità di flusso, pari alla capacità residua sul cammino aumentante trovato; quindi, cancello tutte le etichette e ripeto la procedura iterativamente. L’algoritmo termina 94 Problemi di flusso su grafo quando non riesco ad etichettare t, cioè quando non esiste un cammino aumentante dalla sorgente al pozzo. Prima di esporre l’algoritmo, si vuole sottolineare che la ricerca di un cammino aumentante nel grafo residuo G(x) corrisponde alla ricerca in G di un cammino dalla sorgente al pozzo, non necessariamente orientato, con xij < uij per ogni arco diretto nel verso del cammino e con xij > 0 per ogni arco inverso rispetto al verso del cammino ed esiste un cammino aumentante rispetto ad un certo flusso x se e soltanto se esiste un cammino diretto da s a t in G(x). Se ad un certo passo dell’algoritmo si aggiorna il flusso con un flusso addizionale δ, allora xij varierà rispettando la definizione di capacità residua (rij = uij − xij + xji ) o aumentando xij di δ unità o diminuendo xji di δ unità, oppure si avrà una combinazione convessa delle due possibilità precedenti. Le osservazioni fatte fin qui possono essere formalizzate nell’algoritmo di Ford e Fulkerson descritto in Figura 6.9. La correttezza dell’algoritmo segue dal fatto che sono possibili due casi: o l’algoritmo trova un cammino aumentante dalla sorgente al pozzo, oppure non riesce a trovare alcun cammino. Se si verifica il secondo caso dobbiamo dimostrare che allora il flusso è ottimo. Supponiamo quindi che ad un certo passo, S sia l’insieme dei nodi etichettati e S = V − S l’insieme dei nodi non etichettati, con s ∈ S e t ∈ S. Se l’algoritmo non può etichettare i nodi in S a partire dai nodi in S, allora rij = 0, ∀(i, j) ∈ (S, S); inoltre, dato che rij = uij − xij + xji , xij ≤ uij e xji ≥ 0, allora la condizione rij = 0 implica che xij = uij per ogni arco (i, j) ∈ (S, S) e xij = 0 per ogni arco (i, j) ∈ (S, S). Sostituendo questi valori nell’Equazione 6.12 otteniamo: v= X (i,j)∈(S,S) xij − X xij = (i,j)∈(S,S) X uij = u[S, S] (6.14) (i,j)∈(S,S) Questa relazione mostra che il valore del flusso corrente x eguaglia la capacità del taglio [S, S] e dato che l’Equazione 6.13 implica che x è il flusso massimo e [S, S] è il taglio minimo, allora abbiamo dimostrato il seguente risultato: Teorema 6.2.1 Il valore massimo del flusso dalla sorgente s al pozzo t in un grafo con capacità eguaglia la capacità del minimo taglio s-t. Problemi di flusso su grafo 95 algorithm FORD&FULKERSON; begin etichetta il nodo t; while t è etichettato do begin cancella le etichette di tutte i nodi i ∈ V ; pred(j) = 0, ∀i ∈ V ; etichetta s e poni LISTA= {s}; while LISTA6= ∅ e t è non etichettato do begin rimuovi un nodo i da LISTA; for (i, j) ∈ G(x) do if nodo j è non etichettato then; begin pred(j) = i; etichetta j; aggiungi j a LISTA; end; end; if t è etichettato then begin usa le etichette dei predecessori per trovare all’indietro il cammino aumentante P da s a t; δ = min{rij , (i, j) ∈ P }; aumenta di δ unità di flusso lungo P e aggiorna G(x); end; end; end; Figura 6.9: L’algoritmo di Ford e Fulkerson Il teorema precedente, che chiameremo Teorema del massimo flusso e del minimo taglio ci dice anche che quando l’algoritmo di Ford e Fulkerson termina con il massimo flusso, contemporaneamente ci fornisce anche il taglio minimo. Esempio 6.2.1 Dato il grafo in Figura 6.10, calcolare il flusso massimo dalla sorgente {1} al pozzo {8} utilizzando l’algoritmo di Ford e Fulkerson, disegnando la successione dei grafi residui. Indicare il taglio minimo corrispondente al flusso trovato. 96 Problemi di flusso su grafo 2 2 4 4 2 6 1 5 3 3 3 2 3 3 2 4 5 6 8 4 7 Figura 6.10: Grafo per l’Esempio 6.2.1. Nel grafo in Figura 6.10 il flusso è posto inizialmente a xij = 0 per ogni arco; quindi, il primo grafo residuo coincide con il grafo di partenza. • Nella prima iterazione l’algoritmo trova il cammino aumentante {(1, 2), (2, 5), (5, 8)}, con δ = min{r12 , r25 , r58 } = min{4, 2, 4} = 2. L’algoritmo esegue l’aumento del flusso pari a δ = 2 unità ed aggiorna il grafo residuo, disegnato in Figura 6.11. 2 2 2 2 2 6 1 3 5 2 3 3 3 2 4 2 6 5 3 2 8 4 7 Figura 6.11: Il grafo residuo dopo la prima iterazione. • Nella seconda iterazione l’algoritmo trova il cammino {(1, 2), (2, 4), (4, 7), (7, 8)} con δ = min{r12 , r24 , r47 , r78 } = 2. aumentante L’algoritmo ese- gue l’aumento del flusso pari a δ = 2 unità ed aggiorna il grafo residuo, disegnato in Figura 6.12. • Nella terza iterazione l’algoritmo trova il cammino aumentante {(1, 3), (3, 5), (5, 8)} 97 Problemi di flusso su grafo 2 2 4 2 2 6 1 2 4 6 3 1 2 2 3 3 2 3 5 5 8 2 2 7 Figura 6.12: Il grafo residuo dopo la seconda iterazione. con δ = min{r13 , r35 , r58 } = 2. L’algoritmo esegue l’aumento del flusso pari a δ = 2 unità ed aggiorna il grafo residuo, disegnato in Figura 6.13. 2 2 4 4 3 3 2 3 4 2 2 1 5 2 3 1 4 6 2 5 8 2 2 7 Figura 6.13: Il grafo residuo dopo la terza iterazione. • Nella quarta iterazione l’algoritmo trova il cammino aumentante {(1, 3), (3, 6), (6, 8)} con δ = min{r13 , r36 , r68 } = 3. L’algoritmo esegue l’aumento del flusso pari a δ = 3 unità ed aggiorna il grafo residuo, disegnato in Figura 6.14. • Nella quinta iterazione, l’algoritmo non riesce ad etichettare il pozzo e quindi termina. Il flusso massimo è pari alla somma degli incrementi eseguiti nei singoli passi, cioè v ⋆ = 2 + 2 + 2 + 3 = 9. Il taglio minimo è riportato in Figura 6.15. 98 Problemi di flusso su grafo 2 2 4 1 4 2 2 2 3 3 1 5 6 2 3 1 2 2 4 8 3 3 2 5 7 Figura 6.14: Il grafo residuo dopo la quarta iterazione. 2 2 4 3 4 2 6 1 5 3 3 3 3 2 4 6 2 5 8 4 7 Figura 6.15: Il taglio minimo del grafo di Figura 6.10. 99 Problemi di flusso su grafo Complessità computazionale dell’algoritmo di Ford e Fulkerson Per calcolare la complessità computazionale osserviamo che l’algoritmo esegue una ricerca per trovare un cammino dalla sorgente al pozzo (che sappiamo essere eseguibile, dalla Sezione 5.1, in O(m) passi) tante volte quanto è possibile eseguire degli aumenti di flusso. Per tali aumenti, se le capacità sono intere e limitate da U , allora la capacità di un taglio (s, N − {s}) è al più nU . Di conseguenza, siccome l’algoritmo aumenta il flusso di almeno una unità ogni iterazione, ne segue che globalmente l’algoritmo esegue O(nmU ) iterazioni. L’algoritmo visto è sicuramente uno dei più semplici per risolvere il problema del massimo flusso, ma il valore di complessità computazionale è legato al valore di U e questo potrebbe portare a casi nei quali l’algoritmo non risulta essere efficiente (per esempio, se U = 2n ). Inoltre, in alcuni casi può eseguire tante iterazioni quante indicate dal caso peggiore. Per esempio, se si considera l’istanza della Figura 6.16, l’algoritmo potrebbe selezionare i cammini aumentanti 1 − 3 − 2 − 4 e 1 − 2 − 3 − 4, alternativamente, 106 volte, ogni volta aumentando il flusso di una unità. 2 2 6 6 10 10 1 106 106 3 10 4 1 106 -1 6 1 106 -1 1 1 106 -1 1 1 4 1 1 1 106 3 2 106 -1 106 -1 1 4 1 106 -1 3 Figura 6.16: Istanza patologica per l’algoritmo di Ford e Fulkerson. Un altro difetto dell’algoritmo risiede nel fatto che ad ogni passo deve essere rieseguito il processo di etichettatura e, quindi, le informazioni che si generano sui cammini aumentanti vengono perse ad ogni passo e si devono ricalcolare di nuovo. In [10] sono riportati diversi algoritmi efficienti per la risoluzione del problema del massimo flusso che non risentono dei limiti dell’algoritmo di Ford e Fulkerson. Problemi di flusso su grafo 6.3 Esercizi Da inserire 100 Indice analitico Albero, 44 Ricoprente, 45 Minimo, 69 Algoritmo, 18 Densità, 6 Distanza, 41 Embedding planare, 52 di Kruskal, 73 Faccia, 53 di ordinamento topologico, 67 Flusso, 79 di Prim, 75 Foresta, 44 di Dijkstra, 83 Formula di Eulero, 57 di Ford e Fulkerson, 93 Grafo, 3 di ricerca su grafo, 60 Aciclico, 42 in ampiezza, 63 Bipartito, 11, 43 in profondità, 65 Complemento, 9 di ricerca su stringa, 20 Completo, 6 Archi, 3 Denso, 6 Multipli, 3 Dimensione di un, 5 Disegno di un, 52 Bridge, 40 Duale, 54 Ciclo, 13 Euleriano, 45 Clique, 6 k-regolare, 8 Componenti connesse, 39 Ordine di un, 5 Connessione, 15 Orientato, 32 Crescita di funzioni, 22 Piano, 52 Combinazione di, 24 Planare, 52 Cut-vertex, 40 Semplice, 3 101 102 INDICE ANALITICO Sparso, 6 Insieme indipendente, 9 Isomorfismo, 36 Classi di equivalenza, 39 K-Fattorizzazione, 8 Lemma Handshaking, 7 Lista di adiacenza, 36 Loop, 3 Matrice di adiacenza, 34 Matrice di incidenza, 33 Nodi, 3 Notazione Big-O, 23 Numero cromatico, 12 Ordinamento topologico, 66 Path, 13, 14 Problema decisionale, 17 del cammino minimo, 80 del flusso massimo, 88 dell’albero ricoprente minimo, 69 di flusso a costo minimo, 79 Regione, 53 Sottografo, 4 Indotto, 4 Ricoprente, 4 Taglio, 70 Teorema del massimo flusso e minimo taglio, 95 di Eulero, 45 di Kuratowski, 58 Trail, 14 Euleriano, 45 Hamiltoniano, 47 Vertex covering, 10 Vicinato, 7 Walk, 14 Bibliografia [1] AA.VV. Kaliningrad Business Guide. http://guide.kaliningrad.net. [2] B. Bollobas. Modern graph theory. Springer-Verlag, 1998. [3] K. Steiglitz C. H. Papadimitriou. Combinatorial optimization: algorithms and complexity. Prentice Hall, 1982. [4] N. Christofides. Graph theory, an algorithmic approach. Academic Press, 1975. [5] R. Diestel. Graph Theory. Springer-Verlag, 2005. [6] S. Fortin. The graph isomorphism problem. Technical Report TR96-20, Department of Computer Science, The University of Alberta, Canada, July 1996. [7] D. Jungnickel. Graphs, network and algorithms. Springer-Verlag, 1999. [8] F. Maffioli. Elementi di programmazione matematica. Casa Editrice Ambrosiana, 2000. [9] R. J. Wilson N. L. Biggs, E. K. Lloyd. Graph Theory 1736-1936. Oxford University Press, 1999. [10] J. B. Orlin R. K. Ahuja, T. L. Magnanti. Network Flows. Pearson Education, 1993. [11] K. H. Rosen. Discrete mathematics and its applications. McGraw Hill Text, 1998. [12] P. Serafini. Ottimizzazione. Zanichelli, 2000. [13] A. Ventre. Introduzione ai grafi planari. Zanichelli Editore, 1983. 103 BIBLIOGRAFIA [14] D. B. West. Introduction to graph theory. Prentice Hall, 2000. 104