You are on page 1of 36

Elaborato di Algoritmi e Strutture Dati

Gargiulo Alessandro - Mat. M63/417 5 gennaio 2014

Indice

Il problema del massimo sotto-array


1.1 1.2 Introduzione . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Soluzione Iterativa 1.2.1 1.2.2 1.2.3 1.3 1.3.1 1.3.2 1.3.3 1.4 1.4.1 1.4.2 1.4.3 1.4.4 1.5 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Pseudocodice . . . . . . . . . . . . . . . . . . . . . . . . . Correttezza dell'Algoritmo Complessit . . . . . . . . . . . . . . . . . . . . . . . . . . Pseudocodice . . . . . . . . . . . . . . . . . . . . . . . . . Correttezza dell'Algoritmo . . . . . . . . . . . . . . . . . . Complessit . . . . . . . . . . . . . . . . . . . . . . . . . . Pseudocodice Find-Max-Crossing-Subarray Pseudocodice Find-Maximum-Subarray . . . . . . . .

1
1 1 1 3 3 4 4 5 6 6 7 8 9 10 10 11 11 12 14 14 15 18 19 19 20 21 21

Soluzione Iterativa Semplicata . . . . . . . . . . . . . . . . . . .

Soluzione Ricorsiva . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

Correttezza dell'Algoritmo . . . . . . . . . . . . . . . . . . Complessit . . . . . . . . . . . . . . . . . . . . . . . . . . 1.4.4.1 Complessit con Albero di Ricorrenza . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

Realizzazione in C++ 1.5.1 1.5.2 1.5.3 1.5.4 1.5.1.1 1.5.2.1 1.5.3.1 1.5.4.1 1.5.4.2 1.5.4.3

Listato v1.0 . . . . . . . . . . . . . . . . . . . . . . . . . . Simulazione . . . . . . . . . . . . . . . . . . . . . Simulazione . . . . . . . . . . . . . . . . . . . . . Simulazione . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Prestazioni versione iterativa a doppio ciclo . . . Prestazioni versione iterativa a singolo ciclo . . . Prestazioni versione ricorsiva . . . . . . . . . . . Listato v2.0 . . . . . . . . . . . . . . . . . . . . . . . . . . Listato v3.0 - Ricorsione . . . . . . . . . . . . . . . . . . . Valutazione delle prestazioni

1.6

Conclusioni

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

Topological sort
2.1 2.2 Introduzione . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Depth-First-Search 2.2.1 2.2.2 . . . . . . . . . . . . . . . . . . . . . . . . . Pseudocodice . . . . . . . . . . . . . . . . . . . . . . . . . Correttezza dell'algoritmo . . . . . . . . . . . . . . . . . .

22
22 23 24 24

INDICE

2.2.3 2.3 2.3.1 2.3.2 2.3.3

Complessit . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Pseudocodice . . . . . . . . . . . . . . . . . . . . . . . . . Funzionamento graco . . . . . . . . . . . . . . . . . . . . Correttezza . . . . . . . . . . . . . . . . . . . . . . . . . . 2.3.3.1 2.3.3.2 2.3.3.3 Teorema del cammino bianco . . . . . . . . . . . Teorema sui gra aciclici . . . . . . . . . . . . . Correttezza del topological sort . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

25 26 26 26 27 28 28 28 29 29 31

Topological Sort

2.4

Realizzazione in C++ 2.4.1 2.4.2

Codice topological sort . . . . . . . . . . . . . . . . . . . . Analisi delle prestazioni

Elaborato in ASD: Algoritmi e Strutture Dati

II

Capitolo 1
Il problema del massimo sotto-array

1.1 Introduzione
Si vuole analizzare, risolvere e implementare il seguente problema :

