Lez. 121 Universita' di Ferrara Facolta' di Scienze Matematiche, Fisiche e Naturali Laurea Specialistica in Informatica Algoritmi Avanzati Progettazione di algoritmi: stepwise refinement e differenziazione formale topological sort di un grafo orientato Copyright © by Claudio Salati.
2 TOPOLOGICAL SORT s E' UN INSIEME TRA I CUI ELEMENTI E' DEFINITO UN ORDINAMENTO PARZIALE DESCRITTO DALLA RELAZIONE after after E' UN SOTTOINSIEME DEL PRODOTTO CARTESIANO s s, CIOE' UN INSIEME DI COPPIE (x, y), TALI CHE y after x. INDICHIAMO CON after(x) L'INSIEME DI ELEMENTI CHE SEGUONO DIRETTAMENTE x NELL'ORDINAMENTO PARZIALE, cioe' after(x) = { y s | (x, y) after } after PUO' ESSERE VISTA COME UNA FUNZIONE MULTIVALORE DA s A s, O COME UNA FUNZIONE DA s A POWERSET(s) after e': asimmetrica, transitiva (ma le relazioni ottenibili per transitivita' non sono rappresentate), non riflessiva
3 TOPOLOGICAL SORT Problema: A PARTIRE DA after VOGLIAMO DEFINIRE UN ORDINAMENTO TOTALE order CHE SIA COMPATIBILE CON after. N.B.: DI ORDINAMENTI TOTALI COMPATIBILI CON after CE NE POSSONO ESSERE DIVERSI: uno qualunque ci va bene. Esempio: –s = { A, B, C, D } –after(A) = { B, C }, after(B) = D, after(C) = D –Ordinamenti totali compatibili: A, B, C, D A, C, B, D AD B C
4 TOPOLOGICAL SORT L'INSIEME s E LA RELAZIONE after POSSONO ESSERE RAFFIGURATI COME UN GRAFO ORIENTATO ACICLICO: s = INSIEME DEI NODI after = INSIEME DEI LATI IL PROBLEMA E' CHIAMATO TOPOLOGICAL SORT DI UN GRAFO L'ALGORITMO DI TOPOLOGICAL SORT PUO' ANCHE SERVIRE A DETERMINARE SE UN GRAFO ORIENTATO E' ACICLICO, CIOE' SE I SUOI LATI SONO TALI DA DEFINIRE UN ORDINAMENTO PARZIALE DEI NODI SE IL GRAFO E' ACICLICO, ALLORA E' POSSIBILE DEFINIRE UN ORDINAMENTO TOTALE order COMPATIBILE CON after SE IL GRAFO E' CICLICO, CIO' NON E' POSSIBILE. ESEMPI DI APPLICAZIONE: LAVORI DA FARE IN UN PROGETTO PREREQUISITI TRA CORSI DI UN PROGRAMMA DI STUDI ORDINAMENTO DEGLI ARGOMENTI IN UN LIBRO
5 TOPOLOGICAL SORT // algoritmo base void topSort(SET s, // in SET after, // in LIST & order) { // out order = <>; // LISTA VUOTA 1 while ( next s | ( prec s | (prec, next) after)) { 2 // RIMUOVO next DA s s = s - {next}; 3 // AGGIUNGO ORDINATAMENTE (in fondo) // next A order order = order + ; 4 } if (s != ) order = UNDEFINED; 5 // GRAFO CON CICLI } 6
6 TOPOLOGICAL SORT L'ALGORITMO E' MOLTO INEFFICIENTE PERCHE' E' PIENO DI CICLI, IMPLICITI E/O ESPLICITI next s IMPLICA UNA SCANSIONE DEL SET prec s IMPLICA UNA SCANSIONE DEL SET |(prec, next) after IMPLICA UNA SCANSIONE DEL SET E CIO' COMUNQUE s E after SIANO RAPPRESENTATI IL PROBLEMA E' CHE AD OGNI ITERAZIONE DEL CICLO while CONTINUIAMO A RICALCOLARE SEMPRE LE STESSE INFORMAZIONI MA ALLORA UNA VOLTA CHE LE ABBIAMO CALCOLATE, LE POSSIAMO MEMORIZZARE IN VARIABILI AGGIUNTIVE PER POI UTILIZZARLE NELLE ITERAZIONI SUCCESSIVE! Ovviamente ogni iterazione deve anche modificare il valore di queste variabili aggiuntive, cosi' da mantenerle aggiornate e congruenti
7 TOPOLOGICAL SORT prec s | (prec, next) after PER OGNI next s POSSIAMO CONTARE UNA VOLTA PER TUTTE QUANTI SONO I NODI prec | (prec, next) after DEFINIAMO UNA FUNZIONE numBefore CHE PER OGNI NODO DI s INDICA IL SUO IN-DEGREE (IL NUMERO DI LATI ENTRANTI) UNA FUNZIONE E' UNA SPECIALE RELAZIONE, ED E' QUINDI DI NUOVO DESCRIVIBILE COME UN INSIEME DI COPPIE (nodo, numero) CON nodo s E numero N (NATURALI) PER MANTENERE AGGIORNATO numBefore(n) OCCORRE E BASTA DECREMENTARE numBefore(n) ( n after(next) ) QUANDO INSERISCO next NELL'ORDINAMENTO TOTALE
8 TOPOLOGICAL SORT // primo raffinamento void topSort(SET s, // in SET after, // in LIST & order) { // out 1 PAIR inDegree { 2 node nodo; 3 int numero; }; 4 order = <>; // LISTA VUOTA 5 SET numBefore = {(n, 0) : n s}; 6 for ((prec, next) after) numBefore(next) += 1; 7 while ( next s | numBefore(next)==0) { 8 // RIMUOVO next DA s s = s - {next}; 9 for (n after(next)) numBefore(n) -= 1; 10 // AGGIUNGO next A order order = order + ; 11 } if (s!= ) order = UNDEFINED; // GRAFO CON CICLI 12 } 13
9 TOPOLOGICAL SORT In questa versione di topSort() ogni lato del grafo e' scandito una sola volta (per decrementare after(next) ) N.B.: a patto di poter facilmente calcolare after(next) ovviamente si e' dovuto aggiungere l'inizializzazione di numBefore, ma questa e' O(#s + #after) c'e' pero' ancora un ciclo implicito di scansione di s : next s | numBefore(next)==0 ma noi, quando inizializziamo numBefore e quando lo aggiorniamo, sappiamo bene se e' 0 o no! percio' possiamo definire una nuova variabile, ready, che e' l'insieme dei nodi di s tali che il relativo numBefore sia 0 in questo modo next s | numBefore(next)==0 diventa ready != l'inizializzazione di ready e' O(#s)
10 TOPOLOGICAL SORT // raffinamento finale void topSort(SET s, // in SET after, // in LIST & order) { // out PAIR inDegree { node nodo; int numero; }; // inizializzazione order = <>; // LISTA VUOTA SET numBefore = {(n, 0) : n s}; for ((prec, next) after) numBefore(next) += 1; SET ready = { n s | numBefore(n)==0 }; // continua alla pagina seguente
11 TOPOLOGICAL SORT // raffinamento finale: continua while (ready != ) { // estrae un elemento qualunque da ready next = extractFrom(ready); // RIMUOVO next DA s s = s - {next}; for (n after(next)) { numBefore(n) -= 1; if (numBefore(n) == 0) { ready = ready {n}; } // AGGIUNGO next A order order = order + ; } if (s != ) order = UNDEFINED; // GRAFO CON CICLI }
12 TOPOLOGICAL SORT tutto l'algoritmo e' stato reso O(#s+#after) in quanto nodi e lati del grafo sono scanditi un numero costante di volte pero': con quali strutture dati si possono rappresentare s, after, numBefore, ready, order ? fino ad ora non se ne e' parlato, ma anche per le strutture dati si puo' operare per raffinamenti successivi. ovviamente, perche' l'algoritmo abbia effettivamente la complessita' indicata occorre che gli accessi previsti alle strutture dati abbiano tempo indipendente da #s e #after (le dimensioni di numBefore e ready dipendono da #s); e.g.: estrazione di un nodo noto da s estrazione di un elemento a caso di ready inserimento di un elemento (in un posto qualunque) in ready inserimento di un elemento in coda a order scansione di tutti e soli i lati uscenti di un nodo
13 TOPOLOGICAL SORT Esercizio: completare la definizione in C della funzione topSort() utilizzando strutture dati che garantiscano che la sua complessita' sia O(#s+#after) N.B.: per ready si dice di estrarre un elemento a caso. Non avremmo dovuto estrarre gli elementi da ready in ordine FIFO? In base alla risposta progettare la struttura dati piu adatta per realizzare ready.
14 DIFFERENZIAZIONE FORMALE Per passare dall'algoritmo iniziale a quello finale si e' applicata due volte una tecnica di programmazione detta differenziazione formale. Questa tecnica consiste nell'introdurre variabili addizionali per mantenere informazioni che e' necessario avere a disposizione e che altrimenti sarebbe necessario ri/calcolare. Non necessariamente queste variabili compaiono esplicitamente nella logica dell'algoritmo originale. Le considerazioni con cui sono introdotte queste variabili aggiuntive sono le seguenti: una quantita' q e' bene ricordarla se: 1.informazioni necessarie al programma sono esprimibili in modo facile ed efficiente usando q 2.e' facile ed efficiente mantenere aggiornata q Ovviamente i termini facile ed efficiente devono essere misurati secondo una qualche metrica e confrontati con il resto dell'algoritmo
15 DIFFERENZIAZIONE FORMALE Esempio banale: vogliamo una funzione che stampi il valore di con m che assume tutti i valori dei primi n interi positivi. una prima versione della funzione, di complessita' O(n 2 ), e' la seguente: void printSums(int n) { for (int m=1; m<=n; m+=1) { int sum = 0; for (int k=1; k<=m; k+=1) sum += k; printf("+/[i : i=1..%d] = %d\n", m, sum); }
16 DIFFERENZIAZIONE FORMALE Esempio banale (continua): Il problema di questa funzione e' che il calcolo della sommatoria, per ogni m, ha complessita' O(m) Una prima alternativa e' data dalla applicazione della tecnica della semplificazione algebrica al calcolo della sommatoria. Poiche' possiamo riscrivere printSums() void printSums(int n) { for (int m=1; m<=n; m+=1) { int sum = m * (m+1) / 2; printf("+/[i : i=1..%d] = %d\n", m, sum); }
17 DIFFERENZIAZIONE FORMALE Esempio banale (continua): l'algoritmo che abbiamo ottenuto e' O(n) perche' il calcolo della sommatoria e' O(1): richiede un numero costante di operazioni, una somma un prodotto e una divisione si puo' fare ancora meglio? si' se applichiamo la tecnica della differenziazione formale alla versione banale di printSums() notiamo che cioe' il risultato della sommatoria ad una iterazione e' facilmente ottenibile dal valore della sommatoria all'iterazione precedente
18 DIFFERENZIAZIONE FORMALE Esempio banale (continua): void printSums(int n) { int sum = 0; for (int m=1, m<=n, m+=1) { sum = sum + m; printf("+/[i : i=1..%d] = %d\n", m, sum); } anche questo algoritmo e' O(n) perche' il calcolo della sommatoria e' anche in questo caso O(1): richiede un numero costante di operazioni, una sola somma (risparmiamo un prodotto e una divisione per iterazione)
19 DIFFERENZIAZIONE FORMALE Esercizio: utilizzare la tecnica della differenziazione formale per migliorare la funzione evalP() per il calcolo di polinomi (vedi lezione 11), eliminando la necessita' di utilizzare la funzione pow().