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 © 2006-2009 by Claudio Salati.
Lez. 12
1
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
ss, 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
2
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
B
• A, C, B, D
A
D
C
3
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
4
TOPOLOGICAL SORT
// algoritmo base
void topSort(SET<node> s,
// in
SET<edge> after,
// in
LIST<node>& order) {
// out
order = <>;
// LISTA VUOTA
while ( nexts | ( precs |
(prec, next)after)) {
// RIMUOVO next DA s
s = s - {next};
// AGGIUNGO ORDINATAMENTE (in fondo)
// next A order
order = order + <next>;
}
if (s != ) order = UNDEFINED;
// GRAFO CON CICLI
}
1
2
3
4
5
6
5
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
6
TOPOLOGICAL SORT
•
precs | (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 nodos E numeroN
(NATURALI)
• PER MANTENERE AGGIORNATO numBefore(n)
OCCORRE E BASTA DECREMENTARE numBefore(n)
( n after(next)) QUANDO INSERISCO next
NELL'ORDINAMENTO TOTALE
7
TOPOLOGICAL SORT
// primo raffinamento
void topSort(SET<node> s,
// in
SET<edge> after,
// in
LIST<node>& order) {
// out
PAIR inDegree {
node nodo;
int
numero; };
order = <>;
// LISTA VUOTA
SET<inDegree> numBefore = {(n, 0) : n s};
for ((prec, next)after) numBefore(next) += 1;
while ( nexts | numBefore(next)==0) {
// RIMUOVO next DA s
s = s - {next};
for (n after(next)) numBefore(n) -= 1;
// AGGIUNGO next A order
order = order + <next>;
}
if (s!=) order = UNDEFINED; // GRAFO CON CICLI
}
1
2
3
4
5
6
7
8
9
10
11
12
13
8
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:
nexts | 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
nexts | numBefore(next)==0
diventa
ready !=
• l'inizializzazione di ready e' O(#s)
9
TOPOLOGICAL SORT
// raffinamento finale
void topSort(SET<node> s,
SET<edge> after,
LIST<node>& order) {
// in
// in
// out
PAIR inDegree {
node nodo;
int
numero; };
// inizializzazione
order = <>;
// LISTA VUOTA
SET<inDegree> numBefore = {(n, 0) : n s};
for ((prec, next)after) numBefore(next) += 1;
SET<node> ready = { ns | numBefore(n)==0 };
// continua alla pagina seguente
10
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 + <next>;
}
if (s != ) order = UNDEFINED;
// GRAFO CON CICLI
}
11
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
12
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.
13
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
14
dell'algoritmo
DIFFERENZIAZIONE FORMALE
Esempio banale:
m
• vogliamo una funzione che stampi il valore di
k
k 1
con m che assume tutti i valori dei primi n interi positivi.
• una prima versione della funzione, di complessita' O(n2), 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);
}
}
15
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.
m
*(m
1
)
k
possiamo riscrivere printSums()
2
k
1
m
• Poiche'
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);
}
}
16
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
m
m
1
k
1
k
1
km
k
• cioe' il risultato della sommatoria ad una iterazione e' facilmente
ottenibile dal valore della sommatoria all'iterazione precedente
17
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)
18
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().
19