Sia dato un vettore A di n elementi, determinare il sottoarray contiguo e non vuoto di A tale che la somma dei suoi elementi sia massima
Chiameremo tale sottoarray, massimo sottoarray. Gli n elementi presenti nel vettore A, sono interi positivi e negativi, in quanto il problema con soli numeri positivi banalmente risolto (il massimo sottoarray coinciderebbe con l'intero array A).

1.2 Soluzione Iterativa


Una prima soluzione del problema un algoritmo di tipo iterativo: una soluzione di facile progettazione e interpretazione. Dovendo trovare un sottoarray la cui somma degli elementi sia massima, possiamo pensare di partire dal primo elemento e calcolare la somma parziale tra il singolo elemento e tutti i sottoarray di dimensione via via crescente. Terminata la scansione di tutti i sottoarray aventi come primo elemento, il primo elemento del vettore, andiamo ad esaminare tutti i sottoarray aventi come primo elemento, il secondo elemento del vettore di partenza. Ci che si appena descritto ci suggerisce di utilizzare due iterazioni innestate per visitare tutti i possibili sottoarray.

1.2.1 Pseudocodice
1

CAPITOLO 1.

IL PROBLEMA DEL MASSIMO SOTTO-ARRAY

Figure 1.1: Funzionamento graco : Ricerca del massimo sottoarray

1 2 3 4 5 6 7 8 9 10 11

FIND-MAXIMUM-SUB-ARRAY(A, low, high) maxsum = -infinito for i = low to high partialsum = 0 for j = i to high partialsum = partialsum + A[j] if partialsum > maxsum maxsum = partialsum lowmax = i highmax = j return(maxsum, lowmax, highmax)
La funzione nd-maximum-sub-array prende come parametri di ingresso:

1. Il vettore A nel quale ricercare il sottovettore 2. L'indice del primo valore dal quale iniziare la ricerca. 3. L'indice dell'ultimo valore entro il quale la ricerca termina. I valori di ritorno della funzione sono anch'essi tre: 1. Il valore della somma degli elementi del massimo sottoarray. 2. L'indice estremo inferiore del sottoarray massimo. 3. L'indice estremo superiore del sottoarray massimo.

Dunque come si evince in gura 1.1, l'algoritmo esplora tutti i possibili sottovettori del vettore di partenza a partire dall'indice low no all'indice high. I valori del vettore in celle di colore bianco rappresentano valori non ancora processati nell' i-j-esima iterazione, mentre quelli di colore grigio rappresentano

Elaborato in ASD: Algoritmi e Strutture Dati

CAPITOLO 1.

IL PROBLEMA DEL MASSIMO SOTTO-ARRAY

possibili sottoarray dei quali valutarne la somma dei valori. Una variabile par-

tialsum tiene conto della somma parziale man mano che il sottoarray espande
i propri estremi; se essa risulta essere maggiore della somma dei valori di un precedente massimo sottoarray, allora il nuovo valore del massimo sottoarray proprio la somma parziale e gli estremi del massimo sottoarray si aggiornano a quelli correnti (ossia la i-j-esima iterazione). Questo ci spinge a pensare ad una possibile invariante di ciclo per dimostrare la correttezza dell'algoritmo.

1.2.2 Correttezza dell'Algoritmo


Trattasi di un algoritmo iterativo, quindi la correttezza si dimostra con il semplice principio di induzione. Nel dimostrare la correttezza abbiamo bisogno di denire un'invariante di ciclo, ossia una propriet che viene conservata (e risulta pertanto vericata) ad ogni iterazione. Sia A un vettore di n elementi dato in input alla procedura di ricerca del massimo sottoarray. Ad ogni iterazione, il valore maxsum risulta essere maggiore o uguale di partialsum. Dimostriamo dunque la correttezza con il principio di induzione.

Passo Inizializzazione:

Prima della prima iterazione del ciclo

i = 0

j = i = 0.

Dunque partialsum risulta essere pari al primo elemento

del vettore, che alla prima iterazione risulta essere banalmente il massimo sottoarray (in quanto l'unico esaminato). maxsum quindi inizializzato al primo valore di partial sum, vericando cos l'invariante di ciclo.

Passo Conservazione: Dopo ogni i-j-esima iterazione partialsum viene ricalcolato aggiungendo il valore corrente j-esimo del vettore A. A questo punto partialsum potrebbe essere superiore al valore di maxsum, ma la condizione testata dall' if ci permette di riassegnare a maxsum il valore di

partialsum, ristabilendo l'invariante di ciclo.

i= high e j = high; ma avendo dimostrato il passo di conservazione sappiamo


Passo Conclusione: L'ultima iterazione del doppio ciclo si verica con che maxsum contiene il valore corrente della somma degli elementi del massimo sottoarray. Con l'ultima iterazione, l'unico sottoarray che pu avere una somma di valori maggiore di quella corrente il sottoarray contenente solo A[j], quindi

partialsum = A[j ];

ma ancora una volta la

condizione testata ci permette di ristabilire l'invariante di ciclo, cosicch il valore di ritorno della funzione sia quello corretto.

1.2.3 Complessit
Tralasciando la complessita spaziale, analizziamo la complessit temporale del nostro algorimo in versione iterativa, salvo poi migliorarla con una versione ricorsiva. Supponiamo di avere un vettore con n elementi.

Elaborato in ASD: Algoritmi e Strutture Dati

CAPITOLO 1.

IL PROBLEMA DEL MASSIMO SOTTO-ARRAY

L'algoritmo consta di varie assegnazioni, tutte con complessit costante e di due cicli for innestati. Il ciclo pi esterno esegue una complessit pari a un ciclo ulteriore di Dunque:

(1)

iterazioni, dunque ha

(n). Per ciascuna di queste iterazioni ne viene eseguito n i iterazioni.


n

T (n) =
i=0

n (n i)

T (n) = n (n) + n (n 1) + n (n 2) + ... + n [n (n 1)] T (n) = n (n) + n (n 1) + n (n 2) + ... + n


Il che ci porta a dedurre che ordine che si presenta come

T (n)

ha una forma polinomiale del secondo

an2 + bn + c
Dato che la notazione asintotica nasconde il valore delle costanti diremo che

T (n) n2
Si pu migliorare la complessit dell'algoritmo facendo una semplice osservazione, pur non cambiando la natura iterativa. Tratteremo questo argomento nel prossimo paragrafo.

1.3 Soluzione Iterativa Semplicata


La semplicazione dell'algoritmo trattato nella precedente sezione, nasce dall'osservazione che se una serie di numeri tale da portare la somma parziale a zero oppure ad un numero negativo, allora il massimo sotto-array si trover o prima di questa serie di numeri (in tal caso gli indici estremi del massimo sottoarray si saranno gi aggiornati al loro corretto valore) o dopo di essi: si valuteranno quindi tutti i sottoarray a partire dall'indice che ha reso negativa la somma parziale).

1.3.1 Pseudocodice
Come gi accennato in precedenza, dunque, la dierenza tra i due algoritmi sta nel valutare la condizione

partialsum < 0.

1 2 3 4

FIND-MAXIMUM-SUB-ARRAY(A, low, high) maxsum = -infinito partialsum = 0 j = low


Elaborato in ASD: Algoritmi e Strutture Dati 4

CAPITOLO 1.

IL PROBLEMA DEL MASSIMO SOTTO-ARRAY

Figure 1.2: Funzionamento graco : ricerca del massimo sottoarray (versione semplicata)

5 6 7 8 9 10 11 12 13 14

for i = low to high partialsum = partialsum + A[j] if partialsum > maxsum maxsum = partialsum lowmax = j highmax = i if partialsum < 0 partialsum = 0 j = i + 1 return(maxsum, lowmax, highmax)
In gura 1.2 mostrato il funzionamento dell'algoritmo in versione semplicata. Nella gura 1.2 gli elementi in bianco rappresentano quelli non tenuti in considerazione nella i-esima iterazione, mentre quelli in grigio rappresentano il sottoarray del quale si sta valutando se la somma degli elementi sia massima. Il sottoarray dell'iterazione evidenziata in rosso, rappresenta il massimo sottoarray, da quel momento in poi gli indici lowmax e highmax non cambieranno pi in quanto non sar pi presente un sottoarray avente somma parziale superiore.

1.3.2 Correttezza dell'Algoritmo


Senza spendere ulteriori considerazioni sulla correttezza dell'algoritmo (ma focalizzandoci su quello che era il nostro obiettivo : diminuire la complessit della Elaborato in ASD: Algoritmi e Strutture Dati 5

CAPITOLO 1.

IL PROBLEMA DEL MASSIMO SOTTO-ARRAY

prima versione iterativa) citiamo quella che potrebbe essere una invariante di ciclo per la versione semplicata. Ad ogni i-esima iterazione lowmax e highmax sono gli indici estremi del sottoarray avente somma di elementi pari a maxsum. Inoltre maxsum per ogni iterazione risulta essere maggiore o al pi uguale di partialsum. E' possibile dimostrare l'invarianza di ciclo con un discorso simile a quello trattato nel precedente paragrafo. In pi bisogna giusticare la riga di codice

j = i + 1:

ma questa vera in quanto scaturisce dall'osservazione fatta in

precedenza, che risulta esser vera.

1.3.3 Complessit
La dierenza sostanziale per quanto riguarda le due versioni sin ora trattate che la prima utilizza due cicli innestati, mentre la seconda sfrutta un'interessante osservazione sul massimo sottoarray per utilizzare una sola iterazione. ed al pi delle somme, sono eseguite in tempo costante ( quindi Tutte le operazioni interne al ciclo sono eseguite volte (con n pari al numero di elementi nell'array). Giungiamo quindi a conclusione che la complessit della nuova versione dell'algoritmo lineare ed in particolare : Considerando che tutte le operazioni svolte all'interno dell ciclo sono assegnazioni

(1)). high low volte,

ossia n

T (n) (n)

1.4 Soluzione Ricorsiva


Un'ulteriore versione del nostro algoritmo di ricerca del massimo sottoarray si pu avere applicando il metodo di divide et impera. Questa soluzione prevede di suddividere il problema in pi sottoproblemi a patto che la dicolt di quest'ultimi sia inferiore a quella di partenza, risolvere i sottoproblemi e combinare opportunamente i risultati per arrivare ad una soluzione del problema di partenza. Nel particolare, dati gli indici low, high, mid rispettivamente l'indice estremo pi basso, l'indice estremo pi alto e la mediana degli indici di un vettore A, si prevede di suddividere la ricerca tra tutti i sottovettori in tre sottoproblemi: 1. La ricerca del massimo sottoarray all'interno nel sottoarray di sinistra, ossia

A [low...mid]. A [mid + 1...high]. low i < mid < j high.

2. La ricerca del massimo sottoarray all'interno del sottoarray di destra, ossia in

3. La ricerca del massimo sottoarray nel vettore che passa per mid, ossia tra tutti i sottoarray con indici estremi i e j tali che

Elaborato in ASD: Algoritmi e Strutture Dati

CAPITOLO 1.

IL PROBLEMA DEL MASSIMO SOTTO-ARRAY

