Topological Sort

annuncio pubblicitario
Facoltà di Ingegneria
Corso di Studi in Ingegneria Informatica
Elaborato di Algoritmi e Strutture Dati
Topological Sort
Anno Accademico 2011/2012
Professore
Carlo Sansone
Studente
Lampognana Francesca M63/000144
Topological Sort
Introduzione
Il seguente elaborato illustrerà il funzionamento del Topological Sort, algoritmo di
ordinamento topologico utilizzato nell’ambito della teoria dei grafi. Ne verranno mostrati i
dettagli di funzionamento e il relativo pseudocodice. Si procederà dunque ad effettuarne
l’analisi asintotica per quanto riguarda la sua complessità temporale ed, infine,
mostreremo un’implementazione software dell’algoritmo, effettuata tramite il linguaggio
di programmazione C. Tramite tale software, provvederemo a testare la complessità
temporale dell’algoritmo, verificando l’aderenza dei risultati a quanto calcolato
teoricamente mediante analisi asintotica.
2
Topological Sort
Descrizione dell’algoritmo
Nome capitolo
L’algoritmo Topological Sort si inserisce nel contesto della teoria dei grafi. In particolare
esso va a realizzare l’ordinamento topologico, che definiremo in modo formale nei
prossimi paragrafi. Inizieremo la trattazione andando a mostrare alcune definizioni
fondamentali della teoria dei grafi, senza le quali sarebbe impossibile procedere ad
un’accurata descrizione dell’algoritmo che effettua l’ordinamento topologico.
Un grafo G può essere definito formalmente dalla coppia: G = (V,E)
Ove V rappresenta l’insieme dei vertici del grafo ed E è l’insieme degli archi (o spigoli) ad
esso appartenenti.
Ogni grafo può essere orientato o meno, ovvero ogni arco può avere o meno un verso.
Formalmente, se il grafo è non orientato, dato un arco
, mentre se il
grafo è orientato non è detto che sia vera questa relazione.
Passiamo ora a descrivere in che modo avviene la rappresentazione di un grafo. Esistono
due modi diversi per rappresentare un grafo: la lista di adiacenza e la matrice di adiacenza.
Illustreremo, per rimanere fedeli allo scopo dell’elaborato, solo le liste di adiacenza.
Una lista di adiacenza consiste in un array Adj di |V| elementi, ove |V| rappresenta la
cardinalità dell’insieme dei vertici V. Ogni elemento di tale array punta a una lista
3
Topological Sort
concatenata. Per ogni u
, Adj[u] contiene tutti i vertici v per i quali esiste un arco
. Quindi Adj[u] contiene tutti i vertici adiacenti ad u nel grafo .
Mostriamo in figura un esempio di rappresentazione di un grafo orientato:
Figura 1: Rappresentazione mediante lista di adiacenza
Una proprietà delle liste di adiacenze è che, se il grafo è orientato, la somma delle
lunghezze delle liste è pari a |E|, cardinalità dell’insieme degli archi E. Se invece il grafo
non è orientato, tale somma sarà pari a 2|E| in quanto è necessario rappresentare sia l’arco
(u,v) che quello (v,u).
Topological sort: generalità
Mostriamo ora in che modo funziona l’algoritmo di ordinamento topologico.
L’ordinamento topologico è un ordinamento definito sui vertici di un DAG (directed
acyclic graph), un grafo orientato aciclico.
Possiamo definire il topological sorting del DAG G = (V,E) come un ordinamento lineare
dei suoi vertici, in modo tale che se
u appare prima di v.
Lo si può vedere come un ordinamento orizzontale dei vertici del grafo in modo che tutti
gli archi vadano da sinistra a destra. Questo spiega, intuitivamente, come mai non sia
possibile effettuare un ordinamento topologico su un grafo in cui sono presenti dei cicli.
Questo algoritmo è intimamente legato all’algoritmo di visita in profondità (DFS) dei grafi
e vedremo, anzi, che potrà essere realizzato andando a effettuare una modifica di tale
algoritmo. Quindi, prima di mostrare lo pseudocodice relativo al topological sort,
illustreremo il funzionamento della visita in profondità.
4
Topological Sort
Depth-First Search (DFS): funzionamento
Dato il grafo G, un algoritmo di visita ha come scopo la scoperta dei vertici che fanno
parte del grafo. In particolare, nel caso della ricerca in profondità, è possibile scoprire
sicuramente tutti i vertici del grafo, mentre nel caso dell’altro tipo di visita (BFS) ciò non è
assicurato.
Caratteristica fondamentale della DFS, dalla quale ne deriva il nome, è quella di procedere
in profondità: essa visita sempre il “successore” dell’ultimo nodo visitato; una volta
raggiunto il fondo della profondità, la procedura torna indietro e considera il primo nodo
che non era stato precedentemente considerato.
Per tenere conto delle varie caratteristiche dei nodi durante la procedura, ad ognuno di essi
è associato un colore. Ogni colore va a definire uno stato del nodo:
-
Bianco: Il nodo non è ancora stato scoperto;
-
Grigio: Nodo scoperto, ma non è ancora terminata la ricerca in profondità;
-
Nero: Nodo scoperto e la profondità è stata risolta almeno fino al suo livello;
Un ulteriore valore associato ad ogni nodo è . Per ogni nodo
rappresenta il
predecessore di v. Si dice che u è predecessore di v se il vertice v è stato scoperto
scorrendo la lista di adiacenze del vertice u.
Infine, altri due valori, caratteristici della DFS, associati ai nodi sono due timestamps:
-
Tempo di scoperta: v.d
-
Tempo finale: v.f
Per impostare tali tempi all’interno delle procedure sarà usata la variabile globale time.
La DFS si articola in 2 procedure:
-
DFS(G): effettua un ciclo di inizializzazione sui vertici del grafo, impostando il colore
come Bianco e il valore di
pari a NIL per ognuno di essi. Dopodichè inizia un altro
ciclo sui nodi e, se il nodo v è bianco, invoca la procedura DFS-VISIT(G,v), altrimenti
il nodo è già stato scoperto e la procedura non fa nulla;
5
Topological Sort
-
DFS-VISIT(G,u): a partire dalla sorgente v indicata effettua la ricerca Depth-First. La
procedura è ricorsiva. Incrementa la variabile time, imposta il tempo di scoperta u.d e
setta il colore u.color a Grigio. Controlla la lista di adiacenza del nodo u e per ogni
nodo v che ve ne fa parte controlla se tale nodo è bianco; in tal caso imposta il nodo u
come v. e richiama ricorsivamente la procedura questa volta indicando v come nodo
sorgente. La ricorsione procede finchè non si raggiunge un nodo per il quale tutti i
nodi della sua lista sono grigi. Per tale nodo quindi si imposta il colore Nero e si setta
il tempo finale. La procedura termina e si risale la ricorsione andando a impostare tutti
i tempi finali e i colori come Nero.
Depth-First Search (DFS): pseudocodice
Dopo aver illustrato il funzionamento della DFS, mostriamo qui di seguito gli
pseudocodici relativi alle due procedure di cui esso è composto.
6
Topological Sort
Depth-First Search (DFS): complessità
Mostriamo in che modo viene valutata la complessità temporale della visita in profondità
DFS. Calcolare questa complessità è fondamentale, in quanto rappresenta la quasi totalità
dei fattori che vanno considerati per valutare la complessità dell’algoritmo oggetto di
questo elaborato, il Topological Sort.
Per effettuare questa valutazione utilizzeremo l’analisi ammortizzata, in particolare il
metodo delle aggregazioni.
Nell’analisi ammortizzata, si effettua una media del tempo necessario a eseguire una
sequenza di operazioni secondo il caso peggiore. Il metodo delle aggregazioni afferma
che, per ogni n, una sequenza di n operazioni ha un tempo nel caso peggiore pari a T(n).
Questo tempo è definito come costo aggregato. Si dice che, nel caso peggiore, il costo
ammortizzato di ognuna della n operazioni è pari a T(n)/n.
Questo discorso entra quando si valuta il numero di esecuzioni della procedura DFSVISIT. Possiamo infatti dire che essa è chiamata esattamente una volta
; a causa
del controlli fatti sia nel corpo della procedura DFS, sia nel corpo della stessa DFS-VISIT
sappiamo che il vertice v su cui è chiamato è bianco; dato che la prima cosa che si fa nella
DFS-VISIT è impostare il colore del nodo a grigio, siamo sicuri del numero di esecuzioni
della DFS-VISIT.
Nel corpo della procedura DFS-VISIT il ciclo for è eseguito Adj[v] volte. Dato che:
Il costo totale di tale ciclo è
. Allora, unendo a questo il risultato precedente,
possiamo dire che il tempo totale della Depth-First Search è:
7
Topological Sort
Depth-First Search (DFS): DF-Tree e aciclicità
Un ultimo concetto da introdurre prima di passare ad esporre il topological sort è l’output
della ricerca DFS. Infatti, alla fine di una Depth-First Search possono essere generati i
Depth-First Trees. La procedura va a settare i predecessori
generato il grafo
in maniera tale che venga
in cui i vertici sono i nodi del grafo G e gli archi sono di questo tipo:
(v. , v)
Questo grafo è in realtà visibile come uno o più alberi DF (possono essere più di uno nel
caso sia stato necessario usare più di una sorgente per la ricerca).
Mediante gli alberi DF possiamo effettuare una classificazione degli archi del grafo G.
Essi verranno divisi in 4 tipologie: archi d’albero, archi all’indietro, archi in avanti e archi
trasversali. Ai fini della trattazione, l’unico tipo di arco di cui parleremo sono gli archi
all’indietro.
Un arco all’indietro è un arco (u,v) che connette u con un suo antenato v nell’albero DF.
Vediamo un esempio in figura:
Figura 2: Foresta DF e archi all’indietro
Questi archi ci interessano poiché ci permettono di definire in quali casi un grafo è
aciclico.
Si dice che un grafo G è aciclico se e solo se il Depth-First Tree non presenta archi
all’indietro.
8
Topological Sort
Topological Sort: pseudocodice
Sulla base di quanto detto in precedenza possiamo passare a esporre lo pseudocodice della
procedura di ordinamento topologico.
Ripetiamo che questa procedura è eseguibile nell’ipotesi in cui il grafo G è aciclico.
Ai fini implementativi la procedura è stata realizzata modificando le funzioni DFS e DFSVISIT in modo che la prima restituisse la lista concatenata dei vertici e che la seconda
effettuasse gli inserimenti su tale lista.
Topological Sort: correttezza
Mostriamo ora che tale procedura è corretta e restituisce l’ordinamento topologico del
DAG G fornito in ingresso.
Ipotizziamo che venga svolta la DFS sul grafo diretto aciclico G = (V,E), determinando i
tempi di fine visita dei suoi vertici.
Dobbiamo dimostrare che per ogni coppia di vertici distinti, u,v
, se G contiene un
arco che va da u a v, allora v.f < u.f.
Consideriamo un arco (u,v) esplorato dalla DFS (G). Quando è esplorato, v non può essere
grigio poiché in tal caso v sarebbe un antenato di u e l’arco (u,v) sarebbe un arco
all’indietro (e implicherebbe la presenza di un ciclo nel grafo). Quindi v può essere nero
oppure bianco. Se v è bianco, diventa un discendente di u e quindi v.f < u.f. Se v è nero,
vuol dire che la sua visita è già terminata, v.f è già stato settato e quindi, dato che
dobbiamo ancora impostare u.f è ovvio che v.f < u.f anche in questo caso.
Quindi per ogni arco (u,v) nel DAG, v.f < u.f.
9
Topological Sort
Topological Sort: complessità
E’ molto semplice valutare la complessità temporale di questa procedura, avendo
precedentemente valutato la complessità della DFS(G).
Infatti, la DFS (G) ha un costo pari a
e per svolgere l’inserimento in testa alla
lista concatenata il tempo necessario è O(1). Quindi la complessità del topological sort è
pari a
Valuteremo se questo risultato ha un riscontro nella pratica nel prossimo capitolo, dedicato
all’analisi del software realizzato.
10
Topological Sort
Analisi del Software
Nome capitolo
In questo capitolo verranno illustrate le prestazioni del software realizzato. Esse verranno
valutate mediante varie misure prese e si verificherà l’effettiva aderenza al risultato
formale relativo all’algoritmo Topological Sort mostrato nel precedente capitolo.
Per poter tracciare l’andamento delle prestazioni del software realizzato si sono effettuati
diversi test scegliendo volta per volta diversi input. Tramite i risultati ottenuti si è potuto
tracciare il grafico della funzione T(n) relativa alle prestazioni di topologicalsort.
Una volta ottenuto il grafico di questa funzione si è provveduto a verificare l’aderenza al
risultato teorico.
Tale risultato affermava che la complessità di Topological Sort è Θ(V+E). Per dimostrare
la validità di tale definizione, bisogna dimostrare che:
Detta f (V+E) la funzione che rappresenta il tasso di crescita del tempo di esecuzione di
Topological Sort, bisogna trovare c1,c2 e n0 tali che:
Il software realizzato acquisisce in input il numero di vertici del grafo e genera
casualmente gli archi rispettando sempre l’ipotesi di aciclicità del grafo. Dunque, dato che
11
Topological Sort
il numero di archi non è predicibile e può variare molto sono state effettuate molte misure.
Per effettuare la valutazione è stato poi sommato il numero di vertici richiesto V con il
numero di archi generato casualmente E. Queste somme rappresentano le ascisse dei punti
della funzione.
Mostriamo dunque di seguito il grafico relativo alle prestazioni ottenute con il software
sviluppato fornendo in input i valori precedentemente citati.
Figura 3: Prestazioni di Topological Sort
Il grafico mostra i tempi di esecuzioni espressi in microsecondi dell’algoritmo che lavora
su un vettore formato da numeri casuali di dimensione n. E’ confermata l’aderenza al
risultato teorico in quanto è stato possibile trovare due costanti c1 e c2 che hanno
permesso di generare le rette tratteggiate nel grafico. Le costanti c1 e c2 trovate valgono,
rispettivamente: 4,7 * 10-2 e 3,5* 10-2 . n0 è identificabile più o meno in 4,52 * 105
12
Topological Sort
Codice
Nome capitolo
Mostriamo qui di seguito il codice sorgente relativo all’algoritmo Topological Sort. Esso è
stato scritto utilizzando il linguaggio di programmazione C.
Evidenziamo prima alcuni aspetti del codice. In particolare, si vuole mettere in evidenza il
fatto che nel codice si è realizzata una funzione che genera in modo casuale un Directed
Acyclic Graph del numero di nodi desiderato. Questa funzione ovviamente non effettua la
generazione seguendo la definizione teorica e dunque identificando la presenza di un arco
all’indietro nell’albero DF a valle dell’esecuzione della ricerca DFS sul grafo. Per motivi
di praticità si è scelto di implementare tale funzione in maniera tale che, assegnato un
numero intero progressivo ai vertici del grafo, al suo interno può esistere un arco (u,v) solo
se u < v.
Ovviamente questa è solo una condizione sufficiente a non avere cicli in un grafo, ma non
è assolutamente una condizione necessaria.
Si è scelto di non far inserire il grafo in input all’utente neanche nel caso di ordinamento di
prova poiché sebbene di semplice realizzazione avrebbe solo appesantito il codice,
esulando dagli scopi effettivi di questo elaborato.
13
Topological Sort
/*Software che implementa il Topological Sort, algoritmo di ordinamento
topologico per DAG (grafi orientati aciclici). Il SW genera automaticamente
un grafo aciclico in maniera casuale.
*/
//Autore: Lampognana Francesca M63/144
#include <stdio.h>
#include <stdlib.h>
#include <windows.h>
//necessaria per l'utilizzo di QueryPerformanceCounter
//Dichiarazione delle variabili necessarie per il calcolo dei tempi d'esecuzione
static LARGE_INTEGER frequency;
// ticks per second
static LARGE_INTEGER t1, t2;
// ticks
static double elapsedTime;
//tempo trascorso
//Struttura che rappresenta un nodo del grafo
struct vertice {
int id;
struct vertice *next;
};
typedef struct vertice* vertpunt;
//puntatore a un nodo
int tempo;
//variabile globale necessaria per dfs
//Struttura contenente i dati associati alla dfs
struct visitdata {
int color;
int pi;
int d;
int f;
};
//Calcolo del tempo iniziaòe
void starttime(){
static int first=1;
if(first){
QueryPerformanceFrequency(&frequency);
first=0;
}
QueryPerformanceCounter(&t1);
}
//Calcolo del tempo finale
void endtime(){
QueryPerformanceCounter(&t2);
}
//Calcola la differenza tra i due tempi e la porta in microsecondi
double eltime(){
return ((double)t2.QuadPart - (double)t1.QuadPart) * 1000000.0 /
(double)frequency.QuadPart;
}
//Funzione che genera un nuovo puntatore a un nodo
vertpunt newpunt(){
vertpunt v;
v=(vertpunt)malloc(sizeof(struct vertice));
v->next=NULL;
return v;
14
Topological Sort
}
//Inizializza la lista di adiacenze
void init(vertpunt list[],int n){
int i;
for(i=0;i<n;i++)
list[i]=NULL;
}
//Inizializza i dati necessari per la dfs
void initdfs(struct visitdata dfs[],int n){
int i;
for(i=0;i<n;i++){
dfs[i].color=0;
dfs[i].pi=-1;
dfs[i].d=0;
dfs[i].f=0;
}
}
/*Genera una lista di adiacenze casuale, generata in maniera tale che il grafo
risulti sicuramente aciclico. Restituisce il numero di archi generati, info
necessaria per le valutazioni di complessità temporale*/
int AdjList(vertpunt list[],int n){
int k,i,j;
int e=0;
vertpunt nuovo,p;
for(i=1;i<n;i++){
//la lista è generata in modo che il grafo
for(j=0;j<i;j++){
//risultante sia sicuramente aciclico
k=rand()%2;
//Valore random che determina la creazione degli archi
if(k==1){
nuovo=newpunt();
nuovo->id=i;
p=list[j];
if(p==NULL){
list[j]=nuovo;
//crea arco j->i nel caso di 1° arco uscente da j
e=e+1;
} else {
while(p->next!=NULL)
p=p->next;
p->next=nuovo;
//genera l'arco j->i
e=e+1;
}
}
}
}
return e;
//e sarà necessario per le valutazioni di complessità temporale
}
/*Stampa la lista di adiacenze e la lista topologica generata a valle dell'uso
del Topological Sort. La procedura sarà usata solo nel caso in cui venga scelto
di effettuare un ordinamento di prova.
*/
void VisualizzaLista(vertpunt list[],int n,vertpunt tops){
int i;
vertpunt p,punt;
printf("\nLista di adiacenza:\n");
for(i=0;i<n;i++){
15
Topological Sort
printf("\nLista[%d]-->",i);
p=list[i];
while(p!=NULL){
printf("[%d| ]-> ",p->id);
p=p->next;
}
printf("/ \n");
}
punt=tops;
printf("\nLista topologica:\n");
while(punt!=NULL){
printf("[%d| ]-> ",punt->id);
punt=punt->next;
}
}
/*Aggiunge un elemento in testa alla lista topologica. La prodcedura viene usata
all'interno della dfs_visit a valle del settaggio del colore di un nodo a NERO*/
vertpunt settops(vertpunt tops,int i){
vertpunt nuovo;
nuovo=newpunt();
nuovo->id=i;
nuovo->next=tops;
tops=nuovo;
return tops;
}
/*Procedura dfs_visit leggermente modificata per integrare le operazioni
necessarie al topological sort. In particolare restituirà una lista di nodi,
ovvero la lista topologica, che sarà aggiornata alla fine di ogni chiamata*/
vertpunt dfs_visit(vertpunt list[],struct visitdata dfsdata[],int i,vertpunt
tops){
vertpunt p,punt,nuovo;
int k;
tempo=tempo +1;
dfsdata[i].color=1; //Colore grigio
dfsdata[i].d=tempo;
p=list[i];
while(p!=NULL){
k=p->id;
if(dfsdata[k].color==0){
dfsdata[k].pi=i;
tops=dfs_visit(list,dfsdata,k,tops);
}
p=p->next;
}
dfsdata[i].color=2;
//Colore nero
tempo=tempo+1;
dfsdata[i].f=tempo;
tops=settops(tops,i); //Inserimento in testa nella lista topologica
return tops;
}
/*Procedura principale della dfs, leggermente modificata in accordo al
topological sort. In particolare restituite la lista topologica, una volta che
è stata completata*/
vertpunt dfs(vertpunt list[],int n,vertpunt tops){
16
Topological Sort
int i;
vertpunt p;
struct visitdata dfsdata[n]; //Struttura dati che contiene i dati dfs
initdfs(dfsdata,n);
//Inizializza tale lista
tempo=0;
for(i=0;i<n;i++){
//Per ogni v appartenente a G.V
if(dfsdata[i].color==0)
//Colore bianco
tops=dfs_visit(list,dfsdata,i,tops);
}
return tops;
}
/*Funzione del topological sort. Di fatto è superflua, in quanto non fa niente
oltre a richiamare una funzione e restituirne il risultato. E' stata tenuta
solo per fini espositivi.*/
vertpunt TopologicalSort(vertpunt list[],int n){
vertpunt tops=NULL;
tops=dfs(list,n,tops);
//Esegue dfs modificata: ogni volta che è
settato
//un tempo finale di un nodo aggiunge tale
nodo in testa
return tops;
//alla lista tops
}
/*MAIN*/
int main(){
int n,i,option=0, fine=0, r=1,e;
srand(time(0));
//Setta il seme della rand(). srand va eseguito una sola
volta
printf("***\tTOPOLOGICAL SORT\t***\n\n");
do {
do{
printf("Effettua una scelta tra le seguenti:\n");
printf("1 - Ordinamento di prova con DAG di piccola dimensione e stampa a
video\n");
printf("2 - Misura dei tempi con DAG generato casualmente e output su
file\n");
printf("3 - Termina il programma\n");
printf("Scelta: \t");
scanf("%d",&option);
} while((option<1) || (option>3));
switch(option){
case 1:
{
printf("\nInserisci il numero di nodi: \t");
scanf("%d",&n);
vertpunt lista[n];
vertpunt tops=NULL;
init(lista,n);
//Inizializzo la lista
17
Topological Sort
e=AdjList(lista,n);
//Genera lista di adiacenze di un
DAG
tops=TopologicalSort(lista,n);
VisualizzaLista(lista,n,tops);
printf("\nNumero di archi: %d",e);
printf("\n");
}
break;
case 2:
{
printf("\nInserisci il numero di nodi: \t");
scanf("%d",&n);
printf("\nRipetizioni della misura:\t");
scanf("%d",&r);
vertpunt dag[n];
for(i=0;i<r;i++){
vertpunt toplist=NULL;
init(dag,n);
//Inizializzo la lista
e=AdjList(dag,n); //Genera lista di adiacenze di un
DAG
starttime();
toplist=TopologicalSort(dag,n);
endtime();
elapsedTime=eltime();
printf("\nTempo %d: %f us\n",i,elapsedTime);
//Stampa dei tempi su file di testo
FILE *pf1;
char *nomefile1 = "topologicalsort.txt";
pf1 = fopen(nomefile1, "a");
if(pf1){
fprintf(pf1, "%f\t%d\n", elapsedTime,n+e);
fclose(pf1);
}
}
printf("\nMisurazione terminata. Output stampati sul
file 'topologicalsort.txt'");
printf("\n");
}
break;
case 3:
{
fine=1;
}
break;
}
} while(fine==0);
return 0;
}
18
Scarica