Elaborato di Algoritmi e Strutture Dati
Transcript
Elaborato di Algoritmi e Strutture Dati
Elaborato di Algoritmi e Strutture Dati Trovare una soluzione e analizzarla al problema del Topological-Sort. 2 Indice Introduzione al Problema 4 Soluzione 5 Visita in profondità Topological sort Analisi della complessità Analisi DFS_Visit Analisi DFS Complessità Topological-Sort Analisi della correttezza Correttezza della DFS_Visit Correttezza DFS Correttezza Topological-Sort Teorema del cammino bianco Teorema aciclicità Dimostrazione Correttezza 5 8 9 9 10 10 11 11 12 13 Esecuzione dell’algoritmo 16 Appendice 23 Bibliografia 27 3 Introduzione al problema Nella teoria dei grafi, un grafo è definito come: “Un insieme di elementi detti vertici o nodi che tra di loro sono collegati da archi o lati. Dando una definizione più formale, si dice grafo una coppia ordinata G = (V, E) di insiemi, con V insieme dei nodi ed E insieme degli archi, tali che gli elementi di E siano | | coppie di elementi di V (da questo segue in particolare | | “ Nel nostro studio, sono di interesse i Grafi Orientati, definiti come: “Un insieme D = (V, A), dove V è l'insieme dei vertici di D e A è l'insieme degli archi orientati di D. Un "arco orientato" è un arco caratterizzato da una direzione. In particolare, è composto da una "testa" (rappresentata solitamente dalla punta di una freccia), che si dice raggiunge un vertice in entrata, e una "coda", che lo lascia in uscita.” Possiamo ora definire il problema dell’ordinamento topologico: “Nella teoria dei grafi definisco ordinamento topologico (in inglese topological sort), un ordinamento lineare di tutti i vertici di un grafo aciclico orientato (directed acyclic graph). Si dice che i nodi di un grafo sono ordinati topologicamente se essi sono disposti in modo tale che ogni nodo viene prima di tutti i nodi collegati ai suoi archi uscenti. L'ordinamento topologico non è un ordinamento totale, poiché la soluzione può non essere unica. Nel caso peggiore infatti si possono avere n! ordinamenti topologici diversi che corrispondono a tutte le possibili permutazioni degli n nodi.” Ovviamente non ha senso definire il problema su grafi non aciclici, in quanto è impossibile trovare un ordinamento topologico come descritto sopra. 4 Soluzione In maniera differente all’algoritmo precedente, presenteremo una soluzione a questo problema, ma soffermandoci in maniera precisa ad analizzare ogni sua parte, in termini di correttezza e complessità. Un ordinamento topologico di un grafo aciclico orientato G si può basare sulla visita in profondità del grafo: per ciascuno nodo viene registrato il tempo di inizio e fine visita, un ordinamento può essere ottenuto ordinato i nodi in base al tempo di fine visita. A. Visita in profondità: Una visita in profondità consiste nel visitare il grafo sempre più “in profondità” se possibile. In questo tipo di visita, si ispezionano gli archi dall’ultimo vertice scoperto v che ha ancora archi non ispezionati che escono da esso. Quando tutti gli archi di v sono stati ispezionati, si torna indietro ad analizzare gli archi che escono dal vertice dal quale v era stato scoperto. Questo processo continua finché non saranno stati scoperti tutti i vertici che sono raggiungibile dal vertice sorgente iniziale. Se restano dei vertici non scoperti, uno di essi viene selezionato come nuovo vertice sorgente e ricomincia la visita. Si continua in questo modo finché tutti i vertici non saranno scoperti. Quando un vertice v viene scoperto durante l’ispezione della lista di adiacenza del vertice u già scoperto, si registra questo evento assegnando u all’attributo v.predecessore (v.π) di v. Il sotto-grafo dei predecessore, definito come Gπ (V, Eπ) dove Eπ = ( } Gπ forma una foresta DF composta da alberi Df. Gli archi di Eπ sono detti archi dell’albero. Durante la fase di scoperta, i vertici vengono colorati. Inizialmente sono bianchi, diventano grigi quando vengono scoperti durante la visita e, infine, vengono colorati di nero quando vengono completati. 5 Oltre a creare una foresta DF, la visita in profondità associa anche a ciascun vertice delle informazioni temporali. Ad ogni vertice viene associata una informazione sul momento di scoperta e sul momento di completamento. Questo informazioni temporali potranno essere usate per l’ordinamento topologico. Ovviamente per ogni vertice v sarà verificata la condizione u.tempoScoperta < u.tempo.completamento. Mostriamo il pseudo codice della procedure DFS Visita in profondità DFS DFS() 1 for Ogni vertice u appartenente a G 2 u.color = white 3 u.pred = NULL 4 time = 0; 5 for Ogni vertice u appartenente a G 6 if u.color == Color.White 7 DFS_Visit(G,u); La DFS-VISIT, invece, è la seguente procedura DFS-VISIT DFS_VISIT(G,u) 1 time = time + 1; 2 u.tempoScoperta = time; 3 u.color = Gray; 4 for Ogni vertice v appartenente a G.Adj[u] 5 if v.color == Color.White 6 DFS_Visit(G,u) 7 u.color = Black; 8 u.tempoCompletamento = time = time + 1 La figura successiva, presa dal Cormen, illustra le varia fasi della procedura DFS applicata al grafo illustrato prima figura. 6 La figura mostra che dopo che gli archi sono stati ispezionati dall’algoritmo, vengono rappresentati su uno sfondo grigio ( se sono archi dell’albero) o tratteggiati (negli altri casi). Quelli che non sono archi dell’albero sono etichettati con le lettere A, B o F a seconda che siano archi all’indietro, trasversali o in avanti (che definiremo specificatamente in seguito). All’interno dei vertici sono presenti le informazioni temporali. La procedura DFS opera nella maniera seguente. Le prime 3 righe inizializzano i vertici, cioè attribuiscono a tutti vertici il colore bianco e settano a NULL l’attributo pred( π ). La riga 4 azzera il contatore globale, time. Le successive righe controllano, uno alla volta, tutti i vertici del grafo G e, quando ne trovano uno di colore bianco, lo vistano chiamando la procedura DFS_Visit su quel vertice. Questo vertice diventa la radice di un nuovo albero della foresta DF. 7 B. Topological sort Avendo definito le funzione, possiamo correttamente mostrare il TOPOLOGICAL-SORT, l’algoritmo fa uso della procedura DFS mostrare precedentemente. Ordinamento Toplogico Topological-sort(G) 1 Chiama DFS(G) per calcolare i tempi di completamente per ciascun vertice v 2 Una volta completata l'ispezione di un vertice, inserisce il vertice in testa ad un vettore. 3 return il vettore dei vertici 8 Analisi della complessita Iniziamo calcolando la complessità della procedura più interne, la DFS-Visit. Per farlo utilizzeremo l’analisi ammortizzata, precisamente il metodo delle aggregazioni. Sostanzialmente, nell'analisi ammortizzata, per tempo di esecuzione di una sequenza di operazioni, si intende la media dei costi di tali operazioni. È però importante fare attenzione a non confondersi con l'analisi del costo medio in cui si fa riferimento a concetti di teoria della probabilità per valutare il costo di esecuzione sull'istanza media. Esistono diverse tecniche usate per l'analisi ammortizzata: Metodo dell’aggregazione: Viene determinato un limite superiore del costo totale T(n) di una sequenza di operazioni per poi definire il costo ammortizzato come: T(n)/n dove n è il numero di operazioni della sequenza. Metodo degli accantonamenti: consiste nel definire un “debito” inteso come differenza tra il costo reale dell’operazione e il costo attribuito all'operazione. Il debito viene poi saldato definendo più alto il costo attribuito ad altre operazioni facendo però attenzione a far quadrare il bilancio. Alla “chiusura” è ammesso un bilancio negativo, in modo tale che ogni debito sia stato pagato. Se questo vincolo è stato rispettato, resta legittimato il ragionamento in termini di costi attribuiti anziché di costi reali. Metodo del potenziale: è una tecnica molto simile al Metodo degli accantonamenti in cui però il “credito” accumulato è direttamente associato alla struttura di dati e tale “credito” è visto come “l'energia potenziale” della struttura stessa. A. Analisi DFS_Visit Useremo l’analisi ammortizzata per valutare la complessità dell’Algoritmo. Supponiamo inizialmente di non voler usare l’analisi ammortizzata, la riga 4 mostra un ciclo for, nel caso peggiore la lista di adiacenza ha E elementi. Dunque in ciclo for compio un O(E) confronti. 9 Inoltre, nel caos peggiore, devo suppore che tutti i vertici siano bianchi e ciò mi comporta che avviene sempre la chiamate ricorsiva. Se scrivessi l’equazione ricorrente e la risolvessi otterrei un limite superiore veramente elevato. Usando invece il metodo dell’analisi ammortizzata, notiamo che, la procedura DFS-VISIT è chiamata esattamente una volta per ogni vertice v appartenete a V, poiché la DFS-Visit viene invocata soltanto se un vertice è bianco e la prima cosa che fa è colorare il vertice. Durante un esecuzione di DFS-Visit(v), il ciclo nelle righe 4-7 viene eseguito Adj[v] volte. Poiché: ∑ = (E) Il costo totale per eseguire le righe 4-7 d DFS_visit è teta (E). B. Analisi DFS. Continuando ad analizzare la complessità con il metodo dell’analisi ammortizzata, possiamo vedere che i cicli alle righe 1-3 e 5-7 della DFS, impiegano un tempo (V), escluso il tempo per eseguire la DFS-VISIT. Utilizzando il risultato sulla DFS-Visit, il risultato della complessità è (V + E). C. Complessità topological sort La versione del topological-sort implementata inserisce in testa ad un vettore il vertice una volta che è colorato di nero. Questa operazione aggiunta ha un costo (1) e viene aggiunta alla DFSVisit non fa cambiare la complessità della funzione, ergo la complessità del topological sort è la complessità della DFS che è (V + E). 10 Analisi della correttezza In questo paragrafo dimostreremo la correttezza della procedura di ordinamento topologico. A. Correttezza della DFS-Visit Dimostriamo prima la correttezza della DFS-Visit. Supponiamo, senza perdere di generalità, che il grafo sottoposto alla procedura sia connesso. Il grafo sarà completamente esplorato e dunque i suoi nodi tutti visitati, quando tutti i vertici sono colorati di nero. Un grafo è detto connesso se, per ogni coppia di vertici (u, v) a v. V, esiste un cammino che collega u Essendo la procedura ricorsiva dimostriamo la correttezza usando il principio di induzione completa. Passo Base: Verifichiamo che la chiamata è corretta quando il grafo è composto da un solo elemento, u. Allora la lista ADJ di u è vuota quindi che non entro nel ciclo for e quindi il nodo viene colorato di nero . In questo caso u viene colorato di nero, essendo u l’unico vertice, concludo che tutti i nodi sono neri e dunque la procedura è corretta nel caso base. Passo ricorsivo: Supponiamo che le chiamate ricorsive siano corrette, quindi che la chiamata su un sotto-grafo sorgente v, raggiunge tutto i vertici connessi ad esso. Passo conclusivo: Al passo conclusivo per ogni nodo della lista di adiacenza di u non bianco viene chiamata ricorsivamente la DFS_visit. Ciò vuol dire che tutti i nodi raggiungibili dalla lista di adiacenza di u sono correttamente raggiunti. Questo, perché, essendo il grafo connesso tutti i nodi sono raggiungibile e lo sono o perché adiacenti direttamente ad u oppure perché sono raggiungibile attraverso un elemento della lista di adiacenza di u. Quindi chiamando correttamente la DFS_visit su tutti i nodi bianchi adiacenti a u (quelli non bianchi che sono già stati visitati), ho la certezza di aver visitato tutto l’albero. Allora coloro u di nero. E concludo che tutti i vertici sono neri, ergo il grafo è correttamente scoperto. 11 B. Correttezza DFS Uso l’invariante di ciclo per valutare la correttezza del ciclo for. L’invariante di ciclo che utilizzo è il seguente: “All’ i-esimo passo del for avrò un insieme di vertici analizzati di colore nero e due insieme si vertici non analizzati uno di colore bianco e l’altro nero”. Inizializzazione: Non ho ancora analizzato nessun vertici e sono tutti di coloro bianco. Conservazione: Prima’ i-esima iterazione del ciclo avrò chiamato su k vertici la DFS-Visit, questa procedura colora di nero i vertici su cui è chiamata e se questi vertici fanno parte di un grafo connesso, colora di nero anche tutti i vertici di questo albero. Avrò quindi un insieme di nodi neri che è corrispondente ai vertici analizzati, un altro di nodi neri che sono quelli connessi a quelli analizzati e un insieme di nodi bianchi che sono quelli che fanno parte di sotto-grafi non connessi a quelli scoperti. L’i-esimo vertice se è nero, vuol dire che è stato già visitato, altrimenti è bianco e ne consegue che lo visiterò ed alla fine sarà colorato di nero. Conclusivo: Alla passo conclusivo ho analizzato tutti i vertici di V. Quindi sono correttamente tutti colorati di nero. È presenta l’ovvia ottimizzazione che non permette la chiamata alla DFS-Visit nel caso in cui un nodo è stato già scoperto perché appartenente al grafo connesso di un vertice prima analizzato. La chiamata DFS-Visit, su questo nodo non aggiungerebbe niente di nuovo in quanto è già avvanuta in precedenza. 12 C. Correttezza topological sort Prima di valutare la correttezza dell’algoritmo, diamo le seguente definizioni per la classificazione degli archi: Sia Gπ la foresta DF generata da DFS sul grafo G, definisco: • Arco d’albero: Gli elementi dell’insieme Eπ sopra definito. • Arco di ritorno: gli archi (u, v) che connettono un vertice u con un antenato v nell’albero DF. • Arco in avanti: archi (u, v) non appartenenti all’albero DF che connettono l’arco u con un discendente v • Arco di attraversamento: tutti gli altri archi. Possono connettere vertici nello stesso albero DF (a patto che un vertice non sia antenato dell’altro nell’albero) o vertici in alberi DF differenti. L’algoritmo DFS può essere usato per classificare gli archi di un grafo G. Per farlo si utilizza il colore del vertice che si raggiunge durante la visita dell’arco (u, v): • se v è bianco: allora l’arco è un arco d’albero • se v è grigio: allora l’arco è un arco di ritorno • se v è nero: allora l’arco è un arco in avanti o un arco di attraversamento • se inoltre d[u] < d[v] allora è un arco in avanti • se d[v] < d[u] allora è un arco di attraversamento Per dimostrare la correttezza del topological sort, vanno dimostrati i seguenti teoremi. 13 Teorema del cammino bianco: “In una foresta di un grafo G = (V,E), il vertice v è un discendente del vertice u se e soltanto se, al tempo u.tempoScoperta in cui viene scoperto u, il vertice v può essere raggiunto da u lungo un cammino che è formato esclusivamente da vertici bianchi”. Dimostrazione: => se v = u, allora il cammino da u a v contiene soltanto il vertice u, che è ancora bianco quando impostiamo il valore di u.tempoScoperta. Adesso, supponiamo che v sia un discendente proprio di u nella foresta DF allora u.tempoScoperta < v.tempoScoperta, e quindi v è bianco al tempo u.tempoScopera. Poiché v può essere un discendente qualsiasi di u, tutti i vertici lungo l’unico cammino semplice da u a v nella foresta DF sono bianchi al tempo u.tempoScoperta (Vale facendo questo discorso per tutti i vertici tra v e u) . <= Supponiamo che si sia un cammino di vertici bianchi da u a v al tempo u.tempoScoperta e che v non diventi un discendente di u nell’albero DF. Senza perdere id generalità, supponiamo che tutti gli altri vertici diversi da v lungo il cammino diventino discendenti di u( altrimenti, indichiamo con v il vertice più vicino u lungo il cammino che non diventa discendente di u). Sia w il predecessore di v lungo il cammino, cosicché w è un discendente di u( in effetti w e u potrebbero essere lo stesso vertice) e si ha che w.tempoFinale <= u.tempoFinale. Poiché u.TemoScoperta < v.tempoScoperta < w.tempoFinale <= u.tmpoFinale. Il teorema delle parentesi implica che l’intervallo [v.tempoScoperta, v.tempoFinale] sia interamente contenuto nell’intervallo [u.tempoScoperta, u.tempoFinale]. Dunque v deve essere un discendente di u. Teorema: “Un grafo orientato è aciclico se e solo se l’algoritmo DFS su G non trova alcun arco di ritorno”. Dimostrazione: => Supponiamo che G contenga un ciclo c, allora la DFS necessariamente troverà un arco di ritorno. Infatti, sia v è il primo vertice che viene scoperto in c, e (u,v) l’arco che lo precede in c. Allora, al tempo v.tempoScoperta, c’è un percorso bianco da v a u e, per il teorema del percorso bianco, sappiamo che u diventa un discendente di v nella foresta DF. Perciò, (u,v) deve essere un arco di ritorno 14 <= Supponiamo che DFS incontri un arco di ritorno (u,v). Allora il vertice v è un antenato di u nella foresta DF. Quindi esiste certamente un percorso che va da v a u nel grafo G. Tale percorso, concatenato con l’arco di ritorno (u,v), forma un ciclo, quindi il grafo G non è aciclico. Dimostrati i teoremi precedenti, possiamo dimostrare che la procedura Topological-Sort mostrata è corretta nel caso in cui G sia aciclico. Dimostrazione correttezza topological sort: Dobbiamo dimostrare che vale la proprietà di ordinamento topologico: per ogni arco (u, v) E, u precede v nell’ordinamento. Ma questo equivale a dimostrare che, dopo la DFS, per ogni coppia di vertici u e v, se abbiamo che (u, v) E, allora f[v]<f[u]. In tal caso abbiamo appunto che u precederà v nell’ordinamento (vedi Algoritmo). Dimostriamo che, dopo DFS, per ogni coppia di vertici u e v, se (u, v) E, allora f[v]<f[u]. Preso un qualsiasi arco (u, v) E esplorato da DFS, quando l’arco viene esplorato, v non può essere grigio, altrimenti v sarebbe un antenato di u e (u, v) un arco di ritorno, contraddicendo il teorema precedente. Quindi v o è bianco o è nero. il vertice v è o bianco o nero. a) Se v è bianco, allora diventa un discendente di u è f[v]<f[u] ) b) Se v è nero, allora ovviamente sarà f[v]<f[u]. In conclusione, dato l’ordine di inserimento nella lista e l’aciclicità di G, segue la correttezza. 15 Esecuzione dell’algoritmo Le funzione Topological-Sort sopra descritta è stata implementata ed è stato misurato il tempo di esecuzione dell’ algoritmo al crescere di n. Mostreremo adesso i risultati, commentandoli. Il linguaggio scelto per implementare gli algoritmi è stato il linguaggio java, “compilato” nell’ambiente “Eclipse”, la scelta è dovuta solo alla maggior confidenza personale nei confronti di questo linguaggio e nei confronti dell’ambiente. Sono state implementate tra classi: Una classe Vertice che contiene gli attributi privati: o Key , che contiene il valore associato al vertice; o Adj, è la Lista di adiacenza, implementata attraverso la classe List di java.util; o Color, che contiene il color del vertice; o time_scoperta, il tempo in cui il vertice viene scoperto durante la visita; o time_finale, il tempo in cui è stata completata la visita del vertice; o pred, he contiene il predecessore al vertice nell’albero BF o Un costruttore che prende in input il valore di chiave ed inizializza un vertice con quel valore di chiave, crea la lista di adiacenza, setta a zero tempo_finale e tempo_scoperta, setta a bianco il colore del vertice e setta a NULL il campo pred. public class Vertice { private int key; private List<Vertice> Adj; private int color; private int time_scoperta; private int time_finale; private Vertice pred; public Vertice(int key) { super(); this.key = key; Adj = new ArrayList<Vertice>(); this.color = Color.White; this.time_scoperta = this.time_finale = 0; pred = null; 16 Un interfaccia Color che mette a disposizione i colori per i vertici: Bianco, Grigio e Nero public interface Color { public static int White=0; public static int Gray=1; public static int Black=2; Una classe Grafo che contiene gli attributi private: o V, che è un vettore che rappresenta l’insieme dei vertici del grafo; o Time, il valore temporale utilizzato per settare i tempi nei vertici durante la visita; private Vector<Vertice> V; private int time; Inoltre contiene i metodi private DFS e DFS_Visit e il metodo private Topological-Sort (che rispecchiano fedelmente il psudo-codice scritto nel capitolo precedente, è comunque possibile visualizzare il codice in appendice) È inoltre fornito un costruttore che prende in ingresso un intero n e costruisce un grafo di n vertici ed n+ (n/5) -1 archi. Il costruttore è stato implementato in modo tale che costruisce quasi sempre un grafo aciclico. Il grafo viene costruito in questa maniera : o Viene creato un vettore di n vertice e per semplicità si associa al valore key di ogni vertice un valore crescente che parte da 0. o Questo vettore lo si vede come la linearizzazione di un albero binario completo, tranne l’ultimo elemento o Si creano dunque le liste di adiacenza seguendo la regola che i figli del nodo di posto i nel vettore sono quelli di posto 2i +1 e 2i+2 (nell’ipotesi che i parta da 0). o In maniera casuale si creano n/5 archi in avanti o trasversi. Può capitare che si crea un arco tra un nodo e se stesso. In questo caso si è in presenza di un nodo all’indietro. Invece di eliminare questa situazione, la si è comunque lasciata per far vedere che l’algoritmo è in grado di gestire l’eccezione “Presenza di arco all’indietro”. L’aggiunta di un arco già presenta( cosa possibile perché gli ultimi n/5 sono casuali) viene proibita. In seguito si può vedere il codice: 17 public Grafo(int n){ V = new Vector<Vertice> (); // crea vettore che conterrà i Vertex time = 0; // inizializza il tempo di ricerca int [] V_temp = new int[n+1]; for( int i = 0; i<n;i++){ // crea n vertici e li inserisce nel vettore V_temp[i] = i; Vertice v = new Vertice(i); V.add(v); } //costruisco per semplicità il grafo come albero binario for(int i = 0; i<n/2;i++){ V.get(i).setAdj(V.get(2*i+1) ); if(2*i+2 < n) V.get(i).setAdj(V.get(2*i +2) ); } // Genero casualmente archi in avanti e trasversali . int j_1 , j_2; for(int k = 0; k < n/5;k++){ j_1 = (int) (Math.random() * 100*n) % (n/2); j_2 = j_1 + (int) (Math.random() *100*n) % (n/2); //System.out.println("Creao arco tra: "+j_1+" e :"+j_2); V.get(j_1).setAdj(V.get(j_2)); } Nel caso in cui il grafo è ciclico vuol dire durante la fase si scoperta della DFS_Visit viene analizzato un nodo di colore grigio, ciò vuol dire che è presente un arco all’indietro ergo il grafo è ciclico, quando si verifica questo caso, l’algoritmo riconosce l’arco all’indietro e lancia un eccezione. Descritti questi dettagli di programmazione andiamo a valutare la correttezza dell’algoritmo con un input di 15 elementi: Mostriamo lo screenshot relativo alla costruzione del grafo partendo dall’albero binario: 18 Il grafo creato è il seguente: L’arco tra 0 e 2 non è stato creato in quando già esisteva. L’ordinamento topologico proposto come risultato dell’algoritmo è il seguente(anche se può non sembrare il risultato è uno screenshot): Che graficamente sarebbe: 19 Che come si vede non ha archi all’indietro, è correttamente un ordinamento topologico. Verifichiamo adesso il tempo di calcolo delle funzione. Per valutare questo tempo, misuriamo il tempo di sistema in naso secondi (ns), prima e dopo l’esecuzione della funzione. Sottraendo il tempo finale a quello iniziale, abbiamo una buona stima del tempo impiegato dalla funzione per svolgere le operazione. Visto che siamo interessati a valutare l’andamento della funzione all’aumentare di V + E. Essendo nel nostre esempio di dimensioni comparabile la ocmplessità TETA( E + V) = TETA(E) = TETA(V). Per noi l’aumentare di n è uguale ad un aumento lineare sia degli arci che dei vertici. Valuteremo il tempo di esecuzione all’aumentare del numero di input n in modo esponenzialmente come potenza di 2, fino a 2^16 (Con questo input abbiamo tempi di esevuzione accettabili) e valuteremo come varia conseguentemente il tempo di esecuzione. Per far ciò, ad ogni esecuzione della funzione salviamo in un vettore i risultati del tempo, alla fine del programma scriviamo questi valori su un file di testo. È stato poi scritta una macro in Excel che legge questi file di testo e costruisce conseguentemente i grafi che indicano l’andamento dei tempo di esecuzione (sull’asse y) al variare dell’input. Mostreremo l’andamento della funzione su un grafo, dove sull’asse delle ascisse ci sarà la dimensione del vettore, mentre sull’asse delle ordinata ci sarà il tempo di esecuzione in ns. Per ogni funzione mostreremo due grafi, tra di loro differiscono solo per la scale dell’asse x. Il grafo di sx presenta una scala lineare, mentre quello di dx una scala esponenziale. Ovviamente le funzione sotto sottoposte allo stesso vettori di input. 20 Andamento della funzione: Analizzando i dati possiamo riscontrare per piccoli valori dell’input il tempo di esecuzione varia quasi esponenzialmente, ma per numero di input elevati l’andamento cresce linearmente. Questa osservazioni va a cappello con quanto detto sull’analisi asintotica in quanto essa è valida per un certe n >= n0. La costante c1 è 0,1. 21 Quello che possiamo notare confrontano l’esecuzione di questo algoritmo con quello precedente, ad esempio con il brute-force quadratico, possiamo osservare che anche se in quel caso l’andamento della funzione era quadratico, i tempo di esecuzione della funzione erano minore. Questo lo possiamo spiegare osservando che in questo algoritmo abbiamo dei coefficienti moltiplicativi molto elevati. 22 Appendice Classe Grafo: package topological_sort; import import import import java.io.FileNotFoundException; java.io.PrintWriter; java.util.Stack; java.util.Vector; public class Grafo { private Vector<Vertice> V; private int time;//serve per un esplicito controllo di Dag. public Grafo(int n){ V = new Vector<Vertice> (); time = 0; int [] V_temp = new int[n+1]; for( int i = 0; i<n;i++){ //System.out.println(i); V_temp[i] = i; Vertice v = new Vertice(i); V.add(v); } //costruisco per semplicità il grafo come albero binario for(int i = 0; i<n/2;i++){ V.get(i).setAdj(V.get(2*i+1) ); if(2*i+2 < n) V.get(i).setAdj(V.get(2*i +2) ); } // Genero casualmente archi in avanti e trasversali da aggiungere al grafo. int j_1 , j_2; for(int k = 0; k < n/5;k++){ j_1 = (int) (Math.random() * 100*n) % (n/2); j_2 = j_1 + (int) (Math.random() *100*n) % (n/2); //System.out.println("Creao arco tra: "+j_1+" e :"+j_2); V.get(j_1).setAdj(V.get(j_2)); } } public Vector<Vertice> DFS() throws Throwable{ Vector<Vertice> Tp = new Vector<Vertice>(); for (int i= 0; i<V.size();i++) if( (V.get(i).getColor() == Color.White)){ 23 DFS_Visit(V.get(i),Tp); //System.out.println("\nDFS vertice "+i); } return Tp; } private void DFS_Visit(Vertice u, Vector<Vertice> s) throws Throwable{ u.setColor(Color.Gray); u.setTimeScoperta(++time); for(int i = 0; i < u.getAdjSize(); i++){ if( (u.getAdj(i).getColor() == Color.Gray) ) throw new Throwable("grafo ciclico");// grafo aciclico lancia eccezione else if ((u.getAdj(i).getColor() == Color.White)){ u.getAdj(i).setPred(u); //System.out.print(" DFS_visit vertice :" +u.getAdj(i).getKey() ); DFS_Visit(u.getAdj(i),s); } } u.setColor(Color.Black); u.setTimeFinale(++time); //System.out.println(u.getKey()+" \n\n\n"); s.add(0,u); } public Vertice getV(int i) { return V.get(i); } public void setV(Vertice v) { V.add(v); } public static void main(String args[]) throws FileNotFoundException{ int n = 1; long secondi_start, secondi_stop; long tempo [] = new long[18]; for(int i = 0; i<17;i++){ n = 2*n; Grafo g = new Grafo(n); try { secondi_start = System.nanoTime(); Vector<Vertice> s = g.DFS(); secondi_stop = System.nanoTime(); tempo[i] = (secondi_stop - secondi_start); //System.out.println(s); } catch (Throwable e){ e.printStackTrace(); } } PrintWriter out3 = new PrintWriter("tempo_esecuzione2.txt"); for( int i = 0; i <17; i++) out3.print((int) tempo[i]+" "); out3.close(); System.out.println("fine"); 24 }} . Classe Vertice: package topological_sort; import java.util.ArrayList; import java.util.List; public class Vertice { private int key; private List<Vertice> Adj; private int color; private int time_scoperta; private int time_finale; private Vertice pred; //private Vertice succ; // forse nel topological sort non serve public Vertice(int key) { super(); this.key = key; Adj = new ArrayList<Vertice>(); this.color = Color.White; this.time_scoperta = this.time_finale = 0; pred = null; //succ = null; } public int getKey() { return key; } public void setKey(int key) { this.key = key; } public Vertice getAdj(int i) { return Adj.get(i); } public void setAdj(Vertice v) { if(!Adj.contains(v) || v == this) Adj.add(0, v); // insert in testa //else System.out.println("non creato"); } public int getColor() { return color; } public void setColor(int color) { this.color = color; } public int getTimeFinale() { return time_finale; } 25 public void setTimeFinale(int time) { this.time_finale = time; } public int getTimeScoperta() { return time_scoperta; } public void setTimeScoperta(int time) { this.time_scoperta = time; } public Vertice getPred() { return pred; } public void setPred(Vertice pred) { this.pred = pred; } /*public Vertice getSucc() { return succ; } public void setSucc(Vertice succ) { this.succ = succ; }*/ public int getAdjSize() { return Adj.size(); } public String toString(){ return new String(String.valueOf(this.key)); } } 26 Bibliografia [1] Thomas . Cormen, Charles E. Leiserson, Ronald L. Rivest, Clifford Stein; Introduction to Algorithms third edition; 2009. [2] http://en.wikipedia.org/wiki/Topological_sorting; Wikipedia. [3] http://en.wikipedia.org/wiki/Graph_(mathematics); Wikipedia. 27