Ciascuno dei tre sottoproblemi ha una dimensione decisamente inferiore a quella del problema di partenza (in quanto il vettore originario viene partizionato in due parti), dunque possibile applicare il metodo divide et impera per la soluzione ricorsiva del problema. Qualsiasi sottoarray che passa per la mediana formato da due sottoarray nella forma sottoarray nella forma

A [low...mid]e A [mid + 1...high], quindi basta trovare i massimi A [i...mid]e A [mid + 1...j ] e poi combinarli. Utilizziamo

a questo scopo una funzione di supporto Find-Max-Crossing-Subarray.

1.4.1 Pseudocodice Find-Max-Crossing-Subarray


Questa procedura non fa altro che cercare il sottoarray con somma di elementi massima, tra tutti quelli che passano per il valore di mediana mid. I parametri di ingresso della funzione sono: 1. A : vettore su cui ricercare il massimo sottoarray passante per la mediana. 2. low : indice estremo inferiore del vettore A. 3. high : indice estremo superiore del vettore A. 4. mid : indice della mediana del vettore A.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16

Find-Max-Crossing-Subarray(A, low, high, mid) leftsum = -infinito righttsum = -infinito sum = 0 for i = mid to low sum = sum + A[i] if(sum > leftsum) leftsum = sum leftindex = i sum = 0 for j = mid+1 to high sum = sum + A[j] if(sum > rightsum) rightsum = sum rightindex = j return (leftindex, rightindex, leftsum + rightsum)
L'algoritmo appena illustrato procede nel cercare il valore massimo della somma degli elementi a sinistra di mid, memorizzandone l'indice estremo inferiore, poi trova la somma massima a destra e restituisce inne gli indici del massimo sottoarray ed il valore della somma degli elementi dello stesso. Questa procedura serve solo da supporto per quella che stiamo per discutere. Prima di discutere la procedura che si servir di Find-Max-Crossing-

Subarray, spendiamo qualche parola sulla complessit della stessa.


La procedura composta da due cicli: Elaborato in ASD: Algoritmi e Strutture Dati 7

CAPITOLO 1.

IL PROBLEMA DEL MASSIMO SOTTO-ARRAY

Il primo eettua

mid low + 1

iterazioni. iterazioni.

Il secondo eettua

high mid 1

Per ciascuno dei due cicli vengono eettuate operazioni a complessit costante

(1),

dunque:

(mid low + 1) (1) + (high mid 1) (1) + (1) (mid low + 1) + (high mid 1) + (1) (mid low + 1 + high mid 1) + (1) (high low) + (1) T (n) (n)

1.4.2 Pseudocodice Find-Maximum-Subarray


In principio avevamo suddiviso il problema principale della ricerca del massimo sottoarray in tre sottoproblemi. Nella procedura seguente risolveremo ciasciuno di questi problemi utilizzando ricorsivamente la stessa funzione ed in pi utilizzando la procedura di supporto trattata nel paragrafo precedente. La procedura di ricerca del massimo sottoarray consta di tre parametri di ingresso: 1. A : Vettore all'interno del quale cercare il massimo sottoarray. 2. low : Indice estremo inferiore dell'array. 3. high : Indice estremo superiore dell'array.

1 2 3 4 5 6 7 8 9 10

Find-Maximum-Subarray(A, low, high) if low = high return (low, high, A[low]) else mid = floor(low + high / 2) (maxleft, lowleft, highleft) = Find-Maximum-Subarray(A, low, mid) (maxright, lowright, highright) = Find-Maximum-Subarray(A, mid + 1, high) (maxcross, lowcross, highcross) =
Elaborato in ASD: Algoritmi e Strutture Dati 8

CAPITOLO 1.

IL PROBLEMA DEL MASSIMO SOTTO-ARRAY

11 12 13 14 15 16 17

Find-Max-Crossing-Subarray(A, low, high, mid) if(maxleft > maxright AND maxleft > maxcross) return (maxleft, lowleft, highleft) else if(maxright > maxleft AND maxright > maxcross) return (maxright, lowright, highright) else return (maxcross, lowcross, highcross)
Analizziamo la procedura di ricerca del massimo sottoarray: La condizione di uscita dalla ricorsione che alla funzione stessa sia passato un vettore di un solo elemento (caso somma, il valore stesso dell'elemento. Se gli indici estremi non coincidono, viene calcolato l'indice mediana e si chiama ricorsivamente la funzione sulla parte sinistra, destra e sui sottovettori contenenti l'indice stesso. A questo punto ci troviamo con tre valori interi, corrispondenti alle somme dei massimi sottoarray a sinistra, destra ed a cavallo tra i due. Tra questi tre basta trovare quello pi grande e restituire gli indici associati al vettore che ha generato quella somma.

low = high);

in questo caso il massimo

sottoarray quello con indici coincidenti low e high, e con valore massimo di

1.4.3 Correttezza dell'Algoritmo


L'algoritmo analizzato in questo caso un algoritmo di tipo ricorsivo: non quindi possibile utilizzare il principio di induzione con il metodo di invariante di ciclo. Dobbiamo servirci del principio di induzione completo:

Si dimostra la correttezza nel passo base con

n = 1. n 1 chiamate corn chiamate.

Si suppongono le chiamate ricorsive corrette e quindi

rette. Sulla base di questa si dimostra la correttezza per

Dimostriamo quindi tramite il principio di induzione completa, la correttezza dell'algoritmo, supponendo per semplicit corretta la chiamata alla funzione

Find-Max-Crossing-Subarray.

Per

n = 1, gli indici low

e high, coincidono e la funzione ritorna

A [low]che

anche il massimo sottovettore in un vettore di un singolo valore.

n 1 elementi, la ricorsione opera su n elementi, ossia su un insieme contenuto in n 1 per cui le chiamate 2 ricorsive sono corrette e ritornano il valore corretto della somma degli
Supponendo corrette le chiamate su elementi del sottoarray sinistro, destro e a cavallo tra i due. A questo punto necessario trovare il valore massimo tra i tre, ma l'algoritmo tramite le tre comparazioni opera proprio trovando il valore maggiore. sottoarray. Dunque viene restituito il valore maggiore e relativamente gli indici corretti del

Dunque deduciamo che la funzione operi correttamente. Elaborato in ASD: Algoritmi e Strutture Dati 9

CAPITOLO 1.

IL PROBLEMA DEL MASSIMO SOTTO-ARRAY

1.4.4 Complessit
Per analizzare la complessit di un algoritmo ricorsivo, andiamo ad analizzare la complessit di ciasciuna operazione: una di queste fa uso proprio della funzione della quale si sta calcolando la complessit. Ci si imbatte quindi in una equazione alle ricorrenze. L'algoritmo prevede sostanzialmente dei confronti (con complessit costante), due chiamate ricorsive ed una chiamata alla funzione Find-MaxCrossing-Subarray ( complessit mensioni ciascuna pari ad

(n)

).

Le chiamate ricorsive sono eettuate sul sottoarray destro e sinistro, di di-

n 2 . L'equazione alle ricorrenze per

n>1

quindi:

T (n) = 2 T
Esso risulta

n + (n) 2

essere in forma del teorema dell'esperto n b + f (n) con a 1 e b > 1 logb a Dunque bisogna confrontare (n) con n ed essendo (n) dello stesso log2 2 ordine di grandezza di n = n la soluzione risulta essere nella forma

T (n) = a T

T (n) nlogb a lg (n) T (n) (n lg (n))


1.4.4.1 Complessit con Albero di Ricorrenza

Per trovare la stessa soluzione (o vericare quella trovata) possibile costruire l'albero di ricorrenza. Esso va ad esplorare la complessit di ogni singola chiamata ricorsiva alla funzione stessa. Il nodo della radice esplode nella complessit

n pi due rami che corrispon-

dono alle due chiamate ricorsive sulla met degli elementi. A loro volta ciascuno di questi nodi esplodono in altre due chiamate di funzione sulla met degli elementi di partenza. quello in gura 1.3 Sommando la complessit per ogni livello, otteniamo proprio L'albero che ne viene fuori

n. low =

L'ultimo livello quello costituito da tutte le chiamate a funzioni con

high,

ed il numero di queste chiamate proprio pari alla cardinalit del vettore

in input (ossia

n).
dell'albero. Per un albero binario l'altezza risulta

Dunque la complessit la somma delle complessit ad ogni livello, quindi

hn
essere

con

h pari all'altezza h = lg n + 1 dunque:

T (n) (n (lg n + 1)) T (n) (n lg n)


Troviamo dunque coerenza con il risultato ottenuto per via analitica. Elaborato in ASD: Algoritmi e Strutture Dati 10

CAPITOLO 1.

IL PROBLEMA DEL MASSIMO SOTTO-ARRAY

Figure 1.3: Albero di Ricorrenza

1.5 Realizzazione in C++


Di seguito riporteremo il listato dell'algoritmo di ricerca del massimo sottoarray implementato in C++. La funzione principale che richiama queste funzioni, non fa altro che inizializzare staticamente un vettore di 6000 interi: di questi, viene chiesto all'utente quanti ne vuole utilizzare. Supponiamo di chiamare

x il valore inserito da utente; il vettore verr inizialx x 2;+2


. Successivamente sono applicati

izzato con x valori casuali nel range

una serie di codici per apprezzare con una maggiore precisione il tempo di esecuzione della funzione stessa, ma rimandiamo il lettore ai paragra successivi per la completa spiegazione.

1.5.1 Listato v1.0


Si noti che mentre in pseudocodice possibile indicare una tripla di valori di ritorno della funzione, in C e C++ ci non possibile. Si quindi scelto di utilizzare la soluzione del passaggio di parametri per riferimento. Viceversa il valore della somma degli elementi del massimo sottoarray il valore di ritorno della funzione.

1 2 3 4 5 6

#include "functions.h" int find_maximum_sub_array (const int A[], const int low, const int high, int &low_max, int &high_max){ int partial_sum = 0;
Elaborato in ASD: Algoritmi e Strutture Dati 11

CAPITOLO 1.

IL PROBLEMA DEL MASSIMO SOTTO-ARRAY

7 8 9 10 11 12 13 14 15 16 17 18 19 20 21

int max_sum = -2147483648; int i, j; for(i = low; i <= high; i++){ partial_sum = 0; for(j = i; j <= high; j++){ partial_sum += A[j]; if(partial_sum > max_sum){ max_sum = partial_sum; low_max = i; high_max = j; } } } return max_sum; }
Le variabili locali utili alla funzione sono quattro: 1. partial_sum : variabile di tipo intero che conterr iterazione per iterazione il valore della somma parziale di ogni sottoarray. 2. max_sum : variabile di tipo intero inizializzata al minimo valore esprimibile su 32 bit (con segno), conterr il valore della somma degli elementi del massimo sottoarray. 3. i : indice del ciclo pi esterno. 4. j : indice del ciclo pi interno. Ogni iterazione esterna ha lo scopo di azzerare il valore della somma parziale (da ricalcolare per ogni sottovettore) e di stabilire un indice minimo di partenza per un sottoarray. Con l'iterazione interna invece si esplorano tutti i sottovettori aventi come indice minimo quello dell'i-esima iterazione. Per ogni j-esima iterazione si aggiorna il contenuto della somma parziale e si provvede ad aggiornare quello della somma massima se e solo se quest'ultimo inferiore alla somma parziale.

1.5.1.1

Simulazione

Per testare il funzionamento corretto della nostra subroutine mostriamo il risultato di due simulazioni : 1. Vettore di interi casuale : per mostrare la correttezza per una generica distribuzione di dati in input. 2. Vettore di interi scelti : per testare il corretto funzionamento di tutte le nostre versioni dell'algoritmo con la stessa distribuzione di dati in input.

Elaborato in ASD: Algoritmi e Strutture Dati

12

CAPITOLO 1.

IL PROBLEMA DEL MASSIMO SOTTO-ARRAY

La prima simulazione mostra il seguente risultato : A[0]: -2 A[1]: 1 A[2]: -4 A[3]: -5 A[4]: -3 A[5]: -2 A[6]: 1 A[7]: 2 A[8]: 4 A[9]: -5 Execution Time = 0.001 millisec Maximum Sub-Array Have Sum : 7 Index Values Of Maximum Sub-Array Are 6 And 8 Max Sub-Array Is 1 ; 2 ; 4 ; Utilizziamo nella seconda simulazione un esempio riportato su libro di testo , come esempio di confronto tra tutti gli algoritmi discussi. A[0]: 13 A[1]: -3 A[2]: -25 A[3]: 20 A[4]: -3 A[5]: -16 A[6]: -23 A[7]: 18 A[8]: 20 A[9]: -7 A[10]: 12 A[11]: -5 A[12]: -22 A[13]: 15 A[14]: -4 A[15]: 7 Execution Time = 0.001 millisec Maximum Sub-Array Have Sum : 43 Index Values Of Maximum Sub-Array Are 7 And 10 Max Sub-Array Is 18 ; 20 ; -7 ; 12 ;

1 Fig

4.3 di Introduzione agli algoritmi e strutture dati, terza edizione, McGraw-Hill

[T.Cormen, C. Leiserson, R.Rivest, C.Stein]

Elaborato in ASD: Algoritmi e Strutture Dati

13

CAPITOLO 1.

IL PROBLEMA DEL MASSIMO SOTTO-ARRAY

1.5.2 Listato v2.0


Di seguito viene riportata la versione semplicata ad una iterazione.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23

#include "functions.h" int find_maximum_sub_array (const int A[], const int low, const int high, int &low_max, int &high_max){ int partial_sum = 0; int max_sum = -2147483648; int i, j; j = low; for(i = low; i <= high; i++){ partial_sum += A[i]; if(partial_sum > max_sum){ max_sum = partial_sum; low_max = j; high_max = i; } if(partial_sum <= 0){ partial_sum = 0; j = i + 1; } } return max_sum; }
Il trattamento di partial_sum e max_sum rimane il medesimo; in aggiunta viene valutata la condizione di valore negativo della somma parziale: in tal caso l'indice estremo basso j viene aggiornato al prossimo valore di i.

1.5.2.1

Simulazione

Testiamo il funzionamento della seconda versione dell'algoritmo, utilizzando una distribuzione di dati in input casuale. A[0]: -3 A[1]: 3 A[2]: -5 A[3]: -2 A[4]: -3 A[5]: 1 A[6]: 0 A[7]: -2 A[8]: -4 A[9]: -4 Execution Time = 0 millisec Maximum Sub-Array Have Sum : 3 Elaborato in ASD: Algoritmi e Strutture Dati 14

CAPITOLO 1.

IL PROBLEMA DEL MASSIMO SOTTO-ARRAY

Index Values Of Maximum Sub-Array Are 1 And 1 Max Sub-Array Is 3 ; Quindi, il funzionamento con il nostro esempio base. A[0]: 13 A[1]: -3 A[2]: -25 A[3]: 20 A[4]: -3 A[5]: -16 A[6]: -23 A[7]: 18 A[8]: 20 A[9]: -7 A[10]: 12 A[11]: -5 A[12]: -22 A[13]: 15 A[14]: -4 A[15]: 7 Execution Time = 0 millisec Maximum Sub-Array Have Sum : 43 Index Values Of Maximum Sub-Array Are 7 And 10 Max Sub-Array Is 18 ; 20 ; -7 ; 12 ; Da notare il tempo di esecuzione dei due algoritmi. La stima misurata di 0 millisecondi in quanto l'algoritmo risulta cos veloce che non se ne riesce ad apprezzare una stima di misura al millesimo secondo.

1.5.3 Listato v3.0 - Ricorsione


La versione ricorsiva dell'algoritmo, come visto anche in pseudocodice, utilizza una funzione di appoggio per la ricerca di sottoarray massimi a cavallo tra il sottoarray destro e quello sinistro. seguente: La suddetta funzione di supporto la

1 2 3 4 5 6 7 8 9

#include "functions.h" #include <math.h> int find_max_crossing_subarray (const int A[], const int low, const int mid, const int high, int &low_max, int &high_max){ int i, j; int partial_sum; int right_sum;
Elaborato in ASD: Algoritmi e Strutture Dati 15

CAPITOLO 1.

IL PROBLEMA DEL MASSIMO SOTTO-ARRAY

10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32

int left_sum; //LEFT-SIDE-FIND-MAX-SUB-ARRAY partial_sum = 0; left_sum = -65535; for(i = mid; i >= low; i--){ partial_sum += A[i]; if(partial_sum > left_sum){ left_sum = partial_sum; low_max = i; } } //RIGHT-SIDE-FIND-MAX-SUB-ARRAY partial_sum = 0; right_sum = -65535; for(j = mid+1; j <= high; j++){ partial_sum += A[j]; if(partial_sum > right_sum){ right_sum = partial_sum; high_max = j; } } return left_sum + right_sum; }
All'interno della funzione troviamo variabili utili allo scope della funzione, tra cui:

right_sum : memorizza la somma di elementi dei sottoarray di destra. left_sum : memorizza la somma di elementi dei sottoarray di sinistra partial_sum : memorizza il valore parziale della somma per valutare se quest'ultima massima.

La funzione si suddivide in due fasi, una per la ricerca del massimo sottovettore di sinistra (rispetto all'elemento mid ) e l'altra per la ricerca a destra. Si noti infatti che gli indici del ciclo di sinistra sono decrescenti, in quanto a noi interessa trovare un sottoarray che contenga mid e che sia massimo: logica quindi la scelta di partire ad esaminare il vettore da mid per poi decrescere. Dualmente nel secondo ciclo si parte dall'elemento subito dopo mid (per non includere nella somma due volte il valore di mid ), per poi crescere verso la ne del vettore. Anche qui c' da notare che tra i parametri di ingresso risultano due variabili passate per riferimento : esse rappresentano i valori degli indici del massimo sottoarray, che precedentemente avevamo segnalato come valori di ritorno. Entriamo dunque nel vivo della funzione di calcolo del massimo sottoarray, il cui codice segue.

Elaborato in ASD: Algoritmi e Strutture Dati

16

CAPITOLO 1.

IL PROBLEMA DEL MASSIMO SOTTO-ARRAY

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45

#include "functions.h" #include <math.h> int find_maximum_subarray (const int A[], const int low, const int high, int &low_max, int &high_max){ int mid; int left_low, left_high; int right_low, right_high; int cross_low, cross_high; signed int left_max_sum, right_max_sum, cross_max_sum; if(low == high){ low_max = low; high_max = high; return A[low]; } else{ mid = floor((double)((low + high)/2)); //LEFT SIDE left_max_sum = find_maximum_subarray(A, low, mid, left_low, left_high); //RIGHT SIDE right_max_sum = find_maximum_subarray(A, mid+1, high, right_low, right_high ); //CROSS cross_max_sum = find_max_crossing_subarray (A, low, mid, high, cross_low, cross_high); //FIND MAX VALUE if(left_max_sum >= right_max_sum && left_max_sum >= cross_max_sum){ low_max = left_low; high_max = left_high; return left_max_sum; }else if(right_max_sum >= left_max_sum && right_max_sum >= cross_max_sum){ low_max = right_low; high_max = right_high; return right_max_sum; }else{ low_max = cross_low; high_max = cross_high; return cross_max_sum; } } }

Elaborato in ASD: Algoritmi e Strutture Dati

17

CAPITOLO 1.

IL PROBLEMA DEL MASSIMO SOTTO-ARRAY

L'algoritmo

il

medesimo

di

quello di

visto

in

pseudocodice. che conter-

Nell'implementazione

abbiamo

bisogno

dichiarare

variabili

ranno i valori massimi di somma, e gli indici dei sottoarray di destra, sinistra ed a cavallo tra i due. Gli indici estremi inferiori saranno memorizzati in left_low,

right_low e cross_low, quelli alti in left_high, right_high, cross_high, mentre


le somme in left_max_sum, right_max_sum e cross_max_sum. L'unica nota da aggiungere l'inserimento della libreria <math.h> per l'utilizzo della funzione oor per il calcolo della mediana.

1.5.3.1

Simulazione

Anche in questo caso testiamo il funzionamento della nostra funzione in C/C++ La prima simulazione sar eettuata su una distribuzione di dati in input casuale e dar come risultato: A[0]: -1 A[1]: -1 A[2]: 0 A[3]: 0 A[4]: 2 A[5]: -1 A[6]: -4 A[7]: 4 A[8]: 3 A[9]: -2 Execution Time = 0 millisec Maximum Sub-Array Have Sum : 7 Index Values Of Maximum Sub-Array Are 7 And 8 Max Sub-Array Is 4 ; 3 ; Mentre la seconda sulla nota distribuzione di dati in input, gi utilizzata in precedenza. A[0]: 13 A[1]: -3 A[2]: -25 A[3]: 20 A[4]: -3 A[5]: -16 A[6]: -23 A[7]: 18 A[8]: 20 A[9]: -7 A[10]: 12 A[11]: -5 A[12]: -22

Elaborato in ASD: Algoritmi e Strutture Dati

18

CAPITOLO 1.

IL PROBLEMA DEL MASSIMO SOTTO-ARRAY

A[13]: 15 A[14]: -4 A[15]: 7 Execution Time = 0 millisec Maximum Sub-Array Have Sum : 43 Index Values Of Maximum Sub-Array Are 7 And 10 Max Sub-Array Is 18 ; 20 ; -7 ; 12 ; Il quale il medesimo risultato ottenuto con gli altri due codici precedenti. Con questo si chiude la trattazione sul funzionamento del nostro algoritmo; adesso andremo a valutarne le prestazioni.

1.5.4 Valutazione delle prestazioni


Cercheremo in questo paragrafo di dimostrare i risultati ottenuti analiticamente, sulla complessit degli algoritmi di ricerca del massimo sottoarray. Prima di mostrare la procedura di analisi dei tempi dei programmi scritti in C/C++ ricordiamo la denizione della notazione asintotica

(g (n))

f (n) (g (n)) {f (n) : c1 , c2 , n0 > 0|c1 g (n) < f (n) < c2 g (n) , n n0 }
Ossia, dire che una funzione inferiore alla funzione che

due costanti positive che moltiplicate per

(g (n)) signica dire che possibile trovare g (n) fanno da limite sia superiore che (n), a partire da un certo n0 .

Per la valutazione dei tempi di esecuzione utilizzeremo la libreria sys/-

time.h, che utilizza la variabile clocks_per_sec, ossia quanti colpi di clock


ci sono in un secondo. In sostanza andremo a misurare di quanti colpi di clock necessita la nostra funzione, per poi dividerli per il numero di clock contenuti in un secondo. Siccome le frequenze di clock dei processori moderni sono molto elevate, eseguire una sola volta l'algoritmo darebbe risultati prossimi allo zero (o addirittura zero). Quindi per una misurazione pi precisa eettueremo mille volte l'operazione di ricerca del massimo sottoarray, cos da apprezzare un tempo di esecuzione maggiore di zero, salvo poi dividere il risultato per mille ed ottenere cos il tempo di esecuzione di una sola chiamata di funzione. Per gracare l'andamento temporale al crescere della cardinalit del vettore di ingresso, l'operazione appena descritta verr eettuata per distribuzioni di dati in ingresso di dimensione via via crescendo: in particolare partiremo da un vettore di 100 elementi, per arrivare (di dieci in dieci) a 3000 elementi.

1.5.4.1

Prestazioni versione iterativa a doppio ciclo

In gura 1.4 mostriamo il graco dell'andamento temporale della prima versione del nostro algoritmo. Ricordiamo che la complessit trovata era In questo caso le costanti trovate sono: Elaborato in ASD: Algoritmi e Strutture Dati 19

T (n) n2

CAPITOLO 1.

IL PROBLEMA DEL MASSIMO SOTTO-ARRAY

Figure 1.4: Graco andamento temporale algoritmo v1.0

Figure 1.5: Graco andamento temporale algoritmo v2.0

c1 =

1 250 1 1000

c2 =
1.5.4.2

Prestazioni versione iterativa a singolo ciclo

In gura 1.5 il graco dell'andamento temporale della seconda versione del nostro algoritmo. Ricordiamo che la complessit trovata era Le costanti trovate sono:

T (n) (n)

c1 =

1 200 1 90
20

c2 =

Elaborato in ASD: Algoritmi e Strutture Dati

CAPITOLO 1.

IL PROBLEMA DEL MASSIMO SOTTO-ARRAY

Figure 1.6: Graco andamento temporale algoritmo ricorsivo

1.5.4.3

Prestazioni versione ricorsiva

In gura 1.6 il graco dell'andamento temporale della terza ed ultima versione del nostro algoritmo. Ricordiamo che la complessit trovata era

T (n)

(n lg n)
Le costanti trovate sono:

c1 =

1 300 1 40

c2 =

1.6 Conclusioni
Con l'analisi delle prestazioni degli algoritmi si chiude la trattazione del problema di ricerca del massimo sottoarray. Tra le soluzioni implementate, la prima risulta ovviamente la pi lenta, in quanto al crescere della cardinalit del vettore in input, i tempi crescono con il quadrato. Inoltre stesso in fase di simulazione si riscontrato un tempo di esecuzione totale del programma (da 100 a 3000 valori) improponibile per un semplice algoritmo; diverso il caso degli altri due che hanno necessitato decisamente di meno tempo per l'esecuzione.

Elaborato in ASD: Algoritmi e Strutture Dati

21

Capitolo 2
Topological sort

2.1 Introduzione
Prima di formalizzare il problema dell'ordinamento topologico, bene dare alcune denizioni utili a comprenderlo a fondo. Le denizioni provengono dalla

teoria dei gra.


Un grafo non orientato G = (V, E ) denito da un insieme nito V (G) = {v1 , v2 , ..., vn }di elementi detti nodi o vertici e da un insieme E (G) = {e1 , e2 , ..., em } V xV di coppie non ordinate di nodi dette archi o spigoli. Se ad ogni arco, associamo una direzione (uscente da un vertice ed entrante in un altro), otteniamo un grafo orientato. Un grafo orientato, pu denirsi aciclico se non presenta cicli diretti, ossia se partendo da un nodo qualsiasi non possibile tornare ad esso percorrendo nel giusto verso gli archi. Deniamo dunque l'ordinamento topologico, che il cuore del nostro problema. Denisco ordinamento topologico, un ordinamento lineare di tutti i vertici di un grafo aciclico orientato. 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. L'algoritmo su cui si basa quello dell'ordinamento topologico, la visita in profondit (DFS) di un grafo orientato. Basandoci sulla DFS, l'ordinamento topologico risulta piuttosto semplice, in quanto non altro che una stampa di uno stack, all'interno del quale sono memorizzati man mano i nodi visitati dalla DFS.

22

CAPITOLO 2.

TOPOLOGICAL SORT

Figure 2.1: Ordine di scoperta nodi in DFS

2.2 Depth-First-Search
La visita in profondit Depth-First-Search (DFS) di un grafo consiste nella esplorazione sistematica di tutti i vertici andando in ogni istante il pi possibile in profondit. Gli archi vengono esplorati a partire dall'ultimo vertice scoperto

v che abbia ancora archi non esplorati uscenti e quando questi sono niti si
torna indietro per esplorare gli altri archi uscenti dal vertice dal quale v era stato scoperto. Il procedimento continua no a quando non vengono scoperti tutti i vertici raggiungibili dal vertice sorgente originario. Se al termine rimane qualche vertice non scoperto, uno di questi diventa una nuova sorgente e si ripete la ricerca a partire da esso. E' possibile visualizzare uno dei possibili ordini di scoperta dei nodi (con nodo sorgente '1') in gura 2.1 Per creare un sistema di tracciamento dello stato di visita, ad ogni nodo viene associato un colore:

Ogni vertice inizialmente bianco. E' grigio quando viene scoperto. Viene reso nero quando la visita nita, cio quando la sua lista di adiacenza stata completamente esaminata.

Oltre al colore, si associano ad ogni vertice due informazioni temporali: 1. Tempo di inizio visita: quando un nodo reso grigio per la prima volta. 2. Tempo di ne visita: quando reso nero. Elaborato in ASD: Algoritmi e Strutture Dati 23

CAPITOLO 2.

TOPOLOGICAL SORT

Il tempo un intero compreso fra 1 e due volte il numero di vertici, poich ogni vertice pu essere scoperto una sola volta e la sua visita pu nire una sola volta.

2.2.1 Pseudocodice
Di seguito specichiamo in pseudocodice il funzionamento della DFS

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18

DFS(G) Per ogni u in V[G] u.color = WHITE u.parent = NIL time = 0 Per ogni v in V[G] if v.color = WHITE DFS-Visit(v) DFS-Visit(u) u.color = GREY u.starttime = time = time + 1 Per ogni v in Adj(u) if v.color = white v.parent = u DFS-Visit(v) u.color = black u.endtime = time = time + 1
L'algoritmo DFS comincia con l'inizializzazione di tutti i nodi. Nel particolare tutti i nodi sono inizialmente bianchi e con predecessore nullo. Si noti l'utilizzo di un contatore del tempo, inizializzato a zero. Come da denizione di DFS, si comincia la visita da un qualsiasi nodo che non sia ancora stato esplorato (quindi di colore bianco) per poi andare pi in profondit possibile richiamando ricorsivamente la DFS-Visit. Non appena inizia la visita in profondit di un nodo si pone il colore a grigio e si marca il tempo di inizio visita, per poi andare a valutare i nodi adiacenti al nodo in questione: se uno di questi colorato di bianco, si imposta la dipendenza di predecessore e si inizia una nuova visita sul nodo adiacente bianco. Terminati i nodi adiacenti bianchi, il nodo pu essere marcato di nero e se ne pu registrare il tempo di ne visita.

2.2.2 Correttezza dell'algoritmo


Lo scopo della visita in profondit quello di andare a scoprire man mano tutti i nodi presenti all'interno del grafo, andando ogni volta pi in profondit possibile. In prima battuta bisogna dimostrare la correttezza della DFS-Visit ed in seguito quella della DFS.

Elaborato in ASD: Algoritmi e Strutture Dati

24

CAPITOLO 2.

TOPOLOGICAL SORT

DFS-Visit un algoritmo ricorsivo e per tanto ne dimostriamo la correttezza tramite principio di induzione completa; la visita pu dirsi terminata quando tutti i nodi connessi all'origine sono colorati di nero.

Passo Base. Per

n = 1,

il nodo viene scoperto e colorato di grigio, menDunque non si entra nel ciclo e

tre la lista di adiacenza Adj vuota.

non si richiama ricorsivamente la funzione. Successivamente il nodo viene colorato di nero, ma essendo l'unico nodo, ci fa terminare la visita di profondit.

Passo Induttivo. acenza.

Supponendo che la chiamata ricorsiva operi corretta-

mente, essa viene eettuata su tutti i nodi contenuti nella lista di adiMa al pi un nodo pu essere connesso a tutti gli

n1

nodi

restanti del grafo, ossia un numero inferiore ad

n.

Qualora vi fossero al-

tri nodi connessi all'origine tramite un cammino semplice, essi sarebbero presenti nella lista di adiacenza di un nodo intermedio, che chiamerebbe correttamente la funzione; se non fossero connessi tramite un cammino semplice, non rientrerebbero nella denizione di visita in profondit. Per ipotesi induttiva l'algoritmo opera correttamente. Dimostrare la correttezza della DFS quindi banale, in quanto l'algoritmo non fa altro che richiamare la visita in profondit su ogni nodo del grafo.

Se avessimo di

n nodi indipendenti e quindi non connessi, cadremmo nel caso

chiamate base alla visita in profondit.

Se i nodi sono connessi, la chiamata alla DFS-Visit eettuata solo per i nodi bianchi, e dato che una chiamata corretta (colora di nero tutti i nodi connessi all'origine), scopriremo tutti i nodi del grafo.

2.2.3 Complessit
Per un analisi pi precisa della complessit della visita in profondit (che poi vedremo coincidere con la complessit dell'ordinamento topologico), ricorriamo all'analisi ammortizzata. Se volessimo ricorrere alle equazioni con ricorrenze ci troveremmo di fronte a una soluzione poco accurata, invece con un analisi attenta del codice ci rendiamo conto di conoscere gi esattamente quante iterazioni l'algoritmo eettuer, ossia siamo in grado di fare un analisi ammortizzata. Per quanto riguarda la DFS-Visit, siamo in grado di dire che avvengono (nel ciclo for) 'E' confronti, dove E il numero di archi presenti nel grafo G. Utilizzando per l'analisi ammortizzata ci accorgiamo che questo un limite superiore (ossia la complessit un bianco. Dato che la prima cosa che la DFS-Visit fa marcare il nodo di grigio, essa viene richiamata in un numero di volte che al pi E. Diremo quindi che la complessit

O (E )):

infatti vero che avvengono E

confronti, ma la funzione ricorsiva viene richiamata solo se il nodo marcato

(E ).

Elaborato in ASD: Algoritmi e Strutture Dati

25

CAPITOLO 2.

TOPOLOGICAL SORT

La DFS invece un semplice algoritmo iterativo, che richiama la DFS-Visit per ogni suo vertice. In realt abbiamo detto anche precedentemente, che la chiamata alla DFS-Visit viene fatta per ogni nodo solo se della DFS chiama

E = {}

(caso peggiore

per la DFS). Dunque anche in questo caso, anzich aermare che l'iterazione diremo che la chiama

O (V ) volte (V ).

la DFS-Visit (dove V l'insieme dei vertici),

La complessit della DFS risulta dunque

T (n) (V + E )

2.3 Topological Sort


L'ordinamento topologico un ordinamento denito su i vertici di un grafo orientato aciclico (directed acyclic graph DAG). Si pu pensare all'ordinamento topologico come ad un modo per ordinare i vertici di un DAG lungo una linea orizzontale in modo che tutti gli archi orientati vadano da sinistra verso destra: questo ci da l'idea che i DAG sono utilizzabili per modellare successioni di eventi temporali. Abbiamo introdotto la visita in profondit (DFS) con marcatura temporale in quanto secondo l'ordinamento topologico:

Un vertice v il cui tempo di ne visita e' successivo ad un vertice u, dovra' precederlo nell'ordinamento nale.

2.3.1 Pseudocodice
Sfruttando dunque l'algoritmo DFS, il topological sort risulta banale: baster inserire in una coda i nodi, man mano che diventano neri, per poi leggerli dalla testa (per cui il primo che diventa nero, il primo ad esser letto).

1 2 3 4 5 6

Topological-Sort(G) Chiama DFS(G) per calcolare tutti i tempi di fine visita Data Q coda di vertici Per ogni v in V(G) Q.Enqueue(v) ogni volta che termino la visita su un nodo ( quando diventa nero) return Q
Sostanzialmente il funzionamento del topological-sort sta tutto nella visita in profondit di un grafo. Infatti per quanto riguarda la complessit di questo algoritmo, dato che l'accodamento ha una complessit l'algoritmo ha una complessit pari a quella della DFS, ossia

(1), diremo (V + E ).

che

2.3.2 Funzionamento graco


La gura 2.2 mostra i tempi di inizio e ne visita DFS, dato un grafo orientato aciclico, di ogni nodo. Elaborato in ASD: Algoritmi e Strutture Dati 26

CAPITOLO 2.

TOPOLOGICAL SORT

Figure 2.2: Tempi di inizio e ne visita DFS

Figure 2.3: Ordinamento topologico

L'ordinamento topologico invece da luogo al graco in gura 2.3. La gura 2.3 non presenta archi all'indietro, ed dunque un ordinamento topologico corretto. Nella gura 2.2 sono stati evidenziati anche i tempi di inizio e ne visita della DFS. Ordinare topologicamente equivale semplicemente ad ordinare i nodi a seconda del loro tempo di ne visita (dal pi grande al pi piccolo).

2.3.3 Correttezza
Per quanto riguarda la verica della correttezza di questo tipo di algoritmi, dobbiamo prendere in prestito i teoremi sulla teoria del gra.

topological-sort produce un ordinamento topologico di un grafo orientato aciclico G.


E' suciente dimostrare che per ogni coppia di nodi arco

(u, v ) V ,

se esiste un

(u, v )

allora il tempo di ne visita in v minore di quello in u.

Elaborato in ASD: Algoritmi e Strutture Dati

27

CAPITOLO 2.

TOPOLOGICAL SORT

Prima di dimostrare questo, deniamo i tipi di archi in base al colore dei nodi e deniamo

d[u] = T empoInizioV isita, f [u] = T empoF ineV isita

se v bianco. L'arco un arco d'albero se v grigio. L'arco un arco di ritorno se v nero. L'arco un arco in avanti o un arco di attraversamento se inoltre se

d[u] < d[v ]

allora un arco in avanti

d[u] > d[v ]

allora un arco di attraversamento

2.3.3.1

Teorema del cammino bianco

In una foresta DFS, un nodo v discendente di u, se e solo se al tempo contenente esclusivamente nodi bianchi.

d[u]

(in cui la visita scopre u), il vertice v e raggiungibile da u con un cammino

2.3.3.2

Teorema sui gra aciclici

Un grafo orientato aciclico se e solo se l'algoritmo DFS su G non trova alcun arco di ritorno. Supponiamo che G contenga un ciclo c. Sia v il primo vertice scoperto in c, e

(u, v )

l'arco che lo precede in c.

Allora, al tempo

d[v ],

c' un percorso

bianco da v a u. Per il teorema del cammino bianco, sappiamo che u diventa un discendente di v nella foresta DF. Perci

(u, v )

deve essere un arco di ritorno.

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 forma un ciclo, quindi il grafo G non aciclico. non trova archi di ritorno, il grafo aciclico.

(u, v ),

Ne concludiamo che se DFS

2.3.3.3

Correttezza del topological sort

Dunque, constatata l'assenza di archi di ritorno in un grafo aciclico possiamo dire che per ogni coppia

(u, v ) V ,

l'arco

(u, v )

pu essere:

L'arco di un albero. consegue che

Quindi v bianco e diventa discendente di u; ne

f [v ] < f [u]
poich v gi diventato nero prima di aver concluso la

Un arco in avanti o di attraversamento. In tal caso v nero e ne consegue che

f [v ] < f [u]

visita di u.

Elaborato in ASD: Algoritmi e Strutture Dati

28

CAPITOLO 2.

TOPOLOGICAL SORT

2.4 Realizzazione in C++


2.4.1 Codice topological sort
Come gi abbondantemente accennato, il topological sort utilizza prevalentemente DFS per eseguire l'ordinamento. topologico: Dunque riporteremo qui di seguito il codice della DFS e della DFS-Visit con una variante per realizzare l'ordinamento sostanzialmente ogni volta che termina la visita su di un nodo, esso va inserito all'interno di un vettore, che poi verr letto in ordine contrario (in quanto i tempi di ne visita sono in ordine decrescente) cos da simulare grossolanamente il funzionamento di una coda.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36

#include "dfs.h" #include <iostream> using std::cout; using std::endl; extern int time_visit; extern int index; void DFS(char G[], node_set V, const int riemp, const AdjMatrix Adj, node_set inorder){ int i; for(i = 0; i < riemp; i++){ V[i].id = i; V[i].label = G[i]; V[i].color = w; V[i].parent = 0; V[i].time_start = 0; V[i].time_end = 0; } time_visit = 0; index = 0; for(i = 0; i < riemp; i++){ if(V[i].color == w){ DFS_Visit(V[i], V, Adj, riemp, inorder ); } } index--; } void DFS_Visit(node &u, node_set V, const AdjMatrix Adj, const int riemp, node_set inorder){ int i; u.color = g; u.time_start = ++time_visit; for(i = 0; i < riemp; i++){ if(Adj[u.id][i] == 1){

Elaborato in ASD: Algoritmi e Strutture Dati

29

CAPITOLO 2.

TOPOLOGICAL SORT

37 38 39 40 41 42 43 44 45 46 47 48

if(V[i].color == w){ V[i].parent = u; DFS_Visit(V[i], V, Adj, riemp, inorder); } } } u.color = b; u.time_end = ++time_visit; inorder[index].label = u.label; inorder[index].time_end = u.time_end; index++; }
E' necessario anche riportare parte dell'header le, in quanto necessario visualizzare la struttura di un nodo di un grafo. Ogni nodo presenta un numero identicativo (utile per individuarlo nella matrice di adiacenza), un'etichetta per il riconoscimento da parte dell'utente, un colore, un predecessore, un tempo di inizio visita e ne visita.

1 2 3 4 5 6 7 8 9 10

struct node{ int id; char label; char color; //w = WHITE, g = GRAY, b = BLACK char parent; int time_start; int time_end; }; typedef node node_set[DIM];
L'algoritmo della DFS stato realizzato tramite matrici di adiacenza, dove la i-j esima locazione contiene un 1 se il nodo individuato dalla riga i ha un arco verso il nodo individuato dalla colonna j. L'algoritmo quello gi visualizzato in pseudocodice. Commentiamo per alcune piccole aggiunte dovute all'implementazione. Sono presenti due variabili extern, ossia due variabili dichiarate globali in altri le a cui possibile accedere (in lettura e scrittura) in questo le. Sebbene l'utilizzo di variabili globali (ed ancor pi di tipo extern) sconsigliato nella programmazione, le utilizzeremo in quanto il progetto di piccole dimensioni e trattasi di una simulazione a scopo solo didattico. Una prima variabile conteggia gli istanti di tempo, la seconda conta quanti elementi sono inseriti di volta in volta nell vettore nale da cui leggere l'ordine dei nodi. A questo punto per ogni nodo bianco viene richiamata la DFS-Visit. Essa colora il nodo che sta visitando ed esplora la matrice di adiacenza partendo dalla riga relativa al nodo da visitare, per valutare eventuali nodi

Elaborato in ASD: Algoritmi e Strutture Dati

30

CAPITOLO 2.

TOPOLOGICAL SORT

adiacenti. Se uno di questi nodi adiacenti bianco, la procedura opera ricorsivamente. In seguito viene aumentato il tempo e segnato nel nodo che ha terminato la visita; l'etichetta del nodo viene poi aggiunta alla lista ordinata topologicamente. Baster stampare in ordine inverso questa lista per ottenere un corretto ordinamento topologico. L'output di questo algoritmo scritto in C++, a seguito di un input in gura 2.2 mostrato in gura 2.4. Nell'output del programma troviamo anche segnati i tempi di inizio e ne visita, perfettamente coincidenti con l'analisi graca eettuata in precedenza.

2.4.2 Analisi delle prestazioni


Precedentemente avevamo aermato che la complessit del topological sort fosse

(V + E ).
esecuzione.

Andiamo a simulare il nostro algoritmo registrandone i tempi di

La simulazione consta nell'ordinamento topologico di 256 gra diversi (rispettivamente aventi da 1 a 255 nodi). La matrice di adiacenza viene generata automaticamente con una procedura randomizzata. In seguito l'ordinamento eseguito mille volte, salvo poi dividere il tempo di esecuzione per mille. Gracando i risultati otteniamo il risultato in gura 2.5. A sinistra possibile visualizzare l'andamento temporale al crescere di vertci e transizioni. A destra possibile vedere l'esplicazione della notazione tetha. Si noti che

2901 < n0 < 4148


Le costanti utilizzate sono:

c1 =

1 40 1 50

c2 =

Elaborato in ASD: Algoritmi e Strutture Dati

31

CAPITOLO 2.

TOPOLOGICAL SORT

Figure 2.4: Output topological sort

Elaborato in ASD: Algoritmi e Strutture Dati

32

CAPITOLO 2.

TOPOLOGICAL SORT

Figure 2.5: Andamento temporale dell'ordinamento topologico

Elaborato in ASD: Algoritmi e Strutture Dati

33