Appunti di Algoritmi e Strutture Dati
Alberto Carraro
2
Contents
1 Introduzione informale agli algoritmi
1.1 I numeri di Fibonacci . . . . . . . . . . . . .
1.1.1 Algoritmo numerico . . . . . . . . . .
1.1.2 Algoritmo ricorsivo . . . . . . . . . . .
1.1.3 Algoritmo iterativo . . . . . . . . . . .
1.1.4 Algoritmo basato su potenze ricorsive
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
7
7
7
8
9
10
2 Modelli di calcolo e metodologie di analisi
2.1 Criteri di costo . . . . . . . . . . . . . . . . . .
2.2 La notazione asintotica . . . . . . . . . . . . . .
2.3 Delimitazioni inferiori e superiori . . . . . . . .
2.4 Metodi di analisi . . . . . . . . . . . . . . . . .
2.5 Analisi di algoritmi ricorsivi . . . . . . . . . . .
2.5.1 Metodo di iterazione . . . . . . . . . . .
2.5.2 Metodo di sostituzione . . . . . . . . . .
2.5.3 Il teorema fondamentale delle ricorrenze
2.5.4 Analisi dell’albero della ricorsione . . .
2.5.5 Cambiamenti di variabile . . . . . . . .
2.6 Analisi ammortizzata . . . . . . . . . . . . . . .
2.6.1 Metodo dell’aggregazione . . . . . . . .
2.6.2 Metodo degli accantonamenti . . . . . .
2.6.3 Metodo del potenziale . . . . . . . . . .
2.7 Esercizi . . . . . . . . . . . . . . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
13
13
13
14
14
14
14
15
16
18
19
19
20
20
21
22
3 Correttezza degli algoritmi
3.1 Algoritmi iterativi . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3.2 Algoritmi ricorsivi . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
29
29
29
4 Pile
4.1
4.2
4.3
e code
Pile . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Code . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Esercizi . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
31
31
31
31
5 Liste
5.1 Esercizi . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
33
33
6 Alberi
6.1 Introduzione . . . . . . . . . . . . . .
6.2 Rappresentazioni . . . . . . . . . . .
6.2.1 Rappresentazioni indicizzate .
6.2.2 Rappresentazioni collegate . .
6.3 Visite . . . . . . . . . . . . . . . . .
6.3.1 Visita in profondità . . . . .
6.3.2 Visita in ampiezza . . . . . .
6.4 Alberi binari di ricerca . . . . . . . .
6.5 Esercizi . . . . . . . . . . . . . . . .
37
37
38
38
38
38
39
39
39
40
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
3
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
6.6
Alberi AVL . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
6.6.1 Ribilanciamento tramite rotazioni . . . . . . . . . . . . . . . . . . . . . . . . . . .
6.6.2 Alberi di Fibonacci . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
7 Heap e code di priorità
7.1 Heap . . . . . . . . . .
7.1.1 Costruzione . .
7.1.2 Realizzazione .
7.2 Code di priorità . . . .
.
.
.
.
8 Algoritmi di ordinamento
8.1 Selection sort . . . . . .
8.2 Insertion sort . . . . . .
8.3 Bubble sort . . . . . . .
8.4 Heap sort . . . . . . . .
8.5 Merge sort . . . . . . . .
8.6 Quicksort . . . . . . . .
8.7 Counting sort . . . . . .
8.8 Radix sort . . . . . . . .
8.9 Limitazione inferiore per
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
42
42
43
.
.
.
.
45
45
45
47
47
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
49
49
49
50
50
51
51
52
53
53
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
55
55
56
56
56
10 Tecniche algoritmiche
10.1 Divide et impera . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
10.2 Programmazione dinamica . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
10.3 Paradigma greedy . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
59
59
59
60
11 Grafi
11.1 Definizioni preliminari . . . . . . . . . .
11.2 Rappresentazione di grafi . . . . . . . .
11.2.1 Lista di archi . . . . . . . . . . .
11.2.2 Liste di adiacenza . . . . . . . .
11.2.3 Matrice di adiacenza . . . . . . .
11.2.4 Confronto tra le rappresentazioni
11.3 Visite di grafi . . . . . . . . . . . . . . .
.
.
.
.
.
.
.
63
63
64
65
65
65
65
66
12 Minimo albero ricoprente
12.1 Teorema fondamentale . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
12.2 Algoritmo di Kruskal . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
12.3 Algoritmo di Prim . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
69
70
71
72
13 Cammini minimi con sorgente singola
13.1 Cammini minimi e rilassamento . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
13.2 Algoritmo di Dijkstra . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
13.3 Algoritmo di Bellman-Ford . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
75
76
78
80
14 Cammini minimi fra tutte le coppie
14.1 Cammini minimi e moltiplicazione di matrici . . . . . . . . . . . . . . . . . . . . . . . . .
14.2 Algoritmo di Floyd-Warshall . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
85
85
87
. . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . .
algoritmi basati sul confronto
9 Tabelle hash
9.1 Definizione di funzioni hash .
9.2 Risoluzione delle collisioni . .
9.2.1 Liste di collisione . . .
9.2.2 Indirizzamento aperto
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
4
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
15 Reti di flusso
15.1 Introduzione . . . . . . . . . . . . . . . . . .
15.2 Problema del flusso massimo . . . . . . . .
15.3 Flusso massimo - taglio minimo . . . . . . .
15.4 Algoritmo di Ford-Fulkerson . . . . . . . . .
15.4.1 Ottimizzazione di Edmond-Karp . .
15.5 Abbinamento massimo nei grafi bipartiti . .
15.6 Caso di studio: segmentazione di immagini
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
91
. 91
. 93
. 95
. 97
. 99
. 102
. 105
16 String matching
16.1 Introduzione . . . . . . . . . . . . . . . . .
16.2 Algoritmo ingenuo . . . . . . . . . . . . .
16.3 String matching con automa a stati finiti
16.3.1 Automi a stati finiti . . . . . . . .
16.3.2 L’algoritmo . . . . . . . . . . . . .
16.3.3 Costruzione dell’automa . . . . . .
16.4 Algoritmo di Knuth - Morris - Pratt . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
107
107
108
109
109
110
111
112
17 Geometria computazionale
17.1 Intersezione tra segmenti . . . . . . . . . . . . . . . .
17.1.1 Ricerca di un’intersezione tra diversi segmenti
17.2 Inviluppo convesso . . . . . . . . . . . . . . . . . . .
17.2.1 Algoritmo “brute force” . . . . . . . . . . . .
17.2.2 Algoritmo di Jarvis . . . . . . . . . . . . . . .
17.2.3 Algoritmo di Graham . . . . . . . . . . . . .
17.2.4 Algoritmo Quick Hull . . . . . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
117
118
119
121
122
122
123
124
18 Teoria della NP-completezza
18.1 Complessità di problemi decisionali . . .
18.1.1 Classi di complessità . . . . . . .
18.2 La classe NP . . . . . . . . . . . . . . .
18.2.1 Non determinismo . . . . . . . .
18.2.2 La gerarchia . . . . . . . . . . . .
18.3 Riducibilità polinomiale . . . . . . . . .
18.4 Problemi NP-completi . . . . . . . . . .
18.5 La classe coNP e la relazione tra P e NP
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
125
125
125
126
126
127
127
127
128
19 Appendice
19.1 Serie aritmetica . . . . . . . . . . .
19.2 Serie geometrica . . . . . . . . . .
19.3 Calcolo di somme per integrazione
19.4 Goniometria e trigonometria . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
129
129
129
130
130
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
5
6
Chapter 1
Introduzione informale agli algoritmi
Definizione 1.1. Un algoritmo è un insieme di istruzioni, definite passo per passo, in modo tale da poter
essere eseguite meccanicamente, e tali da produrre un determinato risultato.
1.1
I numeri di Fibonacci
Si vuole scrivere un algoritmo per il calcolo dell’n-esimo numero di Fibonacci, che può essere calcolato
utilizzando la seguente formula:
(
1
se n = 1, 2
Fn =
(1.1)
Fn−1 + Fn−2 se n ≥ 3
Saranno presentati diversi algoritmi, evidenziandone pregi e difetti.
1.1.1
Algoritmo numerico
Un primo algoritmo si basa sull’utilizzo di una funzione matematica che calcoli direttamente i numeri di
Fibonacci; proviamo a vedere se è possibile individuarne una esponenziale della forma an con a 6= 0 che
soddisfi la relazione di ricorrenza 1.1:
an = an−1 + an−2
an−2 · (a2 − a − 1) = 0
Poiché, per ipotesi, a 6= 0, cerchiamo i valori di a che soddisfano l’equazione:
a2 − a − 1 = 0
(1.2)
L’equazione 1.2 ammette due radici reali:
√
1+ 5
≈ +1.618
φ=
2√
1− 5
φ̂ =
≈ −0.618
2
Le funzioni φn e φˆn soddisfano entrambe la relazione (1.1), ma nessuna di esse calcola correttamente
i numeri di Fibonacci come vorremmo: ad esempio, φ2 6= F2 ; questo perché non sono stati considerati i
passi base della definizione ricorsiva.
Per risolvere il problema, è sufficiente osservare che una qualunque combinazione lineare di funzioni
che soddisfano la relazione di Fibonacci soddisfa anch’essa tale relazione; cerchiamo opportune costanti
c1 e c2 che diano la funzione cercata:
(
c1 · φ + c2 · φ̂ = 1
c1 · φ2 + c2 · φˆ2 = 1
Risolvendo il sistema si ottiene:
7
1
c1 = + √
5
1
c2 = − √
5
Segue che il primo algoritmo per il calcolo dei numeri di Fibonacci è il seguente:
Algoritmo Fibonacci numerico
1
2
3
int f ibonacciNumerico ( int n ) {
return √15 · (φn − φˆn );
}
Il limite di tale algoritmo è dato dal fatto che si è costretti ad operare con numeri reali, rappresentati
nei calcolatori con precisione limitata, e quindi si possono fornire risposte errate dovute ad errori di
arrotondamento.
1.1.2
Algoritmo ricorsivo
Data la natura ricorsiva della relazione di Fibonacci, si può pensare di realizzare un algoritmo ricorsivo,
come il seguente:
Algoritmo Fibonacci ricorsivo
1
2
3
4
5
6
int fi bo na cciRicorsivo ( int n ) {
if ( n <= 2)
return 1;
else
return fibonacciRicorsivo (n -1) + fibonacciRicorsivo (n -2) ;
}
Per analizzare le prestazioni di un algoritmo, si possono considerare il numero di linee di codice
eseguite (complessità temporale) e la quantità di memoria occupata (complessità spaziale): consideriamo
la prima.
In generale, ogni algoritmo ricorsivo può essere analizzato mediante una relazione di ricorrenza: il
tempo speso da una routine è pari al tempo speso all’interno della routine più quello speso dalle chiamate
ricorsive; ad esempio, la relazione per l’algoritmo sopra descritto, per n > 2, è la seguente:
T (n) = 2 + T (n − 1) + T (n − 2)
Possiamo rappresentare le chiamate ricorsive con una struttura ad albero, detta albero di ricorsione:
si usa un nodo, la radice dell’albero, per la prima chiamata e generiamo un figlio per ogni chiamata
ricorsiva.
F5 = 5
F4 = 3
F3 = 2
F3 = 2
F 2 = 1 F2 = 1 F1 = 1
F2 = 1
F1 = 1
Figure 1.1: Albero di ricorsione di fibonacciRicorsivo per il calcolo di F5
Per calcolare il numero di linee di codice eseguite da una generica chiamata fibonacciRicorsivo(n)
usiamo i seguenti lemmi.
Lemma 1.1. Sia Tn l’albero delle chiamate ricorsive della funzione fibonacciRicorsivo(n): il numero
di foglie in Tn è pari al numero di Fibonacci Fn .
Proof. Procediamo per induzione su n.
8
Caso base: è banalmente verificato; per n = 1, T1 contiene un solo nodo, e dunque una sola foglia
(F1 = 1) e, per n = 2, T2 contiene anch’esso un solo nodo, e quindi una sola foglia (F2 = 1).
Ipotesi induttiva: sia n > 2 e supponiamo che il lemma sia verificato per ogni k tale per cui 2 ≤ k ≤
n − 1.
Passo induttivo: usando l’ipotesi, dimostriamo che il lemma vale per n. L’albero della ricorsione Tn ha
come sottoalbero sinistro Tn−1 e come sottoalbero destro Tn−2 : per ipotesi essi hanno, rispettivamente, Fn−1 e Fn−2 foglie, dunque Tn ha Fn−1 + Fn−2 = Fn foglie, come si voleva dimostrare.
Lemma 1.2. Sia T un albero binario in cui ogni nodo interno ha esattamente due figli: allora il numero
di nodi interni di T è pari al numero di foglie diminuito di uno.
Proof. Procediamo per induzione su n.
Caso base: se n = 1, T ha una sola foglia e nessun nodo interno, quindi la condizione è verificata.
Ipotesi induttiva: supponiamo per ipotesi che la condizione valga per tutti gli alberi con meno di n
nodi.
Passo induttivo: proviamo che la condizione valga anche per T , ossia che i = f − 1, dove i è il numero
di nodi interni di T e f il numero di foglie di T ; sia T̂ un albero ottenuto da T rimuovendo una
qualunque coppia di foglie aventi lo stesso padre: T̂ avrà i − 1 nodi interni e f − 2 + 1 = f − 1
foglie. Per ipotesi, poiché T̂ ha meno nodi di T , vale la relazione i − 1 = (f − 1) − 1: sommando
uno ad ambo i membri, si ottiene i = f − 1, ossia l’uguaglianza che si voleva dimostrare.
Alla luce di quanto appena dimostrato, la chiamata generica fibonacciRicorsivo(n) comporta
l’esecuzione di Fn righe di codice per via delle foglie (una per ciascuna foglia) e 2 · (Fn − 1) righe
per via dei nodi interni (due per ciascuno), per un totale di 3 · Fn − 2 righe di codice, una soluzione assai
inefficiente.
1.1.3
Algoritmo iterativo
La lentezza di fibonacciRicorsivo è dovuta al fatto che continua a ricalcolare ripetutamente la soluzione
dello stesso sottoproblema (vedi, ad esempio, F3 nell’albero di ricorsione in figura 1.1); per fare di meglio,
si potrebbe risolvere il sottoproblema una volta sola, memorizzarne la soluzione ed usarla nel seguito
invece di ricalcolarla: questa è l’idea che sta alla base della tecnica chiamata programmazione dinamica.
Algoritmo Fibonacci iterativo
1
2
3
4
5
6
7
int fi bo na cciIterativo ( int n ) {
int Fib [ n ];
Fib [0] = Fib [1] = 1;
for ( int i = 2; i < n ; i ++)
Fib [ i ] = Fib [i -1] + Fib [i -2];
return Fib [ n ]
}
Per quanto riguarda l’analisi temporale, occorre operare in maniera diversa: per ogni linea di codice,
calcoliamo quante volte essa è eseguita, esaminando a quali cicli appartiene e quante volte essi sono
eseguiti. Nel nostro caso si ha:
(
T (n) =
4
se n = 1, 2
2n se n > 2
Si tratta di una soluzione decisamente più efficiente dell’algoritmo ricorsivo proposto.
9
Un piccolo miglioramento
L’algoritmo fibonacciIterativo richiede una quantità di spazio di memoria linearmente proporzionale
alla dimensione dell’input n, anche se ogni iterazione utilizza solo i due valori precedenti a Fn , Fn−1
e Fn−2 ; rimpiazzando l’array con due variabili, come proposto nel seguente algoritmo, otteniamo un
notevole risparmio di memoria.
Algoritmo Fibonacci iterativo modificato
1
2
3
4
5
6
7
8
9
int f i b o n a c c i I t e r a t i v o M o d i f i c a t o ( int n ) {
int a = 1 , b = 1;
for ( int i =2; i < n ; i ++) {
c = a + b;
a = b;
b = c;
}
return b ;
}
1.1.4
Algoritmo basato su potenze ricorsive
L’algoritmo che sarà presentato in questa sezione si basa sul seguente lemma.
n−1 1 1
1 1
Fn
Fn−1
Lemma 1.3. Sia A =
. Allora An−1 =
=
.
1 0
1 0
Fn−1 Fn−2
Proof. Procediamo per induzione su n.
Caso base: sia F0 = 0 fissato per convenzione. Per n = 2 il caso base è banalmente verificato:
1 1 1
F2 F1
=
1 0
F1 F0
Ipotesi induttiva: supponiamo valido il lemma per n − 1, ossia:
n−2 1 1
Fn−1 Fn−2
=
1 0
Fn−2 Fn−3
Passo induttivo: dimostriamo la validità del lemma per n:
n−1 1 1
Fn−1 Fn−2
1 1
Fn−1 + Fn−2
An−1 =
=
·
=
1 0
Fn−2 Fn−3
1 0
Fn−2 + Fn−3
Fn−1
Fn−2
=
Fn
Fn−1
Fn−1
Fn−2
Usando il lemma appena dimostrato ed utilizzando il metodo dei quadrati ripetuti per il calcolo della
potenza n-esima della matrice, si ottiene il seguente algoritmo:
Algoritmo Fibonacci con matrici
1
2
3
4
5
6
7
8
9
10
11
12
13
int fibonacciMatrice
( int n ) {
1 1
M =
1 0
potenzaDiMatrice (M , n - 1)
return M [0][0]
}
void potenzaDiMatrice ( int [][] M , int n )
if ( n > 1) {
potenzaDiMatrice (M , n / 2)
M = M * M
}
if ( n dispari
) {
1 1
M = M *
1 0
}
10
L’algoritmo presenta la seguente relazione di ricorrenza:
(
O(1)
se n ≤ 1
T (n) =
T (n/2) + O(1) se n > 1
la cui soluzione è:
T (n) = O(log n)
11
12
Chapter 2
Modelli di calcolo e metodologie di
analisi
2.1
Criteri di costo
Il criterio di misurazione del tempo di esecuzione di un algoritmo che si basa sull’assunzione che le diverse
operazione richiedano tutte lo stesso tempo, indipendentemente dalla dimensione degli operandi coinvolti,
è noto come misura di costo uniforme; si tratta di un criterio utile in prima approssimazione, ma si tratta
di un modello troppo idealizzato.
Per ovviare a questo problema, è stato proposto un criterio, noto come misura di costo logaritmico,
che assume che il costo di esecuzione delle operazioni dipenda dalla dimensione degli operandi coinvolti;
nonostante fornisca una buona approssimazione, in svariati casi può generare una complessità eccessiva
e non necessaria.
2.2
La notazione asintotica
Definizione 2.1. Data una funzione f (n), definiamo:
• O(f (n)) = {g(n) : ∃c > 0 ∧ n0 ≥ 0 : g(n) ≤ cf (n), ∀n ≥ n0 } (g(n) cresce al più come f (n));
• Ω(f (n)) = {g(n) : ∃c > 0 ∧ n0 ≥ 0 : g(n) ≥ cf (n), ∀n ≥ n0 } (g(n) cresce almeno come f (n));
• Θ(f (n)) = {g(n) : ∃c1 , c2 > 0 ∧ n0 ≥ 0 : c1 g(n) ≤ f (n) ≤ c2 g(n), ∀n ≥ n0 } (g(n) cresce esattamente
come f (n)).
Proprietà 2.1. Date due funzioni f (n) e g(n), risulta g(n) = Θ(f (n)) se e solo se g(n) = O(f (n)) e
g(n) = Ω(f (n)).
Proprietà 2.2. Θ gode della proprietà simmetrica: g(n) = Θ(f (n)) se e solo se f (n) = Θ(g(n)).
Proprietà 2.3. O, Ω sono simmetriche trasposte: f (n) = Ω(g(n)) se e solo se g(n) = O(f (n)).
Proprietà 2.4. Per tutte e tre le notazioni vale la proprietà transitiva: per la notazione O, ad esempio,
f (n) = O(g(n)) ∧ g(n) = O(h(n)) ⇒ f (n) = O(h(n)).
Teorema 2.1. Siano f (n) e g(n) funzioni positive:
• limn→+∞
f (n)
g(n)
= +∞
(n)
→ ∃M > 0 ∧ n0 > 0 : fg(n)
≥ M, ∀n ≥ n0
→ f (n) ≥ M · g(n)
→ f (n) = Ω(g(n)) ∧ g(n) = O(f (n))
• limn→+∞
f (n)
g(n)
=0
(n)
→ preso > 0, ∃n0 > 0 : fg(n)
≤ , ∀n ≥ n0
→ f (n) ≤ · g(n)
→ f (n) = O(g(n)) ∧ g(n) = Ω(f (n))
13
• limn→+∞
f (n)
g(n)
= l 6= 0
(n)
− l ≤ , ∀n ≥ n0
→ preso > 0, ∃n0 > 0 : fg(n)
f (n)
g(n) − l ≤ (n)
≤ fg(n)
≤l+
→ − ≤
→l−
→ (l − ) · g(n) ≤ f (n) ≤ (l + ) · g(n)
→ f (n) = Θ(g(n))
2.3
Delimitazioni inferiori e superiori
Definizione 2.2. Un algoritmo A ha costo di esecuzione O(f (n)) su istanze di ingresso di dimensione
n e rispetto ad una certa risorsa di calcolo, se la quantità r di risorsa sufficiente per eseguire A su una
qualunque istanza di dimensione n verifica la relazione r(n) = O(f (n)).
Definizione 2.3. Un problema P ha complessità O(f (n)) rispetto ad una data risorsa di calcolo se esiste
un algoritmo che risolve P il cui costo di esecuzione rispetto a quella risorsa è O(f (n)).
Definizione 2.4. Un algoritmo A ha costo di esecuzione Ω(f (n)) su istanze di dimensione n e rispetto
ad una certa risorsa di calcolo, se la massima quantità r di risorsa necessaria per eseguire A su istanze
di dimensione n verifica la relazione r(n) = Ω(f (n)).
Definizione 2.5. Un problema P ha complessità Ω(f (n)) rispetto ad una data risorsa di calcolo se ogni
algoritmo che risolve P ha costo di esecuzione Ω(f (n)) rispetto a quella risorsa.
Definizione 2.6. Dato un problema P con complessità Ω(f (n)) rispetto ad una data risorsa di calcolo,
un algoritmo che risolve P è ottimo se ha costo di esecuzione O(f (n)) rispetto a quella risorsa.
2.4
Metodi di analisi
Per analizzare il tempo di esecuzione di un algoritmo, solitamente si usa distinguere fra tre diverse
categorie di istanze, a parità di dimensione; sia T (I) il tempo di esecuzione dell’algoritmo sull’istanza I:
Caso peggiore:
Tworst (n) =
max
istanze di
dimensione n
T (I)
Caso migliore:
Tbest (n) =
min
istanze di
dimensione n
T (I)
Caso medio: sia P (I) la probabilità di occorrere dell’istanza I:
X
Tavg (n) =
(T (I) · P (I))
istanze di
dimensione n
2.5
2.5.1
Analisi di algoritmi ricorsivi
Metodo di iterazione
L’idea è quella di ridurre la ricorsione ad una sommatoria dipendente solo dalla dimensione del problema
iniziale.
Esempio 2.1. Sia data la seguente relazione di ricorrenza e assumiamo, per semplicità di analisi, che n
sia una potenza di 3:
(
1
se n = 1
T (n) =
9 · T (n/3) + n se n > 1
Srotolando la ricorsione otteniamo:
14
T (n) = 9 · T (n/3) + n
= 9 · (9 · T (n/9) + n/3) + n
= 92 · T (n/32 ) + 9 · n/3 + n
= ...
= 9i · T (n/3i ) +
i−1
X
(9/3)j n
j=0
Dal momento che n/3i = 1 quando i = log3 n, risulta:
log3 n−1
T (n) = 9log3 n + n ·
X
3j
j=0
Poiché log3 n = log9 n · log3 9, risulta 9log3 n = 9log9 n·log3 9 = n2 ; usando la serie geometrica, si ottiene:
T (n) = n2 +
2.5.2
n−1
3log3 n − 1
= n2 +
= Θ(n2 )
3−1
2
Metodo di sostituzione
L’idea è di intuire la soluzione della relazione di ricorrenza ed utilizzare il principio di induzione per
dimostrare che l’intuizione è corretta.
Esempio 2.2. Sia data la seguente relazione di ricorrenza:
(
1
T (n) =
T (bn/2c) + n
se n = 1
se n > 1
Intuizione: T (n) = O(n), ossia dimostrare che esistono c > 0 e n0 > 0 tali che T (n) ≤ c · n, ∀n ≥ n0
Passo base: provo n = 1
T (1) = 1 ≤ c · 1 → c ≥ 1
Ipotesi induttiva: supponiamo di avere c > 0 tale per cui T (bn/2c) ≤ c · bn/2c.
Passo induttivo: sia n > 1
T (n) = T (bn/2c) + n
≤ c · bn/2c + n
n
≤c· +n
2
Rimane da provare che c ·
n
2
+ n ≤ c · n; ciò risulta essere vero per c ≥ 2.
Affinché siano verificati sia il caso base che il passo induttivo, occorre prendere una qualsiasi c ≥ 2.
Esempio 2.3. Per illustrare altre sottigliezze relative all’uso del metodo di sostituzione, consideriamo la
seguente relazione di ricorrenza:
(
1
T (n) =
9 · T (bn/3c) + n
se n = 1, 2
se n > 2
Intuizione: T (n) = O(n2 ), ossia dimostrare che esistono c > 0 e n0 > 0 tali che T (n) ≤ c·n2 , ∀n ≥ n0
Passo base: T (1) = T (2) = 1 ≤ c · 12 ≤ c · 22 → c ≥ 1
Ipotesi induttiva: assumiamo che la disuguaglianza sia soddisfatta per ogni k < n
15
Passo induttivo: sia n > 2
T (n) = 9 · T (bn/3c) + n
≤ 9 · c · bn/3c2 + n
≤ 9 · c · (n/3)2 + n
= c · n2 + n
Usando questa soluzione, non riusciamo a dimostrare la nostra affermazione poiché c · n2 + n > c · n2 ;
possiamo risolvere facilmente il problema usando un’ipotesi induttiva più forte, in modo da far comparire
un addendo negativo negativo che, sommato ad n, faccia tornare i conti; provando con l’ipotesi T (n) ≤
c · (n2 − n) otteniamo:
T (n) = 9 · T (bn/3c) + n
≤ 9 · c · ((n/3)2 − n/3) + n
= c · n2 − 3 · c · n + n
Rimane da provare che c · n2 − 3 · c · n + n ≤ c · (n2 − n); ciò risulta essere vero per c ≥ 1/2. Abbiamo,
però, un altro problema: il passo base non è più verificato; infatti T (1) = 1 > c · (12 − 1) = 0; ciò può
essere risolto cambiando n0 , che prima avevamo scelto pari a 1.
Poiché T (2) = 1 ≤ c · (4 − 2) per c ≥ 1/2, vediamo se possiamo scegliere n0 = 2: per poterlo fare,
dobbiamo definire tutti i casi che si riconducono a T (1) come casi base e verificare che, per tutti questi,
esista c che verifichi la nostra condizione; in particolare, in questo esempio, dobbiamo definire come passi
base T (3), T (4), T (5), oltre a T (2) (per n ≥ 6 ci si riconduce a un caso per cui la condizione valga):
T (3) = 9 · T (1) + 3 = 12 ≤ c · (32 − 3) per c ≥ 2
T (4) = 9 · T (1) + 4 = 13 ≤ c · (42 − 4) per c ≥ 13/12
T (5) = 9 · T (1) + 5 = 14 ≤ c · (52 − 5) per c ≥ 7/10
Affinché siano verificati tutti i casi base e il passo induttivo, dobbiamo avere n0 = 2 e c ≥ 2.
2.5.3
Il teorema fondamentale delle ricorrenze
Si tratta di un metodo per analizzare algoritmi basati sulla tecnica del dividi et impera, in cui:
• un problema di dimensione n viene diviso in a sottoproblemi di dimensione n/b;
• dividere in sottoproblemi e combinare le soluzioni richiede tempo f (n).
La relazione di ricorrenza corrispondente a questo scenario è la seguente:
n
+ f (n)
(2.1)
T (n) = a · T
b
Per analizzare la relazione di ricorrenza, consideriamo l’albero della ricorsione ed assumiamo che n
sia una potenza esatta di b e che la ricorsione si fermi quando n = 1.
Proprietà 2.5. I sottoproblemi al livello i dell’albero della ricorsione hanno dimensione n/bi .
Proprietà 2.6. Il contributo di un nodo di livello i al tempo di esecuzione (escluso il tempo speso nelle
chiamate ricorsive) è f (n/bi ).
Proprietà 2.7. Il numero di livelli nell’albero della ricorsione è logb n.
Proprietà 2.8. Il numero di nodi al livello i dell’albero della ricorsione è ai .
Usando le proprietà appena elencate, si può riscrivere la relazione di ricorrenza nella seguente forma:
logb n
T (n) =
X
i=0
ai · f
n
bi
(2.2)
La soluzione della (2.2) è data dal seguente teorema, noto come teorema fondamentale delle ricorrenze.
16
Teorema 2.2. La relazione di ricorrenza
(
1
T (n) =
a · T (n/b) + f (n)
se n = 1
se n > 1
ha soluzione:
1. T (n) = Θ(nlogb a ), se f (n) = O(nlogb a− ) per > 0;
2. T (n) = Θ(nlogb a · log n), se f (n) = Θ(nlogb a );
3. T (n) = Θ(f (n)), se f (n) = Ω(nlogb a+ ) per > 0 e a·f (n/b) ≤ c·f (n) per c < 1 ed n sufficientemente grande.
Proof. Assumiamo per semplicità che n sia una potenza esatta di b.
Caso 1: riscriviamo il termine generico della sommatoria 2.2:
n
n logb a− a · b i ai · f i = O ai · i
= O nlogb a− · log a
= O(nlogb a− · (b )i )
b
b
b b
Per limitare superiormente T (n) si può scrivere:
T (n) =
logb n
logb n
X
X
O(nlogb a− · (b )i ) = O nlogb a− ·
i=0
logb a−
O n
!
(b )i
= O nlogb a− ·
i=0
·
b · n − 1
b − 1
b·(logb n+1) − 1
b − 1
!!
=
!!
= O(nlogb a− · n ) = O(nlogb a )
Analizzando l’equazione 2.2 e considerando solo i tempi di esecuzione relativi ai nodi sull’ultimo
livello dell’albero di ricorsione, otteniamo:
T (n) ≥ alogb n = nlogb a → T (n) = Ω(nlogb a )
Dalle due limitazioni segue che T (n) = Θ(nlogb a ).
Caso 2: anche in questo caso, riscriviamo il termine generico della sommatoria:
n
n logb a a i = Θ(nlogb a )
ai · f i = Θ ai · i
= Θ nlogb a · log a
b
b
b b
Da cui segue:
logb n
T (n) =
X
Θ(nlogb a ) = Θ(nlogb a · logb n).
i=0
Caso 3: sotto l’assunzione a · f (n/b) ≤ c · f (n), risulta facile dimostrare che ai · f (n/bi ) ≤ ci · f (n);
infatti:
n
n/bi−1 n ai · f i = ai−1 · a · f
≤ ai−1 · c · f i−1
b
b
b
Iterando il ragionamento si ottiene la disuguaglianza desiderata. Usando l’equazione 2.2, la serie
geometrica con base c < 1 e la disuguaglianza appena dimostrata, si può scrivere:
logb n
T (n) =
X
i=0
ai · f
n
bi
≤ f (n) ·
∞
X
ci = f (n) ·
i=0
1
= O(f (n))
1−c
Dalla relazione 2.2, si ricava immediatamente che T (n) = Ω(f (n)), da cui T (n) = Θ(f (n)).
Esempio 2.4. Consideriamo la seguente relazione di ricorrenza:
(
O(1)
se n = 1
T (n) =
T (n/2) + O(1) altrimenti
Si ha f (n) = O(1) = Θ(nlog2 1 ); ci troviamo, dunque, nel caso 2 del teorema, e quindi risulta T (n) =
Θ(log n).
17
Esempio 2.5. Consideriamo la seguente relazione di ricorrenza:
(
O(1)
se n = 1, 2
T (n) =
9 · T (n/3) + n altrimenti
Si ha f (n) = n = O(nlog3 9− ); dal caso 1 del teorema fondamentale, risulta che T (n) = Θ(n2 ).
Esempio 2.6. Consideriamo la seguente relazione di ricorrenza:
(
O(1)
se n = 1
T (n) =
T (n/2) + n + logn +1
Si ha f (n) = n+log n+1 = Θ(n) = Ω(nlog2 1− ), che può ricadere nel caso 3 del teorema fondamentale;
occorre infatti trovare c < 1 tale che, per n sufficientemente grande, valga:
a · f (n/b) ≤ c · f (n)
n
n
→1·
+ log + 1 ≤ c · (n + log n + 1)
2
2
n
n
1
+
log
2 +1
= < 1 per n → +∞
→c≥ 2
n + log n + 1
2
Esisterà n0 tale che, per n ≥ n0 , si ha
+ log n2 + 1
3
≤
n + log n + 1
4
n
2
È sufficiente prendere c = 3/4 affinché la disuguaglianza del caso 3 del teorema fondamentale sia
verificata per n sufficientemente grande; segue che T (n) = Θ(n).
Esempio 2.7. Consideriamo la seguente relazione di ricorrenza:
(
O(1)
se n = 1, 2
T (n) =
3 · T (n/3) + n · log n altrimenti
Nessuno dei casi del teorema principale può essere applicato; si potrebbe pensare di utilizzare il terzo
caso, ma questo non è possibile poiché n · log n è solo logaritmicamente, e non polinomialmente, più
grande di nlog3 3 = n.
2.5.4
Analisi dell’albero della ricorsione
La tecnica consiste nell’analizzare l’albero delle chiamate ricorsive, indicando le dimensioni dei problemi
di ogni chiamata ricorsiva, ed analizzando la dimensione totale dei problemi ad ogni livello dell’albero.
Esempio 2.8. Consideriamo la seguente relazione di ricorrenza:
(
1
se n = 1, 2
T (n) =
2
9 · T (n/3) + n · log n altrimenti
n
9
n/3
n/3
....
n/3
....
n/3
....
n/3
....
n/3
....
n/3
....
n/3
....
n/3
....
....
Figure 2.1: Parte iniziale dell’albero di ricorsione
Calcoliamo il costo di ciascun livello dell’albero di ricorsione:
18
Liv. 0: problema di dimensione n e costo n2 · log n → totale = n2 · log n;
Liv. 1: problema di dimensione
n
3
Liv. 2: problema di dimensione
n
32
Liv. i: problema di dimensione
n
3i
e costo
2
n
3
e costo
2
n
3i
e costo
Foglie: problemi di dimensione 1 →
n
3k
n
32
· log
2
n
3
· log
·log
n
3i
→ totale = 9 ·
n
32
2
n
3
→ totale = 92 ·
→ totale = 9i ·
2
n
3i
· log n3 ;
n
32
2
·log
n
3i
· log
n
32 ;
= n2 ·log n−i·n2 ·log 3);
= 1 → k = log3 n.
log3 n−1
X
T (n) =
(n2 · log n − n2 · i · log 3) +
i=0
|
3n
9| log
{z }
contributo foglie
{z
contributo nodi interni
}
log3 n−1
= n2 · log n ·
X
log3 n−1
1 − n2 · log 3 ·
i=0
X
i + n2
i=0
(log3 n − 1) · (log3 n)
= n2 · log n · log3 n − n2 · log 3 ·
+ n2
2
log3 n
log23 n
+ n2 · log 3 ·
+ n2
= n2 · log n · log3 n − n2 · log 3 ·
2
2
Risulta quindi T (n) = Θ(n2 · log2 n).
2.5.5
Cambiamenti di variabile
Si tratta di una tecnica che viene utilizzata quando la relazione di ricorrenza presenta delle radici; per
maggiori informazioni, vedere l’esempio.
Esempio 2.9. Consideriamo la seguente relazione di ricorrenza:
(
T (n) =
O(1)
√
T ( n) + O(1)
se n = 1
altrimenti
√
Eseguiamo la sostituzione n = 2x (ossia x = log n), da cui n = 2x/2 e T (2x ) = T (2x/2 ) + O(1);
poniamo inoltre T (2x ) = R(x), da cui otteniamo la relazione di ricorrenza R(x) = R(x/2) + O(1),
risolvibile con il teorema principale (R(x) = O(log x)). Combinando le uguaglianze T (2x ) = O(log x) e
n = 2x , si ha T (n) = O(log log n).
2.6
Analisi ammortizzata
una tecnica usata nel campo dell’analisi delle complessità per calcolare il costo medio di una singola
operazione, all’interno di una sequenza che può contenere operazioni molto costose, in maniera più precisa,
invece di sovrastimare tutte le operazioni attribuendo loro costo massimo.
Attenzione: l’analisi del caso medio considera la tipologia di input “medio”, ovvero quello più
probabile; l’analisi ammortizzata non usa la teoria della probabilità, ma considera una serie di operazioni
e valuta le prestazioni medie di ciascuna operazione nel caso pessimo.
Nell’analisi ammortizzata, si fa sostanzialmente uso di tre metodi:
• metodo dell’aggregazione;
• metodo degli accantonamenti ;
• metodo del potenziale.
19
In seguito, considereremo l’esempio di un contatore binario: dato un array A[k − 1...0] di {0, 1}, che
rappresenta il numero
k−1
X
x=
(A[i] · 2i )
i=0
si vuole calcolare il costo di n incrementi, con x inizialmente a zero; l’algoritmo di incremento è il seguente:
1
2
3
4
5
6
7
8
increment ( array A → void )
i = 0
while ( i < length ( A ) && A [ i ] == 1) {
A[i] = 0
i = i + 1
}
if ( i < length ( A ) )
A[i] = 1
Seguendo la tecnica di analisi tradizionale, poiché nel caso peggiore la complessità dell’incremento è
O(k), concludiamo che il costo di n incrementi è O(n · k). Proviamo, invece, ad usare ciascuna delle tre
tecniche dell’analisi ammortizzata e vediamo che risultati otteniamo.
2.6.1
Metodo dell’aggregazione
Idea: se una sequenza di n operazioni impiega, nel caso peggiore, un tempo T (n), allora il costo ammortizzato di ogni operazione è T (n)/n.
Esempio 2.10. Facciamo n operazioni di increment: chiaramente non tutti i bit cambiano ad ogni
chiamata. In particolare:
• A[0] cambia ad ogni chiamata, per un totale di n volte;
• A[1] cambia ogni due chiamate, per un totale di bn/2c volte;
• A[2] cambia ogni quattro chiamate, per un totale di bn/4c volte;
• A[i] cambia la metà delle volte di A[i − 1], ossia bn/2i c volte.
Il numero totale di cambi in n operazioni è:
T (n) =
k j
X
nk
i=0
2i
≤
k
k i
∞ i
X
X
X
n
1
1
=
n
·
≤
n
·
=2·n
i
2
2
2
i=0
i=0
i=0
Quindi, il costo medio delle operazioni nel caso pessimo è 2 · n/n = 2 = O(1).
2.6.2
Metodo degli accantonamenti
Idea: vengono assegnati costi diversi a operazioni differenti; qualche operazione potrebbe essere associata a un costo minore o maggiore di quello effettivo, detto costo ammortizzato. Quando il costo
ammortizzato supera quello effettivo, la differenza viene trattenuta come credito: questo può essere usato
successivamente per pagare operazioni il cui costo ammortizzato è minore di quello effettivo.
Siano:
• Ci : costo effettivo dell’i-esima operazione;
• Ĉi : costo ammortizzato dell’i-esima operazione;
dopo n operazioni, dev’essere valida la relazione:
n
X
Ĉi ≥
i=1
n
X
Ci
i=1
Il credito totale è dato dalla seguente formula:
n
X
Ĉi −
i=1
n
X
i=1
20
Ci
Esempio 2.11. Definiamo i costi nel seguente modo:
• impostare un bit a 1 costa 2 unità;
• impostare un bit a 0 costa 0 unità.
Il costo ammortizzato di ogni operazione di increment è 2 unità, in quanto viene impostato un solo bit a
1, mentre un numero variabile di bit a 0. Ora dobbiamo provare che il credito totale è sempre maggiore
o uguale a zero; definiamo innanzitutto bi come il numero di bit a 1 dopo l’i-esima operazione.
Proprietà 2.9. Se ogni operazione di increment viene pagata con costo ammortizzato 2 e il contatore
è inizialmente nullo, allora dopo i operazioni il credito è maggiore o uguale a bi .
Proof. Procediamo per induzione su i.
Caso base: si ha i = 0; il credito è 0 e bi = 0, in quanto il contatore è inizialmente nullo.
Ipotesi induttiva: per k < i, il credito dopo la k-esima operazione è maggiore o uguale a bk .
Passo induttivo: sia i > 0; per ipotesi si ha che il credito, dopo i − 1 operazioni, è maggiore o uguale
a bi−1 . L’operazione i-esima:
• deposita due unità, da cui il credito iniziale è maggiore o uguale a bi−1 + 2;
• imposta a zero una serie si di bit: si ha si ≤ bi ;
• se il contatore non è stato azzerato, imposta un bit a 1.
Il costo effettivo della chiamata è quindi minore o uguale a si + 1. All’uscita:
• bi ≤ bi−1 − si + 1;
• il credito è dato dalla differenza tra credito iniziale e costo effettivo:
credito ≥ bi−1 + 2 − (si + 1)
= bi−1 − si + 1
≥ bi
Dunque il credito residuo non è mai negativo.
2.6.3
Metodo del potenziale
Idea: rappresentiamo il credito prepagato come una sorta di “energia potenziale” che può essere liberata
per pagare le operazioni future; il potenziale è associato alla struttura e dipende dalle operazioni fatte.
Fissiamo una struttura ed eseguiamo n operazioni:
φ : {0, 1, ..., n} → R
dove:
• φ(0) è il potenziale iniziale;
• φ(i) è il potenziale dopo l’i-esima operazione;
• Ci è il costo dell’i-esima operazione;
• Ĉi è il costo ammortizzato dell’i-esima operazione.
I fondi effettivi devono essere pari alle spese sostenute, ossia:
Ĉi + φ(i − 1) = Ci + φ(i)
da cui:
Ĉi = Ci + φ(i) − φ(i − 1)
21
Si ha:
n
X
Ĉi =
i=1
=
n
X
i=1
n
X
i=1
=
n
X
(Ci + φ(i) − φ(i − 1))
Ci +
n
X
φ(i) −
i=1
=
n
X
i=1
n
X
φ(i − 1)
i=1
Ci + φ(n) +
i=1
=
n
X
n−1
X
φ(i) −
i=1
Ci + φ(n) +
n
X
φ(i − 1) − φ(0)
i=2
n−1
X
φ(i) −
i=1
n−1
X
φ(j) − φ(0)
j=1
Ci + φ(n) − φ(0)
i=1
Vogliamo che valga:
n
X
Ĉi ≥
n
X
i=1
Ci
i=1
ossia:
φ(n) − φ(0) ≥ 0
(2.3)
Se non sappiamo quante operazioni saranno fatte, per garantire la relazione 2.3, si impone φ(i) ≥ φ(0);
di solito è comodo porre φ(0) = 0 e provare φ(i) ≥ 0.
Esempio 2.12. Scegliamo φ(i) come il numero di bit a 1 dopo la i-esima operazione: si ha φ(0) = 0 e
φ(i) ≥ 0 per ogni i > 0; abbiamo un buon potenziale, in quanto soddisfa φ(i) ≥ φ(0). Consideriamo la
i-esima operazione e sia si il numero di bit impostati a zero nel corso di questa operazione; si ha:
(
bi−1 − si
se bi−1 = k
bi =
bi−1 − si + 1 se bi−1 < k
(
si
se bi−1 = k
Ci =
si + 1 se bi−1 < k
Da cui:
(
si + bi − bi−1
se bi−1 = k
Ĉi =
si + 1 + bi − bi−1 se bi−1 < k
(
si + bi−1 − si − bi−1
se bi−1 = k
=
si + 1 + bi−1 − si + 1 − bi−1 se bi−1 < k
(
0 se bi−1 = k
=
2 se bi−1 < k
Scegliendo 2 come costo ammortizzato siamo a posto.
2.7
Esercizi
Esercizio 2.1. Dimostrare che f (n) ∈ Θ(g(n)) sse f (n) ∈ O(g(n)) ∩ Ω(g(n)).
Svolgimento: (⇒) Supponiamo che f (n) ∈ Θ(g(n)). Allora esistono due costanti c0 , c1 ∈ R+ ed
un numero naturale n0 ∈ N tali che per ogni n ≥ n0 abbiamo c0 g(n) ≤ f (n) ≤ c1 g(n). Il fatto che
∀n ≥ n0 . c0 g(n) ≤ f (n) ci dice che f (n) ∈ Ω(g(n)), mentre (∀n ≥ n0 ).f (n) ≤ c1 g(n) implica che
f (n) ∈ O(g(n)).
(⇐) Siccome f (n) ∈ Ω(g(n)), esistono due costanti c0 ∈ R+ e n0 ∈ N tali che f (n) ≤ c0 g(n), per ogni
n ≥ n0 . Siccome f (n) ∈ O(g(n)), esistono due costanti c00 ∈ R+ e n00 ∈ N tali che c00 g(n) ≤ f (n),
per ogni n ≥ n00 . Sia m0 = max(n0 , n00 ). Allora c00 g(n) ≤ f (n) ≤ c0 g(n), per ogni n ≥ m0 . Dunque
f (n) ∈ Θ(g(n)).
22
n
Esercizio 2.2. Dimostrare che per n > 6 si ha Fn > 2 2 .
Svolgimento: Ricordiamo la definizione ricorsiva della serie di Fibonacci:
(
1
se n = 1 oppure n = 2
Fn =
Fn−1 + Fn−2 se n ≥ 3
Prodediamo
dunque con una
F7 = 13 >
√ prova per induzione completa. Il caso base è n = 7. Abbiamo
√
n−1
n−2
7
2
2
2 = 128 perché 13 = 169. Per ipotesi induttiva ora sappiamo che Fn−1 > 2
e Fn−2 > 2 2 ,
n
n
√
√
n−1
n−2
n
dunque Fn = Fn−1 + Fn−2 > 2 2 + 2 2 = 2√22 + 222 > 2 2 poiché 2 + 2 > 2 2.
Esercizio 2.3. Dire perché il tempo di calcolo dell’algoritmo Fibonacci2 (pag. 6 [1]) cresce cosı̀ rapidamente al crescere di n (argomentando usando l’albero di ricorsione).
Svolgimento: Come dice anche il libro stesso, la formula di ricorrenza che calcola la complessità
n
dell’algoritmo definisce la funzione di Fibonacci stessa. L’esercizio 2.2 ci dice che Fn ∈ Ω(2 2 ), e pertanto
l’algoritmo ha complessità almeno esponenziale.
Esercizio 2.4. Spiegare come l’algoritmo Fibonacci3 (pag. 9 [1]) è migliore di Fibonacci2.
Svolgimento: La complessità di Fibonacci3 è lineare nella dimensione dell’input mentre gli esercizi 2.2,2.3 ci dicono che la complessità di Fibonacci2 è esponenziale nella dimensione dell’input.
√
√
Esercizio 2.5. Dimostrare che n + 10 ∈ Θ( n).
Svolgimento: Dobbiamo
trovare
due costanti
c0 , c1 ∈ R+ ed un numero naturale n0 ∈ N tali che per
√
√
√
ogni n ≥ n0 abbiamo c0 n ≤ n + 10 ≤ c1 n.
√
√
−10
Le soluzioni (nell’incognita n) della disequazione c0 n ≤ n + 10 sono n ≥ (1−c
2 ) . Scegliendo c0 = 2
0
abbiamo n ≥ 10
3 .
√
√
. Scegliendo c1 = 2
Le soluzioni (nell’incognita n) della disequazione n + 10 ≤ c1 n sono n ≥ (c210
1 −1)
abbiamo n ≥ 10.
L’intersezione dei due intervalli di soluzioni {n : n ≥ 10} e {n : n ≥ 10
3 } è uguale a [10, +∞). Quindi
se scegliamo n0 = 10, c0 = 2 e c1 = 2 abbiamo che il sistema
( √
√
c0 n ≤ n + 10
√
√
n + 10 ≤ c1 n
è soddisfatto per ogni n ≥ n0 .
Esercizio 2.6. Dimostrare che 12 n2 − 3n ∈ Θ(n2 ).
Svolgimento: Dobbiamo trovare due costanti c0 , c1 ∈ R+ ed un numero naturale n0 ∈ N tali che per
ogni n ≥ n0 abbiamo c0 n2 ≤ 12 n2 − 3n ≤ c1 n2 .
6
Le soluzioni (nell’incognita n) della disequazione c0 n2 ≤ 12 n2 − 3n sono n ≥ 1−c
. Scegliendo c0 = 12
0
abbiamo n ≥ 12.
Le soluzioni (nell’incognita n) della disequazione 12 n2 − 3n ≤ c1 n2 sono n ≥ 2c−6
. Scegliendo c1 = 1
1 −1
abbiamo n ≥ −6.
L’intersezione dei due intervalli di soluzioni {n : n ≥ 12} e {n : n ≥ −6} è uguale a [12, +∞). Quindi
se scegliamo n0 = 12, c0 = 12 e c1 = 1 abbiamo che il sistema
(
c0 n2 ≤ 12 n2 − 3n
1 2
2
2 n − 3n ≤ c1 n
è soddisfatto per ogni n ≥ n0 .
Esercizio 2.7. Dimostriamo che:
(1) f (n) ∈ O(g(n)) sse g(n) ∈ Ω(f (n))
(2) f (n) ∈ Θ(g(n)) sse g(n) ∈ Θ(f (n))
Svolgimento:
23
(1) Supponiamo f (n) ∈ O(g(n)). Allora esistono c ∈ R+ e n0 ∈ N tali che ∀n ≥ n0 . f (n) ≥ cg(n).
Dunque, ponendo c0 = 1c si ha ∀n ≥ n0 . c0 f (n) ≥ g(n) e siccome c0 ∈ R+ , abbiamo mostrato che
g(n) ∈ Ω(f (n)). Il viceversa si fa in maniera analoga.
(2) Basta usare il punto (1).
Esercizio 2.8. Sia f una funzione per cui esiste un numero naturale n0 tale che f (n) > 0, per ogni
n ≥ n0 . Dimostriamo che per ogni a, b ∈ R+ , af (n) + b ∈ Θ(f (n)).
Svolgimento: Dobbiamo trovare c1 , c2 , n0 tali che ∀n ≥ n0 . c1 f (n) ≤ af (n) + b ≤ c2 f (n). Scegliamo
c1 = a e c2 = a + b. Allora af (n) ≤ af (n) + b ≤ af (n) + bf (n), per ogni n ≥ n0 .
Esercizio 2.9. Dimostriamo che loga n è in Θ(logb n).
Svolgimento: Poiché logb n = (logb a)(loga n), ponendo c = logb a abbiamo che c(loga n) ≤ logb n ≤
c(loga n) per ogni n ≥ 0.
Esercizio 2.10. Verificare che n(2 + sin n) = Θ(n).
Svolgimento: Consideriamo le due disequazioni c1 n ≤ n(2 + sin n) ≤ c2 n. Abbiamo che n ≤
n(2 + sin n) e quindi basta scegliere c1 = 1. Similmente n(2 + sin n) ≤ c2 n sse 2 + sin n ≤ c2 . Scegliendo
c2 = 4 ciò è sempre vero.
√
Esercizio 2.11. Ordinare le seguenti classi di complessità: n2 , n log n, n3 + log n, n, n2 + 2n log n,
3
log(log n), 17 log n, 10n 2 , n5 − n4 + 2n, 5n2 log(log n), 3n2 + n3 log n, n + 6 log n.
Svolgimento: Iniziamo confrontando n5 −n4 +2n e 3n2 +n3 log n. Vogliamo dire che 3n2 +n3 log n ∈
n5 −n4 +2n
n2 −n
o(n5 − n4 + 2n). Calcoliamo limx→∞ 3n
= +∞.
2 +n3 log n = limx→∞ 3
+log n
n
√
2
2
y
Poniamo y = log n. Allora log n ≤ n sse y ≤ n sse y ≤ 2 . Questa disuguaglianza
p√ è vera1 a partire
√
dal valore y = 4 e dunque dal valore n0 = 16 in poi. Quindi log(log n) ≤ log( n) ≤
n = n4 .
9
3
2
Pertanto esiste un n0 tale che 5n log(log n) ≤ 5n 4 ∈ O(n + log n). Similmente possiamo ordinare
tutte le altre funzioni. Alla fine ci risulta che
• 3n2 + n3 log n ∈ O(n5 − n4 + 2n),
• n3 + log n ∈ O(3n2 + n3 log n),
• 5n2 log(log n) ∈ O(n3 + log n),
• n2 + 2n log n ∈ O(5n2 log(log n)),
• n2 ∈ O(n2 + 2n log n),
3
• 10n 2 ∈ O(n2 ),
3
• n log n ∈ O(10n 2 ),
• n + 6 log n ∈ O(n log n)
√
• n ∈ O(n + 6 log n),
√
• 17 log n ∈ O( n),
• log(log n) ∈ O(17 log n).
Inoltre nessuna coppia di queste funzioni appartiene alla stessa Θ-classe di equivalenza.
Esercizio 2.12. Scrivere un algoritmo che prende in input una lista e calcola il numero dei massimi
locali. Calcolarne quindi la complessità.
Svolgimento: Ecco un possibile algoritmo.
24
9
10
input : array A di interi
output : il numero dei massimi locali in A
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
c := 0;
j := 0;
while ( j < lentgh ( A ) ) {
if (j >0 and j +1 < length ( A ) and A [j -1] < A [ j ] > A [ j +1]) {
c := c +1;
}
if (j >0 and j = length ( A ) -1 and A [j -1] < A [ j ]) {
c := c +1;
}
if (j -1=0 and j < length ( A ) and A [j -1] > A [ j ]) {
c := c +1;
}
j := j +1;
}
return c ;
La sua complessità è Θ(n) dove n è la lunghezza di A.
Esercizio 2.13. Risolvere le seguenti relazioni di ricorrenza utilizzando il master theorem:
1. T (n) = 2T ( n4 ) + n0.51
√
2. T (n) = 3T ( n3 ) + n
3. T (n) = 64T ( n8 ) + n2 log n
Svolgimento: Per i casi base (n = 1) le ricorrenze sono definite come Θ(1).
1. Siccome 0.51 = log4 (2)+ε con ε > 0, abbiamo che n0.51 = Ω(nlog4 (2)+ε ). Risolvendo la disequazione
2
2
2( n4 )0.51 ≤ cn0.51 rispetto ad n otteniamo 40.51
≤ c. Siccome 40.51
< 1, possiamo scegliere per c
2
un valore nell’intervallo ( 40.51 , 1), che soddisfa la disequazione per ogni n ≥ 0. Quindi T (n) =
Θ(nlog4 (2) ).
√
2. Abbiamo che n ∈ O(nlog3 (3)−ε ) e dunque T (n) = Θ(nlog3 (3) ).
3. Abbiamo che n2 log n ∈ Ω(nlog8 (64)+ε ). Risolvendo la disequazione 64( n8 )2 (log n8 ) ≤ cn2 log n
rispetto ad n otteniamo 1 − log3 n ≤ c. Al crescere di n l’intervallo (0, 1 − log3 n ) tende all’intervallo
(0, 1). Siccome c va scelto nell’intervallo (1 − log3 n , 1), non c’è un valore di c che vada bene per tutti
gli n a partire da un certo naturale in poi. Qui il master theorem non si può applicare.
Esercizio 2.14. Trovare i limiti asintotici superiori ed inferiori (migliori possibile) per T (n) in ciascuna
delle seguenti ricorrenze (supponendo che T (n) = Θ(1) per n ≤ 2).
1. T (n) = 3T ( n2 ) + n log n
2. T (n) = 5T ( n5 ) +
n
log n
√
3. T (n) = 4T ( n2 ) + n2 n
4. T (n) = 3T ( n3 + 5) +
5. T (n) = 2T ( n2 ) +
n
2
n
log n
6. T (n) = T (n − 1) +
1
n
7. T (n) = T (n − 1) + log n
8. T (n) = T (n − 2) + 2 log n
√
√
9. T (n) = nT ( n) + n
25
Svolgimento: (1) Siccome log2 3 > 1 e n log n ∈ O(nlog2 3−ε ) per un certo ε > 0 (p.e.
applicare il Master Theorem per concludere che T (n) ∈ Θ(nlog2 3 ).
log2 3−1
)
2
basta
(2) In questo caso logb a = log5 5 = 1. Notiamo che logn n 6∈ Θ(n) perché n 6∈ O( logn n ). Per vedere
cn
c
ciò basta considerare la disequazione n ≤ log
n , le cui soluzioni in n sono intervalli della forma (0, 2 ].
n
n
cn
Notiamo anche che log n 6∈ O(n1−ε ) perché la disequazione log n ≤ nε ha come soluzioni i naturali n tali
nε
che log
n ≤ c. Per ogni c > 0 fissato questa disequazione ha un insieme finito di soluzioni naturali. Infine
esiste un ε > 0 tale che logn n ∈ Ω(n1+ε ) perché
n
log n
n
log n
1
nε log n
≥
sse
≥
sse
≥
cn1+ε
cnnε
c
e per ogni c > 0 fissato esiste un n0 > 0 tale che per ogni n ≥ n0 abbiamo
ad esempio ε = 21 . Ora vediamo che
n
5
log
n
5
log n
5 log n−5 log 5
≥
sse
≥
1
nε log n
≥ c. Basta prendere
c logn n
c
x
La funzione f (x) = 5 log log
x−5 log 5 è monotona decrescente per x ≥ 10 e quindi basta scegliere c = f (10) < 1.
Pertanto il Master Theorem ci dice che T (n) ∈ Ω(nε ).
√
5
(3) In questo caso logb a = log2 4 = 2. Inoltre n2 n = n 2 ∈ Ω(n2+ε ) (basta scegliere 0 < ε < 12 ). Ora
vediamo che
5
5
4( n2 ) 2 ≥ cn 2
sse
4
≥ c
5
22
5
Quindi possiamo applicare il Master Theorem e concludere che T (n) ∈ Θ(n 2 ).
(4) Nella forma T (n) = 3T ( n3 + 5) + n2 non si può applicare il Master Theorem. Però possiamo effettuare
y−15
un cambio di variabile y 7→ n + 15 e risolvere S(y) = 3T ( y3 ) + y−15
∈ Θ(y log3 3 )
2 . Siccome f (y) =
2
concludiamo che S(y) ∈ Θ(y log y). Infine T (n) = S(y − 15) ∈ Θ((y − 15) log(y − 15)) = Θ(n log n).
(5) In questo caso logb a = 1. Non si possono applicare i casi del Master Theorem perché
e logn n 6∈ Ω(n1+ε ). Allora useremo il metodo dell’iterazione:
T (n)
=
=
=
≤
n
log n
Plog n−1 1
2log2 n + n j=02
log2 nj
2
Plog2 n−1
1
n + n j=0
Plog n log2 n−j
n + n i=12 1i
n + n log2 n
Quindi T (n) ∈ O(n + n log2 n).
(6) Non si possono applicare i casi del Master Theorem ma useremo il metodo dell’iterazione:
T (n)
=
=
=
≤
T (n − 1) +
Pn−1 1
n−i
Pi=0
n
1
i=1 i
n
Quindi T (n) ∈ O(n).
26
1
n
6∈ O(n1−ε )
(7) Non si possono applicare i casi del Master Theorem ma useremo il metodo dell’iterazione:
T (n)
T (n − 1) + log n
Pn−1
1
log(n−i)
Pi=0
n
1
=
=
=
≤
i=1 log i
n
Quindi T (n) ∈ O(n).
(8) Non si possono applicare i casi del Master Theorem ma useremo il metodo dell’iterazione:
T (n)
= T (n − 2) + 2 log n
P n−1
1
2
=
i=0 2 log(n−2i)
≤ n
Quindi T (n) ∈ O(n).
(9) Non si possono applicare i casi del Master Theorem ma useremo il metodo dell’iterazione:
T (n)
1
1
= n + n 2 T (n 2 )
1
1
1
1
1
= n 20 + n 21 (n 21 + n 22 T (n 22 ))
Pd
1
1 Qi
2i
2j
=
i=0 (n
j=0 n )
P
Pd
1
i
1 j
=
(n 2i n j=0 ( 2 ) )
Pi=0
1
1
d
=
(n 2i n2− 2i )
Pdi=0 1i n2
2
=
1 )
i=0 (n
n 2i
Pd
2
=
i=0 n
2
≤ n log2 n
1
dove d è il massimo numero naturale tale che n 2d ≥ 1. Troviamo che T (n) ∈ O(n2 log n).
27
28
Chapter 3
Correttezza degli algoritmi
3.1
Algoritmi iterativi
Per dimostrare la correttezza degli algoritmi iterativi si utilizza l’invariante di ciclo, ossia una proposizione
(riguardante i contenuti delle variabili della procedura o programma) che rispetta le seguenti proprietà:
Inizializzazione: la proposizione è vera immediatamente prima di entrare nel ciclo.
Mantenimento: se la proposizione è vera prima di eseguire un’iterazione, lo è anche al termine dell’iterazione.
Terminazione: al termine del ciclo, la proposizione permette di ricavare la proprietà che permette di
dimostrare la correttezza dell’algoritmo.
Esempio 3.1. Consideriamo l’algoritmo fibonacciIterativoModificato, del quale vogliamo dimostrare
il fatto che calcoli l’n-esimo numero di Fibonacci; l’invariante di ciclo è il seguente:
Ad ogni iterazione, b = Fi−1 e a = Fi−2
Inizializzazione: poiché i = 3, dobbiamo verificare che b = F2 e a = F1 ; la dimostrazione è immediata,
poiché b = 1 = F2 e a = 1 = F1 .
Mantenimento: assumiamo l’invariante verificato per una generica i-esima iterazione e dimostriamo
che esso vale anche al termine di tale iterazione; ricordiamo inoltre, nonostante non sia esplicitamente indicato, che al termine dell’iterazione viene incrementato l’indice i. Grazie alla nostra
assunzione, abbiamo b = Fi−1 e a = Fi−2 : alla variabile c viene assegnato il valore della somma
a + b = Fi−2 + Fi−1 = Fi , alla variabile a viene assegnato il valore di b = Fi−1 , alla variabile b il
valore di c = Fi ed i viene incrementata (i = i + 1); è immediato dimostrare che continua a valere
l’invariante, ossia che b = F(i+1)−1 e a = F(i+1)−2 .
Terminazione: all’uscita del ciclo, si ha i = n + 1, dunque b = F(n+1)−1 = Fn , che è il valore restituito,
come volevasi dimostrare.
3.2
Algoritmi ricorsivi
La dimostrazione viene svolta procedendo per induzione; occorre formalizzare una proprietà utile per
dimostrare la correttezza dell’algoritmo e provare che:
• valga per i casi base;
• assumendo che valga per problemi di dimensione inferiore, ossia per le chiamate ricorsive eseguite,
provare che vale anche per il problema iniziale (passo induttivo).
Esempio 3.2. Consideriamo l’algoritmo fibonacciRicorsivo e formalizziamo la seguente proprietà:
L’output della chiamata di funzione fibonacciRicorsivo(n) è l’n-esimo numero di Fibonacci.
29
Casi base:
per n = 1 e n = 2, l’algoritmo restituisce 1 = F1 = F2 .
Ipotesi induttiva:
supponiamo che, per k < n, fibonacciRicorsivo(k) restituisca Fk .
Passo induttivo: l’algoritmo restituisce fibonacciRicorsivo(n − 1) + fibonacciRicorsivo(n − 2)
(sia n > 2); per ipotesi, essi restituiscono, rispettivamente, Fn−1 e Fn−2 , la cui somma è Fn , come
volevasi dimostrare.
30
Chapter 4
Pile e code
4.1
Pile
La pila è una struttura dati, realizzabile sia con strutture indicizzate, sia collegate, che può essere descritta
dal seguente schema generale:
Dati: una sequenza S di n elementi.
Operazioni:
isEmpty() → booleano
Restituisce true se S è vuota, false altrimenti.
push(elem e) → void
Aggiunge e come ultimo elemento di S.
pop() → elem
Toglie da S l’ultimo elemento e lo restituisce.
top() → elem
Restituisce l’ultimo elemento di S, senza rimuoverlo.
4.2
Code
La coda, come la pila, è una struttura dati realizzabile sia mediante strutture indicizzate, sia con strutture
collegate; la realizzazione di una coda segue il seguente schema generale:
Dati: una sequenza S di n elementi.
Operazioni:
isEmpty() → booleano
Restituisce true se S è vuota, false altrimenti.
enqueue(elem e) → void
Aggiunge e come ultimo elemento di S.
dequeue() → elem
Toglie da S il primo elemento e lo restituisce.
first() → elem
Restituisce il primo elemento di S, senza rimuoverlo.
4.3
Esercizi
Esercizio 4.1. Realizzare una coda Q utilizzando due pile P1 e P2 .
31
Svolgimento: Supponiamo di avere due pile P1 e P2 . La pila P1 ci serve per mantenere la collezione
di oggetti, mentre P2 serve come ausilio per le operazioni.
Metodo enqueue dell’oggetto Q.
void enqueue(Item x){
while(!P_1.isEmpty()){
P_2.push(P_1.pop())
}
P_1.push(x)
while(!P_2.isEmpty()){
P_1.push(P_2.pop())
}
}
Item dequeue(Item x){
return P_1.pop()
}
boolean isEmpty(){
return P_1.isEmpty()
}
Esercizio 4.2. Realizzare una pila P utilizzando due code Q1 e Q2 .
Svolgimento: Supponiamo di avere due code Q1 e Q2 . La pila Q1 ci serve per mantenere la collezione
di oggetti, mentre Q2 serve come ausilio per le operazioni.
void push(Item x){
while(!Q_1.isEmpty()){
Q_2.enqueue(Q_1.dequeue())
}
Q_1.enqueue(x)
while(!Q_2.isEmpty()){
Q_1.enqueue(Q_2.dequeue())
}
}
Item pop(Item x){
return Q_1.dequeue()
}
boolean isEmpty(){
return Q_1.isEmpty()
}
32
Chapter 5
Liste
5.1
Esercizi
Esercizio 5.1. Data una lista singola scorrendola una sola volta spezzarla a metà:
void split(List l, List *l1, List *l2)
Proof. Ipotizziamo una dichiarazione di lista come seguente
struct Node{
int key; // chiave intera
struct Node* next;
}
typedef Node* List;
Il metodo viene invocato cosı̀, dove l è una lista esistente:
List l1,l2;
split(l,&l1,&l2);
Abbiamo due possibilità:
• possiamo fare in modo che l1 ed l2 puntino alla testa di due nuove liste create copiando il contenuto
dei record componenti la lista l;
• possiamo alterare la lista l e far puntare l1 al primo record di l, ed l2 al record di mezzo di l.
Scegliamo la seconda opzione, che comporta meno operazioni.
void split(List l, List *l1, List *l2){
List p1 = l, p2 = l;
List tail1;
while(p2 != NULL & p2->next != NULL){
tail1 = p1;
p1 = p1->next;
p2 = (p2->next)->next;
}
*l1 = l;
tail1->next = NULL;
*l2 = p1;
}
Esercizio 5.2. Realizzare una funzione
Lista union(Lista S1, Lista S2)
33
che date due liste S1 e S2 i cui record non contengono campi chiave ripetuti, restituisce una lista S i cui
record non contengono campi chiave ripetuti e contiene tutti e soli gli elementi di S1 ed S2.
Proof. Adottiamo una soluzione in cui viene creata una nuova lista contenente copie dei campi chiave
delle liste originarie.
Lista union(Lista S1, Lista S2){
List aux = (List)malloc(sizeof(struct Node));
List head = aux;
List tail = aux;
//copio nella nuova lista gli elementi di S1
//che non compaiono in S2
while(S1 != NULL){
tail = aux;
found = 0;
List p2 = S2;
while(p2 != NULL && found == 0){
if (S1->key == p2->key) found = 1;
p2=p2->next;
}
if (found == 0) aux->key = S1->key;
aux->next = (List)malloc(sizeof(struct Node));
aux = aux->next;
}
//copio gli elementi di S2 in coda alla nuova lista
while(S2 != NULL){
tail = aux;
aux->key = S2->key;
aux->next = (List)malloc(sizeof(struct Node));
aux = aux->next;
}
tail->next = NULL;
return head;
}
Esercizio 5.3. Realizzare una funzione
Lista union(Lista S1, Lista S2)
che date due liste S1 e S2 i cui record sono ordinati in maniera crescente secondo il campo chiave e non
sono ripetuti, restituisce una lista S i cui record sono ordinati in maniera crescente secondo il campo
chiave e non sono ripetuti e contiene tutti e soli gli elementi di S1 ed S2.
Proof. Adottiamo una soluzione in cui viene creata una nuova lista contenente copie dei campi chiave
delle liste originarie.
Lista union(Lista S1, Lista S2){
List aux = (List)malloc(sizeof(struct Node));
List head = aux;
List tail = aux;
while(S1 != NULL && S2 != NULL){
tail = aux;
if (S1->key < S2->key){
aux->key = S1->key;
S1=S1->next;
}
if (S1->key == S2->key){
aux->key = S1->key;
S1=S1->next;
34
S2=S2->next;
}
if (S2->key < S1->key){
aux->key = S2->key;
S2=S2->next;
}
aux->next = (List)malloc(sizeof(struct Node));
aux = aux->next;
}
while(S1 == NULL && S2 != NULL){
tail = aux;
aux->key = S2->key;
S2=S2->next;
aux->next = (List)malloc(sizeof(struct Node));
aux = aux->next;
}
while(S2 == NULL && S1 != NULL){
tail = aux;
aux->key = S1->key;
S1=S1->next;
aux->next = (List)malloc(sizeof(struct Node));
aux = aux->next;
}
tail->next = NULL;
}
return head;
}
Esercizio 5.4. Trovare il numero di foglie ed il numero di nodi interni di un albero k-ario completo
d’altezza h.
Proof. Intanto precisiamo che un albero k-ario completo è d’altezza h si caratterizza per il fatto che ogni
nodo interno ha esattamente k figli e tutte le foglie hanno la stessa profondità. Un albero con solo la
radice ha altezza 0.PIl numero totale di nodi dell’albero è dato dalla somma dei nodi dei vari livelli, dato
h
dalla sommatoria i=0 k i . L’ultimo addendo corrisponde al numero di foglie dell’albero, mentre tutti i
precedenti addendi danno il numero dei nodi interni. Dunque il numero di foglie è k h , ed il numero di
Ph−1
h
nodi interni è i=0 k i = 1−k
1−k .
Esercizio 5.5. Trovare l’altezza di un albero k-ario completo con n foglie.
Proof. Siccome sappiamo dal precedente esercizio un albero di altezza h ha n = k h foglie, allora h =
logk n.
Esercizio 5.6. Dato un array v ordinato in senso crescente di n interi, il cui valore può essere solo 0 e
1, progettare un algoritmo efficiente, di tipo divide-et-impera, che restituisca il numero di occorrenze del
numero 1 in v. Calcolare la complessità al caso pessimo dell’algoritmo proposto indicando, e risolvendo,
la corrispondente relazione di ricorrenza.
Proof. Osserviamo che, visto come una stringa, l’array v è della forma 0∗ 1∗ .
int occorrenze1(int v[], int start, int end){
int pivot = (end-start)/2;
if
if (v[pivot] == 0)
return occorrenze1(v,pivot+1,end);
else {
int left=0, right=0;
if (pivot-1>=start)
35
left = occorrenze1(v,start,pivot-1);
if (pivot+1<=end)
right = occorrenze1(v,pivot+1,end);
return 1+ left+ right;
}
}
Esercizio 5.7. Progettare un algoritmo di costo Θ(n log n) (in termini di tempo di esecuzione), ricevuto
in input un intero k e un array a, non ordinato, di n elementi distinti, restituisca il k-esimo elemento più
piccolo di a.
Proof. Possiamo ordinare l’array tramite mergeSort in-place (ovvero a viene modificato) e poi restituire
a[k − 1].
36
Chapter 6
Alberi
6.1
Introduzione
Definizione 6.1. Un albero è una coppia T = (N, A) costituita da un insieme N di nodi e da un insieme
A ⊆ N × N di coppie di nodi, dette archi.
In un albero, ogni nodo v (esclusa la radice) ha un solo padre tale che (u, v) ∈ A; ogni nodo, inoltre,
può avere un certo numero di figli w tali che (v, w) ∈ A, ed il loro numero è detto grado del nodo. Un
nodo senza figli è chiamato foglia e tutti i nodi che non sono né foglia né radice sono detti nodi interni.
La profondità di un nodo è definita come segue:
• la radice ha profondità zero;
• se un nodo ha profondità k, i suoi figli avranno profondità k + 1.
I nodi che hanno lo stesso padre sono detti fratelli, e dunque avranno la stessa profondità. L’altezza
di un albero è definita come la massima profondità tra quelle delle varie foglie.
Un albero d-ario è un albero in cui tutti i nodi tranne le foglie hanno grado d; se tutte hanno medesima
profondità, si dice che è completo.
Lo schema generale delle operazioni eseguibili su un albero è il seguente:
Dati: un insieme di nodi e un insieme di archi
Operazioni:
numNodi() → intero
Restituisce il numero di nodi presenti nell’albero.
grado(nodo v ) → intero
Restituisce il numero di figli del nodo v.
padre(nodo v ) → nodo
Restituisce il padre del nodo v nell’albero, null se v è la radice.
figli(nodo v ) → hnodo, nodo, ..., nodoi
Restituisce i figli del nodo v.
aggiungiNodo(nodo u) → nodo
Inserisce un nuovo nodo vcome figlio di u e lo restituisce. Se v è il primo nodo ad essere inserito
nell’albero, diventa la radice.
aggiungiSottoalbero(albero a, nodo u) → albero
Inserisce nell’albero il sottoalbero a in modo che la sua radice diventi figlia di u.
rimuoviSottoalbero(nodo v ) → albero
Stacca e restituisce l’intero sottoalbero radicato in v.
37
6.2
Rappresentazioni
Le modalità di rappresentazione di un albero possono essere essenzialmente suddivise in due categorie:
le rappresentazioni indicizzate e le rappresentazioni collegate.
Le prime, nonostante risultino essere di facile realizzazione, rendono difficoltoso l’inserimento e la
cancellazione di nodi nell’albero, mentre le seconde risultano essere decisamente più flessibili, nonostante
siano leggermente più complesse da implementare.
6.2.1
Rappresentazioni indicizzate
Vettore padri
La più semplice rappresentazione possibile per un albero T = (N, A) con n nodi è quella basata sul
vettore padri : è un array di dimensione n le cui celle contengono coppie (info, parent), dove info è il
contenuto informativo del nodo e parent il riferimento al padre (o null se si tratta della radice). Con
questa implementazione, da ogni nodo è possibile risalire al padre in tempo O(1), ma la ricerca dei figli
richiede tempo O(n).
Vettore posizionale
Consideriamo un albero d-ario completo con n nodi, dove d ≥ 2; un vettore posizionale è un array P di
dimensione n tale che P [v] contiene l’informazione associata al nodo v e i figli sono memorizzati nelle
posizioni P [d · v + i], con 0 ≤ i ≤ d − 1. Da ciascun nodo è possibile risalire in tempo costante sia al
proprio padre (indice bv/dc se v non è la radice), sia a uno qualsiasi dei propri figli.
6.2.2
Rappresentazioni collegate
Puntatori ai figli
Se ogni nodo dell’albero ha al più grado d, è possibile mantenere in ogni nodo un puntatore a ciascuno
dei possibili figli (se il figlio non è presente, si imposta a null il riferimento).
Lista figli
Se il numero massimo di figli non è noto a priori, si può mantenere per ogni nodo una lista di puntatori
ai figli.
Primo figlio - fratello successivo
Variante della soluzione precedente, prevede di mantenere per ogni nodo un puntatore al primo figlio e
uno al fratello successivo (null se non è presente, rispettivamente, il figlio o fratello); per scandire tutti i
figli di un nodo, è sufficiente visitare il primo figlio e poi tutti i suoi fratelli.
6.3
Visite
1
2
4
3
5
6
Figure 6.1: Esempio di albero
38
6.3.1
Visita in profondità
In una visita in profondità, si prosegue la visita dall’ultimo nodo lasciato in sospeso: può essere realizzata
mediante l’utilizzo di pile o, in maniera più semplice, usando la ricorsione.
1
2
3
4
5
6
7
visitaSimmetrica ( nodo r → void ) {
if ( r != null ) {
visitaSimmetrica ( figlio sinistro di r )
visita il nodo r
visitaSimmetrica ( figlio destro di r )
}
}
Tre varianti classiche della visita in profondità sono le seguenti:
Visita in preordine: si visita prima la radice, poi vengono eseguite le chiamate ricorsive sul figlio
sinistro e destro (Figure 6.1 → 1, 2, 4, 3, 5, 6)
Visita simmetrica: si effettua prima la chiamata sul figlio sinistro, poi si visita la radice e infine si
esegue la chiamata ricorsiva sul figlio destro (Figure 6.1 → 4, 2, 1, 5, 3, 6)
Visita in postordine: si effettuano prima le chiamate ricorsive sul figlio sinistro e destro, poi viene
visitata la radice (Figure 6.1 → 4, 2, 5, 6, 3, 1)
6.3.2
Visita in ampiezza
La visita in ampiezza è realizzata tramite l’uso di code e la sua caratteristica principale è il fatto che i
nodi vengono visitati per livelli: l’ordine di visita dell’albero rappresentato in Figure 6.1 è 1, 2, 3, 4, 5, 6.
6.4
Alberi binari di ricerca
Definizione 6.2. Un albero binario di ricerca è un albero binario che soddisfa le seguenti proprietà:
• ogni nodo v contiene un elemento elem(v) cui è associata una chiave chiave(v) presa da un dominio
totalmente ordinato;
• le chiavi nel sottoalbero sinistro di v sono minori o uguali a chiave(v);
• le chiavi nel sottoalbero destro di v sono maggiori o uguali a chiave(v).
Un albero binario di ricerca è descritto dal seguente schema generale:
Dati: un albero binario di ricerca di altezza h e n nodi, ciascuno contenente coppie (elem, chiave).
Operazioni:
search(chiave k ) → elem
Partendo dalla radice, ricerca un elemento con chiave k, usando la proprietà di ricerca per
decidere quale sottoalbero esplorare (complessità O(h)).
insert(elem e, chiave k ) → void
Crea un nuovo nodo v contenente la coppia (e, k) e lo aggiunge all’albero in posizione opportuna, in modo da mantenere la proprietà di ricerca (complessità O(h)).
delete(elem e) → void
Se il nodo v contenente l’elemento e ha al più un figlio, elimina v collegando il figlio all’eventuale
padre, altrimenti scambia il nodo v con il suo predecessore ed elimina il predecessore (complessità O(h)).
39
6.5
Esercizi
Esercizio 6.1. Trovare l’altezza di un albero binario.
Svolgimento:
HEIGHT(x){
if (x == NIL)
return 0
return 1+MAX(HEIGHT(x.left),HEIGHT(x.right))
}
Esercizio 6.2. Trovare l’altezza di un albero k-ario realizzato con left child, right sibling.
Svolgimento:
HEIGHT(x){
if (x == NIL)
return 0
y := x.leftChild
height := 0
while (y != NIL){
a:=HEIGHT(y)
if (height < a)
height:= a
y := y.rightSibling
}
return 1+height
}
Esercizio 6.3. Scrivere un algoritmo per trovare il numero di foglie di un albero binario. Dimostrarne
la correttezza per induzione. Calcolarne la complessità.
Svolgimento:
LEAVES(x){
if (x == NIL)
return 0
if (x.left == NIL AND x.right == NIL)
return 1
return LEAVES(x.left)+LEAVES(x.right)
}
Esercizio 6.4. Verificare se un albero binario è completo.
Svolgimento: Ricordiamo che un albero binario è completo se ogni nodo interno ha esattamente 2
figli e tutte le foglie hanno la stessa profondità. Immaginiamo che l’algoritmo COM P LET E ritorni come
risultato un tipo coppia. Nel linguaggio C si può simulare questo comportamento utilizzando parametri
passati per indirizzo. Se c = (x, y) è un dato di tipo coppia, allora π1 (c) = x e π2 (c) = y.
COMPLETE(x){
if (x == NIL)
return (TRUE, 0)
if ((x.left == NIL AND x.right != NIL) OR (x.left != NIL AND x.right == NIL))
return (FALSE,1)
c_l = COMPLETE(x.left)
c_r = COMPLETE(x.right)
return (\pi_1(c_r) AND \pi_1(c_r) AND (\pi_2(c_l) == \pi_2(c_r)), 1+MAX(\pi_2(c_l),\pi_2(c_r)))
40
}
L’albero x è completo sse π1 (COM P LET E(x)) = T RU E.
Esercizio 6.5. Un nodo di un albero binario detto centrale se il numero di foglie del sotto-albero di cui
radice pari alla somma delle chiavi dei nodi appartenenti al percorso dalla radice al nodo stesso. Trovare
il numero di nodi centrali in un albero binario.
Esercizio 6.6. Sia x un albero binario. Dato un certo livello k vogliamo stampare i nodi di quel livello.
Qual’è la complessità dell’algoritmo rispetto a k?
Svolgimento:
PRINT-LEVEL(x,k,i){
if (x != NIL && i == k)
print(x)
if (x != NIL && i < k){
PRINT-LEVEL(x.left,k,i+1)
PRINT-LEVEL(x.right,k,i+1)
}
}
Per stampare i nodi del livello k bisogna invocare P RIN T −LEV EL(x, k, 0). La complessità di P RIN T −
LEV EL(x, k, 0) è lineare nel numero di nodi dell’albero ristretto ai nodi di profondità alpiù k. Il caso
Pk
peggiore è quello in cui l’albero è completo: in tal caso i nodi di profondità alpiù k sono j=0 2j = 2k+1 −1.
Quindi la complessità è alpiù esponenziale in k, O(2k+1 ).
Ricordiamo che un BST A è un albero binario (memorizzato con una struttura a nodi linkati) tale
che per ogni nodo x in A, ogni nodo y nel sottoalbero sinistro di x ed ogni nodo z nel sottoalbero destro
di x abbiamo che y.key ≤ x.key ≤ z.key.
Esercizio 6.7 (Appello del 03-02-09). Discutere la complessità asintotica dell’algoritmo di ricerca di una
chiave in un albero BST con n nodi.
Svolgimento:
TREE-SEARCH(x, k)
if (x == NIL OR k == x.key)
return x
if (k < x.key)
return TREE-SEARCH(x.left, k)
else
return TREE-SEARCH(x.right, k)
Il tempo di calcolo è O(h), dove h è l’altezza dell’albero. Attenzione che la complessità in generale non è
O(log n), visto che il caso peggiore è un albero a forma di linea che non contiene la chiave k. Alla peggio
dunque il tempo di esecuzione è O(n), mentre se il BST è completo la complessità scende a O(log n).
Esercizio 6.8 (Appello del 05-02-10). Un nodo di un albero binario è detto centrale se il numero di
foglie del sottoalbero di cui è radice è pari alla somma delle chiavi dei nodi appartenenti al percorso dalla
radice al nodo stesso.
a. Scrivere una funzione efficiente in C che restituisca il numero di nodi centrali.
b. Discutere la complessità della soluzione trovata.
c. Se vogliamo modificare la funzione in modo che restituisca l’insieme dei nodi centrali che tipo di
struttura dati si può utilizzare per rappresentare l’insieme? La complessità dell’algoritmo deve
rimanere la stessa che nel caso (a).
Si deve utilizzare il seguente tipo per la rappresentazione di un albero binario:
41
typedef struct Node{
int key;
struct node * left;
struct node * right;
} * Node;
Svolgimento:
int centrali(Node * x, int * count, int keysum){
int leaf_number = 0;
if (x != NULL){
if (x-> left == NULL && x->right == NULL){
if (keysum == 1) (*count)++;
return 1;
}
else{
int left_leaf_number = centrali(x->left, count, keysum+(x->key));
int right_leaf_number = centrali(x->right, count, keysum+(x->key));
leaf_number = left_leaf_number+right_leaf_number;
if (keysum == leaf_number) (*count)++;
}
}
return leaf_number;
Se root è un puntatore alla radice dell’albero e count è una variabile intera, questa funzione va invocata
cosı̀:
int leaves = centrali(root, &count, 0);
Alla fine ritroviamo in leaves il numero di foglie dell’albero e in count il numero di nodi centrali. La
complessità della nostra soluzione è Θ(n) dove n è il numero di nodi dell’albero.
6.6
Alberi AVL
Nel caso peggiore, l’altezza di un albero binario di ricerca può essere proporzionale al numero n di nodi,
mentre, se fosse bilanciato, avrebbe altezza logaritmica, migliorando l’efficienza delle operazioni; per
risolvere il problema, vengono usati gli alberi AVL.
Definizione 6.3. Un albero è bilanciato in altezza se le altezze dei sottoalberi sinistro e destro di ogni
nodo differiscono al più di un’unità.
Definizione 6.4. Il fattore di bilanciamento β(v) di un nodo v è la differenza tra l’altezza del sottoalbero
sinistro e quella del sottoalbero destro di v:
β(v) = altezza(sin(v)) − altezza(des(v))
(6.1)
Un albero AVL è un albero bilanciato in altezza e, oltre all’elemento e alla chiave, ciascun nodo
mantiene l’informazione sul fattore di bilanciamento. Per mantenere l’albero bilanciato a seguito di
cancellazioni ed inserimenti, occorre operare, qualora risultasse necessario, delle rotazioni.
6.6.1
Ribilanciamento tramite rotazioni
Le operazioni di rotazione vengono effettuate su nodi sbilanciati, ossia nodi il cui fattore di bilanciamento,
in valore assoluto, è maggiore o uguale a 2; si possono distinguere vari casi:
Sinistra - sinistra: si esegue quando un nodo ha coefficiente di bilanciamento +2 ed il figlio destro un
coefficiente pari a 0 o a +1 (in Figure 6.2 il nodo sbilanciato è quello con chiave 3).
Destra - destra: si esegue quando un nodo ha coefficiente di bilanciamento -2 ed il figlio sinistro un
coefficiente pari a 0 o -1 (in Figure 6.2 il nodo sbilanciato è quello con chiave 6).
42
Sinistra - destra: si esegue quando un nodo ha coefficiente di bilanciamento -2 e il figlio sinistro un
coefficiente pari a +1 (in Figure 6.2 il nodo sbilanciato è quello con chiave 3).
Destra - sinistra: si esegue quando un nodo ha coefficiente di bilanciamento +2 e il figlio destro un
coefficiente pari a -1 (in Figure 6.2 il nodo sbilanciato è quello con chiave 6).
Proprietà 6.1. Una rotazione SS, SD, DS o DD, applicata ad un nodo v con fattore di bilanciamento
±2, fa decrescere di 1 l’altezza del sottoalbero radicato in v prima della rotazione.
Mentre l’operazione di ricerca si svolge esattamente come in un albero binario di ricerca, le operazioni
di cancellazione e inserimento sono soggette a modifica:
Inserimento
• si crea un nuovo nodo e lo si inserisce nell’albero con lo stesso procedimento usato per i BST;
• si ricalcolano i fattori di bilanciamento che sono mutati in seguito all’inserimento (solo i fattori
di bilanciamento dei nodi nel cammino tra la radice e il nuovo elemento possono mutare e
possono essere facilmente calcolati risalendo nel cammino dalla foglia verso la radice);
• se nel cammino compare un nodo con fattore di bilanciamento pari a ±2, occorre ribilanciare mediante rotazioni (si può dimostrare che è sufficiente una sola rotazione per bilanciare
l’albero).
Cancellazione
• si cancella il nodo con il medesimo procedimento usato nei BST;
• si ricalcolano i fattori di bilanciamento mutati in seguito all’operazione (solo i nodi nel cammino
tra la radice e il padre del nodo eliminato possono aver subito una modifica del fattore);
• per ogni nodo con fattore di bilanciamento pari a ±2, procedendo dal basso verso l’alto, si
opera una rotazione (può essere necessario eseguire più rotazioni, in questo caso).
6.6.2
Alberi di Fibonacci
Definizione 6.5. Tra tutti gli alberi di altezza h bilanciati in altezza, un albero di Fibonacci ha il minimo
numero di nodi.
Un albero di Fibonacci di altezza h può essere costruito unendo, tramite l’aggiunta di una radice, un
albero di Fibonacci di altezza h − 1 e uno di altezza h − 2; è facile verificare che ogni nodo interno di un
albero di questo tipo ha fattore di bilanciamento pari a +1 (o -1, dipende dalla costruzione): studiamo
ora il rapporto tra numero di nodi ed altezza di un albero di Fibonacci.
Lemma 6.1. Sia Th un albero di Fibonacci di altezza h e sia nh il numero dei suoi nodi; risulta h =
Θ(log nh )
Proof. Per dimostrare la tesi, proviamo prima che nh = Fh+3 − 1, per induzione su h.
Passo base: si ha h = 0 e n0 = 1 = 2 − 1 = F3 − 1.
Ipotesi induttiva: supponiamo nk = Fk+3 − 1 per k < h.
Passo induttivo: sia h > 0; si ha:
nh = 1 + nh−1 + nh−2 = 1 + Fh+2 − 1 + Fh+1 − 1 = Fh+3 − 1
Poiché Fh = Θ(φh ), con φ ≈ 1.618, possiamo concludere che h = Θ(log nh ).
Corollario 6.1. Un albero AVL con n nodi ha altezza O(log n).
Proof. Sia h l’altezza dell’albero AVL; per dimostrare che h = O(log n) consideriamo l’albero di Fibonacci
di altezza h, avente nh nodi: per definizione di albero di Fibonacci si ha nh ≤ n e, per il lemma appena
dimostrato, si ottiene il risultato voluto.
43
4
4
6
3
2
7
5
2
6
5
3
1
7
1
(a) Rotazione SS
4
4
6
3
2
7
5
1
6
5
3
1
7
2
(b) Rotazione SD
5
5
6
3
8
4
2
3
6
4
2
8
1
7
1
7
(c) Rotazione DS
5
6
3
2
1
5
4
3
7
2
1
8
(d) Rotazione DD
Figure 6.2: Esempi di rotazione
44
7
4
6
8
Chapter 7
Heap e code di priorità
7.1
Heap
Definizione 7.1. Uno heap è un albero binario quasi completo che rispetta la proprietà heap definita
come segue:
• nel caso di min-heap, il padre ha associata una chiave minore o uguale alle chiavi di tutti i suoi figli;
• nel caso di max-heap, il padre ha associata una chiave maggiore o uguale alle chiavi associati ai suoi
figli.
Solitamente uno heap viene rappresentato utilizzando un vettore (a partire dalla posizione di indice
1) dove, per un generico nodo in posizione i, si ha:
• il padre in posizione bi/2c;
• il figlio sinistro in posizione 2 · i;
• il figlio destro in posizione 2 · i + 1.
Lemma 7.1. Uno heap con n nodi ha altezza O(log n).
Proof. Sia h l’altezza dello heap con n nodi; poiché lo heap è completo almeno fino a profondità h − 1, e
h
−1
= 2h − 1 nodi, segue che 2h − 1 < n; con semplici
un albero binario completo di profondità h − 1 ha 22−1
passaggi algebrici, si giunge alla relazione h < log2 (n + 1) = O(log n).
Nel seguito faremo riferimento solo a max-heap, ma quanto detto (con opportune modifiche dovute
alla condizione heap diversa) può essere applicato anche a min-heap.
7.1.1
Costruzione
Per costruire un max-heap, si utilizza una funzione ausiliaria heapifyDown.
Tale funzione verifica che il valore di A[i] sia maggiore di quello dei figli, se presenti, e, in caso
contrario, A[i] migra verso il basso fino a che non vale la proprietà heap; la complessità è O(h), dove h è
l’altezza dell’albero. La funzione per la costruzione dello heap è la seguente:
Per dimostrare la correttezza della funzione, usiamo il seguente invariante di ciclo:
∀k : i + 1 ≤ k ≤ length(A), A[k] è radice di uno heap.
Inizializzazione: si ha i = length(A)/2; ∀k : i + 1 ≤ k ≤ length(A), A[k] è una foglia e dunque,
banalmente, radice di uno heap.
Mantenimento: assumiamo che la proprietà valga all’inizio di una generica iterazione e dimostriamo
che vale anche alla fine; si ricorda che, ad ogni passo, i viene decrementata, nonostante non sia
esplicitamente indicato. La chiamata heapifyDown(A, i ) fa diventare A[i] radice di uno heap,
senza togliere questa proprietà agli altri: al termine dell’iterazione si ha che A[(i − 1) + 1], A[(i −
1) + 2], ..., A[length(A)] sono radici di heap, ossia quanto volevamo dimostrare.
Terminazione: si ha i = 0 e che A[1], A[2], ..., A[length(A)] sono tutti radici di heap, dunque A è uno
heap.
45
Complessità
Per il calcolo della complessità, ci servono le seguenti formule:
∞
X
1
1−x
xk =
k=0
(7.1)
Proof.
x0 = 1
∞
∞
X
X
xk −
xj = 1
k=0
∞
X
k=0
∞
X
j=1
xk −
∞
X
xk+1 = 1
k=0
xk − x ·
k=0
∞
X
xk = 1
k=0
(1 − x) ·
∞
X
xk = 1
k=0
∞
X
xk =
k=0
∞
X
1
1−x
k · xk =
k=0
Proof.
∞
X
xk =
k=0
x
(1 − x)2
(7.2)
1
1−x
∞
d X k
d 1
x =
dx
dx 1 − x
k=0
∞
X
k · xk−1 =
k=0
x·
∞
X
1
(1 − x)2
k · xk−1 =
k=0
∞
X
k · xk =
k=0
x
(1 − x)2
x
(1 − x)2
Per procedere, dobbiamo calcolare quante volte la funzione buildHeap richiama heapify; dividiamo
n
. Per nodi alla stessa altezza h, la
i vertici in base all’altezza: per ogni altezza h ce ne sono al più 2h+1
complessità di heapify è O(h); la complessità totale risulta essere:
blog nc l
X
h=0
n m
2h+1
· O(h) = O n ·
blog nc l
!
h m
X
h=0
2h+1
Sostituendo x = 1/2 nell’equazione (7.2) otteniamo:
∞ l
X
1/2
h m
=
=2
h+1
2
(1 − 1/2)2
h=0
Segue che:
O n·
blog nc l
X
h=0
h m
2h+1
!
!
∞ l
X
h m
=O n·
= O(n)
2h+1
h=0
46
7.1.2
Realizzazione
Per realizzare le operazioni di inserimento e rimozione, risulta utile un’ulteriore funzione d’appoggio,
simmetrica a heapifyDown.
void heapifyUp (int A[], int i)
if (i != 1 && A[i] < A[padre(i)]){
swap(A[i], A[padre(i)])
heapifyUp(A, padre(i))
}
Inserimento: viene creato un nuovo nodo e inserito come foglia nella prima posizione libera del vettore
usato per realizzare lo heap; a questo punto, per ripristinare la proprietà heap, viene richiamata la
funzione heapifyUp sul nodo appena inserito. La complessità dell’operazione è O(log n).
Cancellazione: il nodo da rimuovere viene scambiato con l’ultimo elemento occupato del vettore; a
questo punto, non sapendo se il nodo appena scambiato debba scendere o salire nell’albero, vengono
richiamate su di esso entrambe le funzioni heapifyUp e heapifyDown per ripristinare la proprietà
heap. La complessità dell’operazione, anche in questo caso, è O(log n).
7.2
Code di priorità
Le code di priorità possono essere realizzate usando degli heap: a seconda del fatto che sia usato un minheap o un max-heap, si possono avere code a min-priorità e code a max-priorità. Una coda a min-priorità
è descritta dal seguente schema generale:
Dati: un insieme S di n elementi di tipo elem a cui sono associate chiavi di tipo chiave prese da un
universo totalmente ordinato.
Operazioni:
findMin() → elem
Restituisce l’elemento di S con la chiave minima.
insert(elem e, chiave k ) → void
Aggiunge a S un nuovo elemento e con chiave K.
delete(elem e) → void
Cancella da S l’elemento e.
deleteMin() → void
Cancella da S l’elemento con chiave minima.
increaseKey(elem e, chiave d ) → void
Incrementa della quantità d la chiave dell’elemento e in S.
decreaseKey(elem e, chiave d ) → void
Decrementa della quantità d la chiave dell’elemento e in S.
Una coda a max-priorità, invece, al posto di funzioni per ricercare ed estrarre l’elemento con chiave
minima, fornirà funzioni per ricercare ed estrarre quello con chiave massima.
47
48
Chapter 8
Algoritmi di ordinamento
8.1
Selection sort
L’algoritmo di ordinamento per selezione opera nel modo seguente: supponiamo che i primi k elementi
siano ordinati; l’algoritmo sceglie il minimo degli n − k elementi non ancora ordinati e lo inserisce in
posizione k + 1; partendo da k = 0 e iterando n volte il ragionamento, si ottiene la sequenza interamente
ordinata.
void selectionSort (int A[]){
for k = 0 to length(A) - 2{
m = k + 1
for j = m + 1 to length(A) {
if (A[j] < A[m])
m = j
}
swap(A[m], A[k + 1])
}
}
Complessità
L’estrazione del minimo (righe 2 - 4) richiede n − k − 1 confronti e, poiché il ciclo esterno viene eseguito
n − 1 volte, si ha:
T (n) =
8.2
n−2
X
n−1
X
k=0
i=1
(n − k − 1) =
i = Θ(n2 )
Insertion sort
L’algoritmo di ordinamento per inserzione opera in modo simile al precedente: supponiamo che i primi k
elementi siano ordinati; l’algoritmo prende il (k + 1)-esimo elemento e lo inserisce nella posizione corretta
rispetto ai k elementi già ordinati; partendo da k = 0 ed iterando n volte il ragionamento, il vettore viene
completamente ordinato.
void insertionSort (int A[]){
for k = 1 to length(A) - 1{
value = A[k + 1]
j = k
while (j > 0 && A[j] > value){
A[j + 1] = A[j]
j-}
A[j + 1] = value
49
}
}
Complessità
Le righe 4 - 6 individuano la posizione in cui l’elemento va inserito, eseguendo al più k confronti; poiché
il ciclo esterno viene eseguito n − 1 volte, si ha:
T (n) =
n−1
X
k = O(n2 )
k=1
8.3
Bubble sort
L’algoritmo di ordinamento a bolle opera una serie di scansioni del vettore: in ogni scansione sono
confrontate coppie di elementi adiacenti e viene effettuato uno scambio se i due elementi non rispettano
l’ordinamento; se durante una scansione non viene eseguito alcuno scambio, allora il vettore è ordinato.
void bubbleSort (int A[]){
for i = 1 to length(A) - 1 {
for j = 2 to (n - i + 1) {
if (A[j - 1] > A[j])
swap(A[j - 1], A[j])
if (non ci sono stati scambi)
break
}
}
}
Lemma 8.1. Dopo l’i-esima scansione, gli elementi A[n − i + 1], ..., A[n] sono correttamente ordinati e
occupano la loro posizione definitiva, ovvero ∀h, k con 1 ≤ h ≤ k ∧ n − i + 1 ≤ k ≤ n, risulta A[h] ≤ A[k].
Proof. Procediamo per induzione sul numero i dell’iterazione.
Caso base: si ha i = 1; dopo la prima scansione, l’elemento massimo di A ha raggiunto l’n-esima
posizione, quindi il passo base è facilmente verificato.
Ipotesi induttiva: supponiamo che l’enunciato valga per le prime i − 1 scansioni.
Passo induttivo: verifichiamo la validità della proprietà dopo l’i-esima scansione; al termine di tale
scansione, il massimo degli elementi A[1], ..., A[n − i + 1] avrà raggiunto la posizione n − i + 1 e
quindi, per ogni h < n − i + 1, risulta A[h] ≤ A[n − i + 1]; combinando questa osservazione con
l’ipotesi induttiva, si può verificare facilmente la validità dell’enunciato.
Complessità
Per il lemma appena dimostrato, dopo al più n−1 cicli il vettore è ordinato e, nel caso peggiore, all’i-esima
scansione vengono eseguiti n − i confronti; si ottiene quindi:
T (n) =
n−1
X
(n − i) =
i=1
8.4
n−1
X
j = O(n2 )
j=1
Heap sort
L’algoritmo di ordinamento heap sort, come dice il nome, si basa sull’utilizzo di una struttura dati di
tipo heap.
50
void heapSort (int A[]){
buildHeap(A)
for i = length(A) - 1 downto 1 {
swap(A[i], A[1])
heapifyDown(A[1..(i - 1)], 1)
}
}
Complessità
La funzione buildHeap ha complessità O(n), mentre heapifyDown, che viene richiamata n volte, ha
complessità O(log n); in totale si ha:
T (n) = O(n) + n · O(log n) = O(n · log n)
8.5
Merge sort
L’algoritmo di ordinamento per fusione si basa sulla tecnica del divide et impera:
Divide: si divide il vettore in due parti di uguale dimensione; se ha un numero dispari di elementi, una
delle due parti conterrà un elemento in più.
Impera: supponendo di avere le due sottosequenze ordinate, si fondono in una sola sequenza ordinata.
Complessità
La complessità della funzione ausiliaria merge è O(n); la relazione di ricorrenza è la seguente:
T (n) = 2 · T (n/2) + O(n)
Applicando il secondo caso del teorema fondamentale, si ottiene:
T (n) = Θ(n · log n)
Per quanto riguarda lo spazio di memoria, mergeSort non opera in loco e pertanto ha un’occupazione
di memoria pari a 2 · n.
8.6
Quicksort
Anche questo tipo di algoritmo segue un approccio di tipo divide et impera:
Divide: sceglie un elemento x della sequenza (il perno), partiziona la sequenza in due sottosequenze
contenenti, rispettivamente, gli elementi minori o uguali a x e gli elementi maggiori di x, e ordina
ricorsivamente le due sottosequenze.
Impera: concatena le sottosequenze ordinate.
L’invariante della procedura partition è il seguente:
Proprietà 8.1. In ogni istante, gli elementi A[i], ..., A[inf − 1] sono minori o uguali al perno, mentre gli
elementi A[sup + 1], ..., A[f ] sono maggiori del perno.
51
Complessità
L’algoritmo ordina un vettore A di dimensione n suddividendolo in due sottovettori, A1 e A2 , di dimensioni
a e b tali che a + b = n − 1, ed ordinando ricorsivamente A1 e A2 ; poiché la partizione richiede n − 1
confronti, si può scrivere la seguente relazione di ricorrenza:
T (n) = T (a) + T (b) + n − 1
Nel caso peggiore, si ha che ad ogni passo viene scelto come perno l’elemento più piccolo (o più grande)
della porzione di vettore considerato, riducendo la relazione di ricorrenza a:
T (n) = T (n − 1) + n − 1 = O(n2 )
Dal risultato sembra che quickSort sia un algoritmo inefficiente; l’idea per migliorarlo consiste
nell’usare la randomizzazione. Supponiamo di scegliere il perno come il k-esimo elemento di A, dove
k è scelto a caso: per calcolare il numero atteso di confronti, bisogna calcolare la somma dei tempi di
esecuzione pesandoli in base alla probabilità di fare una certa scelta casuale; in questo caso, la scelta è il
valore di k e la probabilità di scegliere il k-esimo elemento come perno, ∀k, 1 ≤ k ≤ n è 1/n. Otteniamo
la seguente relazione di ricorrenza:
T (n) =
n−1
X
a=0
n−1
X2
1
· (T (a) + T (n − a − 1) + n − 1) = n − 1 +
· T (a)
n
n
a=0
(8.1)
Dimostriamo, usando il metodo di sostituzione, che T (n) = O(n · log n), ossia che esiste una costante
α > 0 tale per cui C(n) ≤ α · n · log n per ogni n ≥ n0 , n0 > 0.
Passo base: si ha n = 1; T (1) = 0 = α · (1 · log 1) per ogni α.
Ipotesi induttiva: supponiamo che esista α tale per cui T (i) ≤ α · i · log i per i < n.
Passo induttivo: usando l’ipotesi induttiva nella relazione 8.1 e la definizione di somme integrali, si
ottiene:
n−1
X
T (n) = n−1+
i=0
Z
n−1
n−1
X2
2
2·α n
2·α X
i·log i ≤ n−1+
·T (i) ≤ n−1+
·α·i·log i = n−1+
·
·
x·log xdx
n
n
n i=2
n
2
i=0
Procediamo usando il metodo di integrazione per parti:
T (n) ≤ n − 1 +
n
2 · α 2 log n n2
· n ·
−
+ 2 · ln 2 + 1 = n − 1 + α · n · log n − α · − O(1) ≤ α · n · log n
n
2
4
2
quando n − 1 < α · (n/2), ossia per α ≥ 2.
Possiamo dunque concludere che l’algoritmo quickSort, su un input di dimensione n ha complessità
O(n2 ) nel caso peggiore, ma il numero atteso di confronti è O(n · log n).
8.7
Counting sort
Si tratta di un algoritmo per ordinare vettori di interi di n elementi compresi nell’intervallo [1, k].
Complessità
Il primo ciclo ha complessità O(k), il secondo O(n) e il terzo, nonostante sia costituito da due cicli
innestati, è anch’esso O(n); per rendersene conto, basta osservare che il tempo necessario per eseguire gli
n incrementi dev’essere uguale a quello necessario per gli n decrementi, e gli incrementi sono eseguiti dal
secondo ciclo. In totale la complessità risulta essere:
T (n) = O(n + k)
L’algoritmo countingSort è assai efficiente se k = O(n), mentre è preferibile usarne altri all’aumentare
dell’ordine di grandezza di k; si osservi, inoltre, che la memoria occupata è anch’essa O(n + k).
52
8.8
Radix sort
Si tratta di un algoritmo per ordinare interi in tempo lineare, quando l’intervallo dei numeri che possono essere presenti nel vettore è troppo grande per pensare di utilizzare il countingSort; consiste
nell’ordinare, ad ogni ciclo, in base all’i-esima cifra, partendo da quella meno significativa fino a quella
più significativa.
Complessità
Sia k il valore più grande presente nel vettore; l’algoritmo esegue log10 k chiamate di bucketSort, ciascuna
delle quali ha costo O(n), per un totale di O(n · log k); è possibile aumentare il valore della base usata nel
bucketSort, senza però aumentare significativamente il tempo di esecuzione, come afferma il seguente
teorema.
Teorema 8.1. Usando come base per il bucketSort un valore b = Θ(n), l’algoritmo radixSort ordina
n interi in [1, k] in tempo
!!
log k
O n· 1+
log n
Proof. Ci sono logb k = O(logn k) passate di bucketSort, ciascuna delle quali dal costo O(n): il tempo
totale è O(n · logn k) = O(n · log k/ log n), per le regole del cambiamento di base dei logaritmi; per
considerare il caso k < n, aggiungiamo O(n) alla complessità, che è il tempo richiesto per leggere la
sequenza. Il risultato della somma è quanto si voleva dimostrare.
8.9
Limitazione inferiore per algoritmi basati sul confronto
Usiamo la seguente proprietà per stabilire la limitazione:
Proprietà 8.2. Il numero di confronti fatti dall’algoritmo A nel caso peggiore è pari all’altezza dell’albero
di decisione, ovvero alla lunghezza del più lungo cammino dalla radice ad una foglia.
Lemma 8.2. Un albero di decisione per l’ordinamento di n elementi contiene almeno n! foglie.
Proof. Ad ogni foglia dell’albero di decisione non ci sono più confronti da effettuare, dunque corrisponde
ad una soluzione del problema dell’ordinamento; ogni soluzione corrisponde, a sua volta, ad una permutazione degli n elementi da ordinare, quindi l’albero deve contenere un numero di foglie pari almeno al
numero di permutazioni degli n elementi da ordinare, ossia n! foglie.
Lemma 8.3. Sia T un albero binario in cui ogni nodo interno ha esattamente 2 figli e sia k il numero
delle sue foglie: l’altezza è almeno log2 k.
Proof. Definiamo h(k) come l’altezza di un albero binario con k foglie (con 2 figli per ogni nodo interno)
e dimostriamo il lemma per induzione su k.
Caso base: si ha k = 1; si tratta, dunque, di un albero con un solo nodo, quindi l’altezza è 0 ≥ log2 1.
Ipotesi induttiva: assumiamo che h(i) ≥ log2 i per i < k.
Passo induttivo: poiché uno dei sottoalberi ha almeno la metà delle foglie, si ha:
k
k
h(k) ≥ 1 + h
≥ 1 + log2
= 1 + log2 k − log2 2 = log2 k
2
2
come volevasi dimostrare.
Teorema 8.2. Il numero di confronti necessari per ordinare n elementi nel caso peggiore è Ω(n · log n).
53
Proof. Per il lemma 8.2, il numero di foglie di un albero di decisione è almeno n! e, per la proprietà 8.2,
il numero di confronti necessari per l’ordinamento è, nel caso peggiore, pari all’altezza dell’albero, ossia
≥ log n! per il lemma 8.3. La disuguaglianza di De Moivre - Stirling afferma che, per n sufficientemente
grande, si ha:
n n
n n √
√
1
≤ n! ≤ 2 · π · n ·
· 1+
2·π·n·
e
e
12 · n − 1
Si ha dunque:
n n
√
n! ≈ 2 · π · n ·
e
da cui:
1
log n! ≈ n · log n − 1.4427 · n − · log n + 0.826
2
Da questa relazione segue direttamente la limitazione inferiore che si voleva dimostrare.
54
Chapter 9
Tabelle hash
Sono strutture dati realizzate mediante vettori che permettono, nel caso medio, di eseguire operazioni
in tempo costante. Sia U l’universo delle chiavi associabili agli elementi che vogliamo memorizzare; nel
caso in cui U non sia l’insieme di numeri naturali [0, m − 1], l’utilizzo di tavole ad accesso diretto (ossia
tavole che usano direttamente la chiave come indice per reperire l’elemento nella tabella) risulta troppo
costoso, a causa del fattore di carico troppo basso: per questo motivo vengono usate le funzioni hash per
la trasformazione delle chiavi in indici.
Definizione 9.1. Il fattore di carico di una tavola è definito come il rapporto α = n/m tra il numero n
di elementi in essa memorizzati e la sua dimensione m.
Definizione 9.2. Una funzione hash è una funzione h : U → {0, ..., m − 1} che trasforma chiavi in indici
di una tavola.
Definizione 9.3. Una funzione hash h è perfetta se è iniettiva, ovvero ∀u, v ∈ U, u 6= v ⇒ h(u) 6= h(v).
Affinché una funzione hash sia perfetta, occorre che |U | ≤ m, ossia ci dev’essere spazio per tanti
elementi quante sono le chiavi possibili: questo comporta un enorme spreco di memoria se l’insieme delle
chiavi è molto grande. Se una funzione hash non è perfetta, allora potrebbe verificarsi una collisione,
ossia si possono avere più chiavi diverse con lo stesso valore associato: per risolvere il problema, occorre
operare delle strategie di risoluzione delle collisioni, che hanno lo svantaggio di ridurre le prestazioni.
9.1
Definizione di funzioni hash
Definizione 9.4. Sia
Q(i) =
X
P (k)
k:h(k)=i
la probabilità che, scegliendo una chiave, questa finisca nella cella i; una funzione hash h gode della
proprietà di uniformità semplice se, ∀i ∈ {0, ..., m − 1},
Q(i) =
1
m
Per definire funzioni hash con buone caratteristiche di uniformità, con l’assunzione che ogni chiave
abbia la stessa probabilità di essere scelta, si usano spesso le seguenti tecniche.
Metodo della divisione: il metodo calcola il resto della divisione della chiave k per m, dove m è la
dimensione della tabella hash; sebbene nella maggior parte dei casi si hanno buoni risultati, in altri
potrebbero verificarsi molte collisioni: la bontà del metodo dipende dalla scelta di m, che sarebbe
preferibile fosse un numero primo vicino ad una potenza di due, e dal fatto che la funzione hash
dovrebbe dipendere da tutti i bit della chiave.
Metodo del ripiegamento: consiste nel dividere la chiave k in l parti e definire la funzione hash come
l’applicazione di una funzione f , con codominio {0, ..., m − 1}, sulle parti di chiave ottenute con la
divisione; ossia:
h(k) = f (k1 , k2 , ..., kl )
55
9.2
Risoluzione delle collisioni
9.2.1
Liste di collisione
Questo metodo consiste nell’associare a ciascuna cella della tabella hash una lista di chiavi, detta lista di
collisione, di lunghezza media pari al fattore di carico α; per questo motivo, assumendo che la funzione di
hashing goda della proprietà di uniformità semplice, si ha che il tempo medio necessario per un’operazione
di ricerca o eliminazione è O(1 + α), mentre l’inserimento può essere realizzato in tempo O(1).
9.2.2
Indirizzamento aperto
Nel caso in cui la posizione h(k) in cui inserire una chiave k sia già occupata, il metodo prevede di
posizionarla in un’altra cella vuota, anche se quest’ultima potrebbe spettare di diritto ad un’altra chiave.
Le operazioni vengono realizzate come segue:
Inserimento:
• se v[h(k)] è vuota, inserisci la coppia (el, k) in tale posizione;
• altrimenti, a partire da h(k), ispeziona le celle della tabella secondo una sequenza opportuna di
indici c(k, 0), c(k, 1), ..., c(k, m−1) e inserisci nella prima cella vuota; la sequenza, chiaramente,
deve contenere tutti gli indici {0, ..., m − 1}.
Ricerca:
• se, durante la scansione delle celle, ne viene trovata una con la chiave cercata, restituisci
l’elemento trovato;
• altrimenti, se si arriva a una cella vuota o si è scandita l’intera tabella senza successo, restituisci
null.
Cancellazione: affinché la ricerca con il metodo appena descritto funzioni, occorre adottare una strategia
particolare per la cancellazione, ossia utilizzare un valore speciale canc nel campo el dell’elemento
che si vuole rimuovere: in particolare, l’inserimento tratterà tale cella come vuota e si fermerà su
di essa, mentre la ricerca la oltrepasserà.
Le prestazioni delle operazioni implementate dipendono dalla particolare funzione c(k, i) usata, ossia
dal tipo di scansione scelto:
Scansione lineare:
c(k, i) = (h(k) + i) mod m con 0 ≤ i < m
Dopo un certo numero di inserimenti, tendono a formarsi degli agglomerati sempre più lunghi di celle
piene, che comportano un decadimento delle prestazioni; si parla del problema di di agglomerazione
primaria.
Scansione quadratica:
c(k, i) = h(k) + c1 · i + c2 · i2 mod m, con 0 ≤ i < m e c1 e c2 opportuni
Nonostante la scansione quadratica distribuisca le chiavi in modo da evitare l’agglomerazione primaria, ogni coppia di chiavi k1 e k2 con h(k1 ) = h(k2 ) continua a generare la stessa sequenza di
scansione; questo dà luogo all’agglomerazione secondaria.
Hashing doppio:
c(k, i) = bh1 (k) + i · h2 (k)c mod m
con h1 e h2 funzioni hash distinte e m e h2 (k) primi tra loro. Si tratta di un metodo che permette
di eliminare virtualmente il fenomeno dell’agglomerazione, facendo dipendere dalla chiave anche il
passo dell’incremento dell’indice usando una seconda funzione hash.
Complessità
Nell’ipotesi che le chiavi associate agli elementi di una tavola hash siano prese dall’universo delle chiavi
con probabilità uniforme, il numero medio di passi richiesto da un’operazione di ricerca (contando anche
le celle marcate come canc) è descritto nella seguente tabella:
56
Esito ricerca
Scansione lineare
Scansione quadratica / Hashing doppio
Chiave trovata
Chiave non trovata
1
1
2 + 2·(1−α)
1
1
2 + 2·(1−α)2
− α1 · ln(1 − α)
1
1−α
57
58
Chapter 10
Tecniche algoritmiche
10.1
Divide et impera
Si tratta di una tecnica già incontrata in precedenza, utilizzata, ad esempio, dagli algoritmi di ordinamento mergeSort e quickSort; il principio su cui si basa questa tecnica consiste nel dividere i dati in
ingresso in un certo numero di sottoinsiemi (divide), risolvere ricorsivamente il problema sui sottoinsiemi
e ricombinare le sottosoluzioni cos ottenute per ottenere la soluzione del problema originario (impera).
10.2
Programmazione dinamica
Si tratta di una tecnica bottom-up introdotta, in maniera informale, con l’algoritmo fibonacciIterativo,
che si basa sull’utilizzo di una tabella per memorizzare le soluzioni dei sottoproblemi incontrati: in
questo modo, qualora si dovesse trovare un sottoproblema già risolto, si può usare la tabella per ricavarne
velocemente la soluzione, invece di ricalcolarla. In maniera del tutto generale, si può descrivere la tecnica
di programmazione dinamica nel modo seguente:
1. identifichiamo dei sottoproblemi del problema originario ed utilizziamo una tabella per memorizzare
i risultati intermedi ottenuti;
2. all’inizio, definiamo i valori iniziali di alcuni elementi della tabella, corrispondenti ai sottoproblemi
più semplici;
3. al generico passo, avanziamo in modo opportuno sulla tabella calcolando il valore della soluzione
di un sottoproblema (corrispondente ad un dato elemento della tabella) in base alla soluzione dei
sottoproblemi precedentemente risolti;
4. alla fine, restituiamo la soluzione del problema originario, che è stata memorizzata in un particolare
elemento della tabella.
Esempio 10.1. Vogliamo risolvere il problema riguardante il calcolo della distanza tra due stringhe di
caratteri: siano X = x1 x2 ...xm e Y = y1 y2 ...yn due stringhe di caratteri, rispettivamente di lunghezza m
e n; possiamo definire il costo della trasformazione di X in Y come il numero di operazioni da apportare
alla stringa X per ottenere Y . Le possibili operazioni che possiamo compiere sono:
• inserisci(a): inserisci il carattere a nella posizione corrente della stringa;
• cancella(a): cancella il carattere a dalla posizione corrente della stringa;
• sostituisci(a, b): sostituisci il carattere a con il carattere b nella posizione corrente della stringa.
Assumendo che il costo di ciascuna operazione sia 1, possiamo definire il costo della trasformazione
tra X e Y come la somma di tutti i costi pagati per compiere le operazioni di trasformazione da X in Y ;
la distanza tra due stringhe X e Y sarà il costo minimo di trasformazione di X in Y .
Indichiamo con δ(X, Y ) la distanza tra le stringhe X e Y ; inoltre, data una stringa X, definiamo il
prefisso di X fino al carattere i-esimo come la stringa Xi = x1 x2 ...xi se 1 ≤ i ≤ m, la stringa vuota
X0 = ∅ se i = 0. Anziché risolvere il problema generale, proviamo a considerare i sottoproblemi P (i, j),
ovvero trovare la distanza δ(Xi , Yj ) tra il prefisso Xi e il prefisso Yj :
59
• alcuni sottoproblemi sono particolarmente semplici: la soluzione del sottoproblema P (0, j) è immediata, in quanto è sufficiente inserire, uno dopo l’altro, i j caratteri di Yj e dunque si ha δ(X0 , Yj ) = j;
analogamente, la soluzione del sottoproblema P (i, 0) consiste nel cancellare, uno dopo l’altro, gli i
caratteri di Xi e dunque si ha δ(Xi , Y0 ) = i;
• il problema che vogliamo risolvere è il sottoproblema P (m, n), ossia consiste nel calcolo della distanza δ(Xm , Yn ).
Ci manca solo un passo per poter scrivere l’algoritmo: come calcolare il valore della soluzione del
sottoproblema P (i, j) in funzione della soluzione dei sottoproblemi precedentemente risolti; chiaramente
ci sono varie possibilità, a seconda dei valori di xi e yj . In particolare, se xi = yj , il costo minimo per
trasformare Xi in Yj è pari al costo minimo per trasformare Xi−1 in Yj−1 ; definendo D come la matrice
in cui vengono salvati i risultati dei sottoproblemi, si ha D[i, j] = D[i − 1, j − 1]. Invece, se xi 6= yj ,
dobbiamo distinguere in base all’ultima operazione eseguita per trasformare Xi in Yj :
• inserisci(yj ): il costo minimo per la trasformazione è dato dal costo ottimo per trasformare Xi
in Yj−1 , più 1 per l’inserimento del carattere yj ; si ha dunque:
D[i, j] = D[i, j − 1] + 1
• cancella(xi ): il costo minimo per la trasformazione è dato dal costo ottimo per trasformare Xi−1
in Yj , più 1 per la cancellazione del carattere xi ; si ha dunque:
D[i, j] = D[i − 1, j] + 1
• sostituisci(xi , yj ): il costo minimo per la trasformazione è dato dal costo ottimo per trasformare
Xi−1 in Yj−1 , più per la sostituzione del carattere xi con yj ; si ha dunque:
D[i, j] = D[i − 1, j − 1] + 1
Le precedenti relazioni assumono che sia nota l’ultima operazione utilizzata per la trasformazione
di Xi in Yj , anche se in realtà non è cos; tuttavia sono possibili solo tre tipi di operazione e, dunque,
dev’essere necessariamente usata una di esse: per trovare la scelta ottima, quindi, basta calcolare i valori
relativi alle tre operazioni e scegliere il migliore. La relazione che lega la soluzione di un problema a
quella di alcuni sottoproblemi è la seguente:
(
D[i − 1, j − 1]
se xi = yj
D[i, j] =
1 + min {D[i, j − 1], D[i − 1, j], D[i − 1, j − 1]} se xi 6= yj
La tabella delle distanze mantenuta dall’algoritmo, per la trasformazione della stringa RISOTTO nella
stringa PRESTO, è la seguente:
R
I
S
O
T
T
O
0
1
2
3
4
5
6
7
P
1
1
2
3
4
5
6
7
R
2
1
2
3
4
5
6
7
E
3
2
2
3
4
5
6
7
S
4
3
3
2
3
4
5
6
T
5
4
4
3
3
3
4
5
O
6
5
5
4
3
4
4
4
L’algoritmo è il seguente:
10.3
Paradigma greedy
Si tratta di una tecnica tipicamente usata per risolvere problemi di ottimizzazione; nella situazione più
generale, in un problema di tale tipo avremo:
60
• un insieme di possibili candidati;
• l’insieme dei candidati già utilizzati;
• una funzione ammissibile che verifica se un insieme di candidati fornisce una soluzione (anche non
ottima) al problema;
• una funzione ottimo che verifica se un insieme di candidati fornisce una soluzione ottima al problema;
• una funzione seleziona per estrarre un elemento dall’insieme dei candidati possibili non ancora
esaminati;
• una funzione obiettivo che fornisce il valore di una soluzione.
Per risolvere il problema, occorre trovare un insieme di candidati che:
• è una soluzione;
• ottimizza il valore della funzione obiettivo.
Un algoritmo greedy generico può essere schematizzato come segue:
Esempio 10.2. Consideriamo il problema del resto in un distributore automatico: vogliamo scrivere
un algoritmo goloso per determinare l’insieme di monete da restituire per fare in modo che il numero di
monete sia minore possibile; avremo:
• l’insieme dei candidati possibili è l’insieme delle monete del distributore;
• la funzione ammissibile restituisce vero se il valore delle monete nell’insieme scelto non è superiore
al resto che dev0essere restituito;
• la funzione ottimo restituisce vero se il valore delle monete dell’insieme scelto è pari al resto da
erogare;
• la funzione seleziona sceglie la moneta di valore maggiore tra quelle ancora da considerare;
• la funzione obiettivo restituisce il numero di monete della soluzione;
• la funzione valore restituisce il valore totale delle monete nell’insieme considerato.
Il codice è il seguente:
61
62
Chapter 11
Grafi
11.1
Definizioni preliminari
Definizione 11.1. Un grafo G = (V, E) consiste di:
• un insieme V di vertici ;
• un insieme E di coppie di vertici, detti archi
Definizione 11.2. Un grafo si dice non orientato se E rappresenta una relazione simmetrica in V , ossia
(u, v) ∈ E ⇔ (v, u) ∈ E, altrimenti si dice orientato; nel primo caso si ha |E| ≤ n · (n − 1)/2, mentre nel
secondo |E| ≤ n2 .
Definizione 11.3. Dato un grafo G = (V, E), G0 = (V 0 , E 0 ) è sottografo di G se V 0 ⊆ V e E 0 ⊆
E ∩ (V × V 0 ).
Definizione 11.4. Dato un grafo G = (V, E), G0 = (V 0 , E 0 ) è sottografo indotto di G se V 0 ⊆ V e
E 0 = E ∩ (V × V 0 ) (ossia, se contiene tutti e soli gli archi presenti in E i cui vertici appartengono
entrambi a V 0 ).
Definizione 11.5. Dato G = (V, E), un cammino in G è una sequenza di vertici π = hx0 , x1 , ..., xk i tali
che (xi , xi+1 ) ∈ E, ∀i = 0..k − 1; la lunghezza del cammino è pari al numero di archi che lo compongono.
π si dice cammino semplice se i 6= j ⇒ xi 6= xj ∀i, j = 0..k.
Definizione 11.6. Il vertice v si dice raggiungibile da u se esiste un cammino π tale che x0 = u e xk = v.
Definizione 11.7. Dato un cammino π, un sottocammino è una sequenza di vertici hxi , xi+1 , ..., xj i con
0 ≤ i ≤ j ≤ k.
Definizione 11.8. Un ciclo è un cammino dove x0 = xk ; un ciclo si dice semplice se tutti i vertici
intermedi sono distinti.
Definizione 11.9. La distanza δ(u, v) tra due vertici u e v è il numero minimo di archi di un qualsiasi
cammino da u a v; se v non è raggiungibile da u, allora δ(u, v) = ∞.
Definizione 11.10. Se un cammino π da u a v è tale che length(π) = δ(u, v), allora π è un cammino
minimo.
Definizione 11.11. Un grafo non orientato G = (V, E) è connesso se, per ogni u, v ∈ V , esiste un
cammino da u a v.
Definizione 11.12. Sia G = (V, E) un grafo non orientato; V 0 ⊆ V è componente connessa se:
• il sottografo indotto da V 0 è connesso;
• se V 00 ⊆ V induce un sottografo connesso e V 0 ⊆ V 00 , allora V 0 = V 00 (vincolo di massimalità).
Definizione 11.13. Un albero libero è un grafo non orientato, connesso e aciclico.
Proprietà 11.1. Sia G = (V, E) un albero libero, con |V | = n e |E| = m: allora m = n − 1.
63
Proof. Procediamo per induzione su n.
Caso base: si ha n = 1, ossia l’albero con un nodo (e nessun arco); si ha m = 0 = n − 1.
Ipotesi induttiva: assumiamo valida la proprietà per k < n.
Passo induttivo: sia n > 1; prendiamo v ∈ V e consideriamo il sottografo indotto da V − {v}, ossia
G0 = (V 0 , E 0 ) con V 0 = V − {v} e E 0 = E ∩ (V 0 × V 0 ). Siano V1 , V2 , ..., Vk le componenti connesse
di G0 ; si ha n1 + n2 + ... + nk = n − 1, con ni la cardinalità di Vi . Vi è connessa per definizione ed è
aciclica, essendo sottografo indotto di un grafo aciclico; inoltre |Vi | < n, quindi posso usare l’ipotesi,
ossia mi = ni − 1. Da ciascuna componente connessa ci dev’essere un arco che la congiunge a v,
affinché il grafo di partenza sia un albero libero; si ha:
|E| =
k
X
mi + k =
1=1
k
k
k
X
X
X
(ni − 1) + k =
ni −
1+k =n−1−k+k =n−1
i=1
i=1
i=1
come volevasi dimostrare.
Definizione 11.14. Sia G = (V, E) un grafo non orientato: il grado di un vertice u ∈ V è definito come
deg(u) = |{v ∈ V : (u, v) ∈ E}|
Sia ora G = (V, E) un grafo orientato: il grado entrante e il grado uscente di un vertice u ∈ V sono
definiti, rispettivamente, come
degin (u) = |{v ∈ V : (v, u) ∈ E}|
degout (u) = |{v ∈ V : (u, v) ∈ E}|
Lemma 11.1. Sia G = (V, E) un grafo non orientato; si ha:
X
deg(u) = 2 · |E|
u∈V
Sia ora G = (V, E) un grafo orientato; si ha:
X
degin (u) = |E|
u∈V
X
degout (u) = |E|
u∈V
11.2
Rappresentazione di grafi
Una struttura dati di tipo grafo è descritta dal seguente schema generale:
Dati: un insieme di vertici (di tipo vertice) e di archi (di tipo arco).
Operazioni:
numVertici() → intero
Restituisce il numero di vertici presenti nel grafo.
numArchi() → intero
Restituisce il numero di archi presenti nel grafo.
grado(vertice v ) → intero
Restituisce il numero di archi incidenti sul vertice v.
archiIncidenti(vertice v ) → harco, arco, ..., arcoi
Restituisce, uno dopo l’altro, gli archi incidenti sul vertice v.
estremi(arco e) → hvertice, verticei
Restituisce gli estremi x e y dell’arco e = (x, y).
opposto(vertice x, arco e) → vertice
Restituisce y, l’estremo dell’arco e = (x, y) diverso da x.
64
sonoAdiacenti(vertice x, vertice y) → booleano
Restituisce true se esiste l’arco (x, y), false altrimenti.
aggiungiVertice(vertice v ) → void
Inserisce un nuovo vertice v.
aggiungiArco(vertice x, vertice y) → void
Inserisce un nuovo arco tra i vertici x e y.
rimuoviVertice(vertice v ) → void
Cancella il vertice v e tutti gli archi ad esso incidenti.
rimuoviArco(arco e) → void
Cancella l’arco e.
11.2.1
Lista di archi
È una rappresentazione che si basa sull’utilizzo di:
• una struttura per rappresentare i vertici (tipo arraylist);
• una lista per rappresentare gli archi.
11.2.2
Liste di adiacenza
In questa rappresentazione, ogni vertice viene associato ad una lista contenente i suoi vertici adiacenti
(struttura realizzata con array di liste).
11.2.3
Matrice di adiacenza
È una rappresentazione basata sull’uso di una matrice n × n le cui righe e colonne sono indicizzate dai
vertici del grafo:
(
1 se (u, v) ∈ E
M [u, v] =
0 altrimenti
Si tratta di una rappresentazione utile per il seguente fatto:
k
Proprietà 11.2. Sia Mk = M
| · M{z· ... · M}: si ha M [u, v] = 1 se e solo se esiste un cammino di k vertici
kvolte
che collega u a v.
11.2.4
Confronto tra le rappresentazioni
Operazione
grado(v )
archiIncidenti(v )
sonoAdiacenti(x, y)
aggiungiVertice(v )
aggiungiArco(x, y)
rimuoviVertice(v )
rimuoviArco(e)
Lista di archi
O(m)
O(m)
O(m)
O(1)
O(1)
O(m)
O(m)
Lista di adiacenza
O(deg(v))
O(deg(v))
O(min {deg(x), deg(y)})
O(1)
O(1)
O(m)
O(deg(x) + deg(y))
Matrice di adiacenza
O(n)
O(n)
O(1)
O(n2 )
O(1)
O(n2 )
O(1)
Table 11.1: Tempi di esecuzione
Spazio
Lista di archi
O(m + n)
Lista di adiacenza
O(m + n)
Matrice di adiacenza
O(n2 )
Table 11.2: Spazio occupato
65
11.3
Visite di grafi
Visitare un grafo significa visitarne tutti i nodi, assicurandosi di passare per ciascuno una ed una sola
volta, nell’ipotesi che il grafo sia connesso, partendo da un nodo sorgente s. Durante la visita, i nodi
vengono marcati con opportuni colori per tenere traccia del fatto che siano stati già considerati o meno:
Verde: nodo non ancora considerato.
Giallo: nodo già considerato, ma non ancora visitato.
Rosso: nodo già visitato.
L’algoritmo costruisce un sottografo T di G i cui archi formano un albero radicato in s; mantiene,
inoltre, una struttura U in cui inserisce i vertici da esplorare.
Proprietà 11.3.
• durante il ciclo while, U contiene solo vertici gialli, T − U solo vertici rossi e V − T solo vertici
verdi ;
• ogni vertice raggiungibile da s viene inserito in U ;
• ogni vertice viene colorato esattamente una volta per ogni colore e nell’ordine verde, giallo, rosso:
→ ogni vertice viene inserito in U esattamente una volta;
→ il corpo del while viene eseguito una volta per ogni vertice;
• se il corpo del while viene eseguito sul vertice v, ha lo stesso costo asintotico della ricerca dei vertici
che sono in Adj[v]:
P
→ costo del while = v∈V costo della ricerca dei vertici in Adj[v]
Complessità: il costo totale è dato da ”‘costo del primo for each”’ + ”‘costo del while”’
Lista di archi
costo while =
P
v∈V
O(m) = O(m · n)
costo totale = O(n + m · n) = O(m · n)
Liste di adiacenza
costo while =
P
v∈V
O(deg(v)) = O(
P
v∈V
deg(v)) = O(2 · m) = O(m)
costo totale = O(n + m)
Matrice di adiacenza
costo while =
P
v∈V
O(n) = O(
P
v∈V
n) = O(n2 )
costo totale = O(n + n2 ) = O(n2 )
Se U è gestito come:
• pila, viene eseguita una visita in profondità;
• coda, viene eseguita una visita in ampiezza.
Proprietà 11.4. Sia T l’albero prodotto dalla visita in ampiezza del grafo G: allora, per ogni nodo v,
il livello di v in T è pari alla distanza tra la sorgente s e il nodo stesso; per tale ragione, T si dice albero
dei cammini minimi.
66
F
F
I
B
B
E
D
A
A
D
C
H
F
E
I
B
H
A
D
G
E
C
G
C
G
Figure 11.1: Grafo, albero della visita in profondità e albero della visita in ampiezza partendo da F
67
I
H
68
Chapter 12
Minimo albero ricoprente
Definizione 12.1. Dato un grafo G = (V, E) non orientato e connesso, un albero ricoprente di G è un
sottografo T ⊆ E tale che:
• T è un albero;
• T contiene tutti i vertici di G.
Sia definita una funzione peso w : E → R; possiamo definire il costo di un albero di copertura come
la somma dei pesi degli archi che lo compongono:
w(T ) =
X
w(e)
e∈T
Definizione 12.2. Dato un grafo G = (V, E) non orientato, connesso e pesato sugli archi, un minimo
albero ricoprente di G è un albero di copertura di G di costo minimo.
Le definizioni viste finora possono essere estese al caso in cui G non sia connesso: in tal caso si parlerà
di minima foresta ricoprente.
Lemma 12.1. Siano G = (V, E) un grafo connesso e non orientato, T ⊆ E un albero di copertura,
(u, v) ∈ E − T e p un cammino semplice da u a v con tutti gli archi in T (esiste in quanto T è connesso);
allora (T − {(x, y)}) ∪ {(u, v)} è un albero di copertura.
Proof. Sia T 0 = (T − {(x, y)}) ∪ {(u, v)}: poiché (u, v) ∈
/ T , si ha che (u, v) non fa parte del cammino p,
ossia che (u, v) 6= (x, y), da cui |T 0 | = |V | − 1. Ci rimane da provare che T 0 è connesso.
Consideriamo il cammino p = p1 , hx, yi , p2 , dove:
• p1 è un cammino da u a x che non contiene (x, y);
• p2 è un cammino da y a v che non contiene (x, y).
Siano a, b ∈ V : dobbiamo trovare un cammino da a a b fatto di archi in T 0 . Poiché T è connesso,
esiste un cammino q da a a b fatto da archi in T ; possiamo avere due casi:
1. se q non contiene l’arco (x, y), è un cammino anche in T − {(x, y)} e dunque anche in T 0 ;
2. se q contiene l’arco (x, y), possiamo scrivere q = q1 , hx, yi , q2 dove:
• q1 è un cammino da a a x che non contiene (x, y);
• q2 è un cammino da y a b che non contiene (x, y).
←
←
Sia q 0 = q1 , p1 , hu, vi , p2 , q2 : q 0 è cammino in T 0 , dunque a e b sono connessi in T 0 .
Abbiamo che T 0 è connesso e, avendo n − 1 archi, è anche aciclico: dunque è un albero di copertura,
come volevasi dimostrare.
69
12.1
Teorema fondamentale
Definizione 12.3. Un taglio (S, V − S) di un grafo non orientato G = (V, E) è una partizione di V .
Definizione 12.4. Un arco attraversa il taglio (S, V − S) se uno dei suoi estremi si trova in S e l’altro
in V − S.
Definizione 12.5. Un taglio rispetta un insieme A di archi se nessun arco di A attraversa il taglio.
Definizione 12.6. Un arco che attraversa un taglio si dice leggero se ha peso pari al minimo tra i pesi
di tutti gli archi che attraversano tale taglio.
Definizione 12.7. Sia A sottoinsieme di un qualche albero di copertura minimo; un arco si dice sicuro
per A se può essere aggiunto ad A e quest’ultimo continua ad essere sottoinsieme di un albero di copertura
minimo.
Teorema 12.1. Sia G = (V, E, w) un grafo non orientato, connesso e pesato; sia inoltre:
• A ⊆ E contenuto in qualche MST;
• (S, V − S) un taglio che rispetta A;
• (u, v) ∈ E un arco leggero che attraversa il taglio.
Allora (u, v) è sicuro per A.
Proof.
Ipotesi: esiste T ⊆ E MST tale che A ⊆ T .
Tesi: dobbiamo trovare T 0 ⊆ E MST tale che A ∪ {(u, v)} ⊆ T 0 .
Poiché (S, V − S) rispetta A e (u, v) attraversa il taglio, allora (u, v) ∈
/ A. Possiamo distinguere due
casi:
1. se (u, v) ∈ T , allora A ∪ {(u, v)} ⊆ T MST.
2. se (u, v) ∈
/ T , dal momento che quest’ultimo è connesso, esisterà in esso un cammino semplice p
da u a v; poiché (u, v) attraversa il taglio, esiste almeno un arco (x, y) di p che lo attraversa. Sia
T 0 = (T − {(x, y)}) ∪ {(u, v)}: per il lemma precedentemente dimostrato, è un albero di copertura.
Si ha:
(a) (u, v) ∈ T 0 ;
(b) A ⊆ T e, poiché (x, y) attraversa il taglio ed il taglio rispetta A, (x, y) ∈
/ A; da questo segue:
A ⊆ T − {(x, y)} → A ⊆ (T − {(x, y)}) ∪ {(u, v)} = T 0 → A ∪ {(u, v)} ⊆ T 0
T 0 è un albero di copertura contenente A ∪ {(u, v)}.
(c) w(T 0 ) = w(T ) − w(x, y) + w(u, v); dal momento che (x, y) attraversa il taglio e (u, v) attraversa
il taglio ed è leggero:
w(x, y) ≥ w(u, v) → w(u, v) − w(x, y) ≤ 0 → w(T 0 ) ≤ w(T )
Dal momento che T è MST per ipotesi, segue che w(T 0 ) = w(T ); dunque anche T 0 è MST e
contiene A ∪ {(u, v)}.
Corollario 12.1. Sia G = (V, E, w) un grafo non orientato, connesso e pesato; siano:
• A ⊆ E contenuto in un MST;
• C componente connessa della foresta GA = (V, A);
• (u, v) ∈ E arco leggero che connette C ad un’altra componente connessa di GA .
Allora (u, v) è sicuro per A.
70
Proof. è sufficiente applicare il teorema fondamentale considerando il taglio (C, V − C); poiché:
• il taglio rispetta A;
• (u, v) è leggero per il taglio;
si hanno le ipotesi del teorema, dal quale si ottiene che (u, v) è sicuro per A.
12.2
Algoritmo di Kruskal
L’algoritmo mantiene, istante per istante, una foresta contenuta in qualche MST; per poter gestire gli
insiemi disgiunti che rappresentano le varie componenti connesse della foresta, occorre usare una struttura
dati appropriata. La tecnica utilizzata consiste nel partizionare l’insieme V in k classi tali che:
Sk
• i=1 Vi = V
• i 6= j ⇒ Vi ∩ Vj = ∅
e rapppresentare ogni classe con un elemento della classe stessa; devono essere permesse le seguenti
operazioni:
makeSet(v): crea una classe il cui unico elemento è v e lo elegge come rappresentante della stessa
(costo O(1));
findSet(v): restituisce il rappresentante della classe cui v appartiene (costo O(1));
union(u, v): unisce le classi di u e v (partendo da una partizione generata da n chiamate a
makeSet, se si eseguono k union, il costo totale di tutte le operazioni è O(k · log k)).
Invariante di ciclo: A ⊆ M ST .
Complessità
1. O(1)
2-3. O(n)
4. O(m · log m)
5-8. in tutte le iterazioni, si eseguono in totale:
6. 2 findSet per ogni arco → O(2 · m)
7. A viene aggiornato n − 1 volte (diventa un albero) → O(n)
8. n − 1 union → O(n · log n)
Totale: O(1) + 2 · O(n) + O(m · log m) + O(2 · m) + O(n · log n) = O(m · log m) = O(m · log n), poiché
m = O(n2 ).
Esempio di esecuzione
Assumiamo che gli archi siano ordinati nel modo seguente: AB, CD, BD, BC, AD, AC. Consideriamo,
nell’ordine, gli archi che vengono selezionati:
1. AB : poiché i nodi A e B non sono ancora collegati, l’arco viene selezionato;
2. CD: non esiste un cammino tra C e D, dunque l’arco viene selezionato;
3. BD: non esiste un cammino tra B e D, quindi l’arco viene scelto (il MST è ora pronto);
4. BC : esiste già un cammino tra B e C, dunque l’arco non viene selezionato;
5. AD: è già presente un cammino tra A e D, perciò l’arco viene scartato;
6. AC : tra A e C esiste già un cammino, quindi l’arco non viene scelto.
71
1
A
4
5
6
B
3
C
2
D
1
A
6
B
4
5
6
3
C
2
1
A
D
C
B
4
5
3
2
D
1
A
6
C
B
4
5
3
2
D
Figure 12.1: Simulazione dell’esecuzione dell’algoritmo di Kruskal
12.3
Algoritmo di Prim
L’algoritmo di Prim è fondato direttamente sul teorema fondamentale degli alberi ricoprenti minimi e
mantiene, istante per istante, un albero contenuto in qualche MST; l’algoritmo, inoltre, vuole in ingresso
anche un nodo r ∈ V che sarà la radice dell’albero di copertura.
Viene utilizzata una struttura dati del tipo coda a min-priorità per gestire l’insieme dei vertici Q non
ancora inclusi nell’albero; per ciascun vertice u ∈ Q vengono memorizzati i seguenti attributi:
• key[u] è la chiave di priorità: il suo valore è pari al minimo tra i pesi di tutti gli archi che collegano
u ai nodi dell’albero V − Q, se u è adiacente a un vertice di tale albero, altrimenti il valore è infinito.
• π[u] memorizza il predecessore di u nell’albero generato dall’algoritmo.
Si può ricostruire l’albero finale utilizzando i nodi e le informazioni sui predecessori:
A = {(u, π[u]) ∈ E : u ∈ (V − {r})}
Correttezza: segue direttamente dal teorema fondamentale (ad ogni iterazione, basta considerare il
taglio (V − Q, Q).
Complessità
1-5. O(n)
7. n estrazioni di costo O(log n) → O(n · log n)
8-11. consideriamo tutti i vertici adiacenti a quello estratto: l’operazione più costosa è il decremento della
chiave, del costo O(log n): per ogni vertice u ∈ Q, si paga:
X
O(log n) = deg(u) · O(log n)
v∈Adj[u]
Considerando tutti i vertici, si ha:
X
δ(u) · O(log n) = 2 · m · O(log n) = O(m · log n)
u∈V
12. costo della ricostruzione O(n)
Totale: O(n) + O(n · log n) + O(m · log n) + O(n) = O(m · log n), poiché m ≥ n − 1.
Esempio di esecuzione
Per ciascun nodo, è indicata la coppia (chiave, predecessore) associata ad ogni iterazione del ciclo; i nodi
marcati in nero sono quelli ancora presenti in Q, il nodo rosso è quello estratto nell’iterazione considerata
e quelli blu sono i nodi già estratti da Q ed esaminati.
L’albero restituito dall’algoritmo, ricostruito a partire dai dati dell’ultima figura, è quello costituito
dagli archi AB, BD e CD.
72
(0, NIL)
A
6 5
C
(∞, NIL)
(0, NIL)
A
6 5
C
(2, D)
1
2
1
2
(∞, NIL)
B
4
3
(0, NIL)
A
6 5
D
(∞, NIL)
C
(6, A)
(1, A)
B
4
3
(0, NIL)
A
6 5
D
(3, B)
C
(2, D)
1
2
1
2
(1, A)
B
4
3
(0, NIL)
A
6 5
D
(5, A)
C
(4, B)
(1, A)
B
4
3
(0, NIL)
A
6 5
D
(3, B)
C
(2, D)
1
2
1
2
(1, A)
B
4
3
D
(3, B)
(1, A)
B
4
3
D
(3, B)
Figure 12.2: Simulazione dell’esecuzione dell’algoritmo di Prim (nodo sorgente a)
73
74
Chapter 13
Cammini minimi con sorgente
singola
Definizione 13.1. Sia G = (V, E, w) un grafo orientato e pesato; dato il cammino p = hv0 , v1 , ..., vk i in
Pk
G, il valore w(p) = i=1 w(vi−1 , vi ) rappresenta il peso del cammino.
Dati u, v ∈ V , definiamo:
C(u, v) = {p : p è cammino da u a v in G}
L’insieme è infinito se il grafo è ciclico (infatti basta eseguire più volte un ciclo per ottenere cammini
diversi).
Definizione 13.2. La distanza da u a v corrisponde al peso minimo di un cammino da u a v:
(
+∞
se C(u, v) = ∅
δ(u, v) =
minp∈C(u,v) w(p) se C(u, v) 6= ∅
Definizione 13.3. Sia p = hu, ..., vi: si dice che p è cammino minimo se w(p) = δ(u, v).
Varianti al problema dei cammini minimi
• cammino minimo tra due vertici;
• cammini minimi con sorgente singola;
• cammini minimi con destinazione singola;
• cammini minimi tra tutte le coppie di vertici.
Cicli di peso negativo
3
A
B
-6
L’esistenza dei cicli di peso negativo è problematica nell’ambito della ricerca dei
cammini minimi; nell’esempio in figura, non esiste un cammino minimo tra a e b:
infatti, se supponiamo di aver trovato un cammino minimo, basta eseguire un’altra
volta il ciclo e se ne ottiene uno di peso inferiore.
Rappresentazione dei cammini minimi
Ad ogni vertice, associamo un attributo π[u], che rappresenta il precedente di u in un albero di cammini
minimi; gli algoritmi aggiornano opportunamente i valori di π in modo che, alla fine, l’albero rappresentato
sia un albero di cammini minimi. Definiamo:
Sottografo dei predecessori indotto da π
Gπ = (Vπ , Eπ )
Vπ = {v ∈ V : π[v] 6= N IL} ∪ {s}
Eπ = {(π[v], v) : v ∈ Vπ − {s}}
75
Albero dei cammini minimi: dato G = (V, E, w) orientato e pesato, l’albero dei cammini minimi è
G0 = (V 0 , E 0 ), con V 0 ⊆ V , E 0 ⊆ E tale che:
• V 0 è l’insieme dei vertici raggiungibili da s in G;
• G0 è albero radicato in s;
• ∀u ∈ V 0 , l’unico cammino da s a u in G0 è un cammino minimo da s a u in G.
13.1
Cammini minimi e rilassamento
Lemma 13.1 (Sottocammini di cammini minimi sono cammini minimi). Siano G = (V, E, w) un grafo
orientato e pesato e p = hv0 , v1 , .., vk i cammino minimo da v0 a vk ; presi gli indici 0 ≤ i < j ≤ k, il
sottocammino pij = hvi , ..., vj i è cammino minimo da vi a vj .
Proof. Procediamo per assurdo. Assumiamo che pij non sia minimo: esisterà dunque un cammino q da
vi a vj tale che w(q) < w(pij ); consideriamo ora il cammino p0i , q, pjk :
w(p0i , q, pjk ) = w(p0i ) + w(q) + w(pjk )
< w(p0i ) + w(pij ) + w(pjk )
= w(p)
Ma p è cammino minimo per ipotesi: assurdo. Quindi pij deve essere minimo.
Proposizione 13.1. Siano G = (V, E, w) grafo orientato e pesato e p = hv0 , ..., vk i cammino minimo da
u = v0 a v = vk ; allora ∀i = 0..k, δ(u, v) = δ(u, vi ) + δ(vi , v).
Proof. Segue direttamente dal lemma precedente; sia p = p0i , pik :
w(p) = w(p0i ) + w(pik )
δ(u, v) = δ(u, vi ) + δ(vi , v)
come si voleva dimostrare.
Corollario 13.1. Siano G = (V, E, w) un grafo orientato e pesato e p un cammino da s a v scomponibile
in un cammino da s a u ∈ V , di nome p0 e un arco da u a v; allora il peso di p è pari a δ(s, v) =
δ(s, u) + w(u, v).
Lemma 13.2. Siano G = (V, E, w) un grafo orientato e pesato e s ∈ V un nodo sorgente; allora,
∀(u, v) ∈ E si ha δ(s, v) ≤ δ(s, u) + w(u, v).
Proof. Dobbiamo distinguere due casi:
• u non è raggiungibile da s:
δ(s, u) = +∞ → δ(s, v) = δ(s, u) + w(u, v)
• u è raggiungibile da s, dunque esiste un cammino p da s a u; sia q il cammino formato da p e (u, v):
δ(s, v) ≤ w(q)
= w(p) + w(u, v)
= δ(s, u) + w(u, v)
In entrambi i casi la proprietà è valida, dunque il lemma è dimostrato.
76
Algoritmi
Gli algoritmi che analizzeremo (Dijkstra, Bellman-Ford) aggiornano due attributi assegnati ai nodi:
d[u] : stima del peso del cammino minimo dalla sorgente a u;
π[u] : predecessore di u nell’albero dei predecessori.
Al termine vogliamo avere:
• d[u] = δ(s, u);
• Gπ è l’albero dei cammini minimi.
Inizializzazione
Rilassamento
Lemma 13.3. Dopo la chiamata a relax(u, v, w) si ha che d[v] ≤ d[u] + w(u, v), qualunque sia l’ordine
delle chiamate alla funzione.
Lemma 13.4. Siano G = (V, E, w) un grafo orientato e pesato e s ∈ V il nodo sorgente; supponiamo
che all’inizio sia stata invocata initSingleSource(G, s). Allora:
1. ∀u ∈ V si ha d[u] ≥ δ(s, u);
2. la proprietà al punto 1 rimarrà invariata per qualunque sequenza di relax;
3. nel momento in cui si dovesse verificare che d[u] = δ(s, u) per qualche u ∈ V , allora d[u] non verrà
più modificato da alcuna chiamata di relax.
Proof.
1. Dopo initSingleSource(G, s):
• se u 6= s, d[u] = +∞ ≥ δ(s, u)
• se u = s, d[u] = 0
– se s non sta in un ciclo negativo:
δ(s, s) = 0
d[s] = δ(s, s)
– se s sta in un ciclo negativo:
δ(s, s) = −∞
d[s] = 0 ≥ δ(s, s)
2. Procediamo per assurdo. Assumiamo che, dopo un certo numero di relax, ci sia un vertice v tale
che d[v] < δ(s, v). Supponiamo, inoltre, che v sia il primo vertice per cui valga questa proprietà,
immediatamente dopo la chiamata relax(u, v, w); abbiamo:
d[v] = d[u] + w(u, v) < δ(s, v) ≤ δ(s, u) + w(u, v)
e, per la proprietà transitiva:
d[u] + w(u, v) < δ(s, u) + w(u, v)
d[u] < δ(s, u)
Visto che d[u] non può essere stato modificato da relax(u, v, w), dev’essere stato cambiato prima
da un’altra chiamata; ma v è stato definito come il primo vertice tale per cui d[v] < δ(s, v): assurdo.
3. Osserviamo che:
• nessuna relax, per definizione, incrementa d[u], ma può solo decrementarlo;
• d[u] ≥ δ(s, u) dopo ogni relax.
77
Segue che, dal momento in cui d[u] = δ(s, u), tale valore non può più cambiare.
Corollario 13.2. Se G è inizializzato con initSingleSource(G, s) e v non è raggiungibile da s, allora
d[v] = +∞ non verrà mai aggiornato da alcuna relax.
Lemma 13.5. Siano dati G = (V, E, w) grafo orientato e pesato e p il cammino minimo da s a v che
include (u, v) come arco finale. Supponiamo di inizializzare con initSingleSource(G, s) e chiamiamo
relax più volte; se, ad un certo punto, si ha d[u] = δ(s, u), allora dopo la chiamata relax(u, v, w) si ha
d[v] = δ(s, v).
Proof. Prima di relax(u, v, w) vale δ(s, v) ≤ d[v] per il lemma 13.4 e che d[u] = δ(s, u) per ipotesi. Dopo
relax(u, v, w) si ha che:
d[v] ≤ d[u] + w(u, v)
= δ(s, u) +w(u, v)
| {z }
w(p0 )
= w(p0 ) + w(u, v)
= w(p)
= δ(s, v)
0
dove p è cammino minimo da s a u essendo sottocammino di p. Dal lemma 13.4 si ha che δ(s, v) ≤ d[v],
da cui segue d[v] = δ(s, v).
13.2
Algoritmo di Dijkstra
Complessità
Sia Q implementato come heap binario:
2. O(n)
3. O(1)
4. O(n)
5-9. il ciclo viene eseguito n volte:
6. ogni estrazione costa O(log n), per un totale di O(n · log n)
7. O(1), per un totale di O(n)
8-9. chiama relax una volta per ogni arco del grafo, quindi si ha un costo totale di O(m · log n);
ogni chiamata, infatti, può modificare la chiave di priorità, operazione dal costo O(log n)
Totale: 3 · O(n) + O(1) + O(n · log n) + O(m · log n) = O((n + m) · log n).
Sia Q implementato come array lineare:
2. O(n)
3. O(1)
4. O(n)
5-9. il ciclo viene eseguito n volte:
6. bisogna scorrere ogni volta l’intero array, per un totale di O(n2 )
7. O(1), per un totale di O(n)
8-9. chiama relax una volta per ogni arco del grafo, che ha costo costante; in totale, dunque, il
costo è O(m)
Totale: 3 · O(n) + O(1) + O(n2 ) + O(m) = O(n2 + m).
La prima organizzazione risulta migliore nel caso il grafo sia sparso (m = O(n)), mentre la seconda è
conveniente se il grafo è denso (m = O(n2 )).
78
Correttezza
Teorema 13.1. Sia G = (V, E, w) un grafo orientato e pesato con w(u, v) ≥ 0, ∀(u, v) ∈ E. Allora, al
termine dell’esecuzione dell’algoritmo, si ha:
1. d[u] = δ(s, u), ∀u ∈ V ;
2. Gπ è l’albero dei cammini minimi.
Proof. Dimostriamo solo il primo punto, in quanto il secondo segue da relax.
Proviamo, comunque, una proprietà più forte: ∀u ∈ V , nel momento in cui u è inserito in S, vale
d[u] = δ(s, u) (e per il lemma 13.4, non diminuirà ulteriormente).
Procediamo per assurdo: assumiamo che u sia il primo vertice tale che d[u] 6= δ(s, u) nel momento in cui
viene inserito in S. Dopo initSingleSource(G, s) si ha d[s] = 0; nel grafo non ci sono cicli negativi,
poiché i pesi sono tutti non negativi, dunque si ha δ(s, s) = 0: abbiamo dunque d[s] = δ(s, s), quindi il
vertice u non può essere s.
Visto che s viene estratto per primo, S 6= ∅ quando u viene inserito; può essere δ(s, u) = +∞? Dopo
initSingleSource, d[u] = +∞, quindi ancora prima di iniziare le iterazioni si ha d[u] = δ(s, u); nelle
iterazioni si eseguono delle chiamate a relax che, però, non modificano d[u]: segue, dunque, che u
dev’essere raggiungibile da s, ossia δ(s, u) < +∞. Sia p il cammino minimo da s a u; la situazione che si
ha quando viene estratto u da Q è:
79
S
Q=V −S
u
Sia (x, y) arco di p tale che x ∈ S e y ∈ Q. Si ha:
d[u] ≤ d[y], perché u estratto da Q che contiene anche y;
s
p2
p1
d[u] = min d[v]
v∈Q
x
y
d[x] = δ(s, x) perché u è il primo vertice per cui non vale
d[u] = δ(s, u) e x è inserito in S prima di u. Inoltre, quando è
stato inserito x, è stata chiamata relax(x, y, w); dopo questa
chiamata, d[y] = δ(s, y) per il lemma 13.5.
δ(s, y) ≤ δ(s, u) poiché i pesi sono non negativi.
δ(s, u) 6= d[u] per ipotesi e d[u] ≥ δ(s, u) per il lemma 13.4; segue che d[u] > δ(s, u).
Da tutte queste proprietà, segue:
d[u] ≤ d[y]
= δ(s, y)
≤ δ(s, u)
< d[u]
Si ottiene dunque un assurdo: il vertice u che cercavamo non esiste e la correttezza è dimostrata.
Esempio di esecuzione
Per ciascun nodo, è indicata la coppia (chiave, predecessore) associata ad ogni iterazione del ciclo; i nodi
marcati in nero sono quelli ancora presenti in Q, il nodo rosso è quello estratto nell’iterazione considerata
e quelli blu sono i nodi già estratti da Q ed esaminati. In ogni figura, sono evidenziati gli archi che
compongono il sottografo indotto da π.
(∞, NIL)
(∞, NIL)
E
D
6
(∞, NIL)
(∞, NIL)
(3, A)
(∞, NIL)
6
B
C
3 2
3
1
(0, NIL)
7
3
A
2
5
E
D
6
(5, A)
(∞, NIL)
(3, A)
(9, B)
6
B
C
3 2
1 3
(0, NIL)
7
3
A
2
5
E
D
6
(5, A)
(∞, NIL)
(8, E)
(3, A)
6
B
C
3 2
1 3
(0, NIL)
7
3
A
2
5
E
D
6
(5, A)
(11, E)
(8, E)
(3, A)
6
B
C
3 2
1 3
(0, NIL)
7
3
A
2
5
E
D
6
(5, A)
(10, C)
(8, E)
(3, A)
6
B
C
3 2
1 3
(0, NIL)
7
3
A
2
5
E
D
6
(5, A)
(10, C)
B
(0, NIL)
A
3
2
3
1
6
C
3
2
7
5
Figure 13.1: Simulazione dell’esecuzione dell’algoritmo di Dijkstra (nodo sorgente a)
13.3
Algoritmo di Bellman-Ford
L’algoritmo di Dijkstra presenta dei problemi nel caso in cui siano presenti dei cicli di peso negativo: per
risolverli, è stato proposto l’algoritmo di Bellman-Ford.
80
algoritmo bellmanFord (grafo pesato G, sorgente s) → booleano
1
2
3
4
5
6
7
8
initSingleSource (G , s )
for i = 1 to | V | - 1 do
for each (u , v ) ∈ E do
relax (u , v , w )
for each (u , v ) ∈ E do
if ( d [ v ] > d [ u ] + w (u , v ) ) then
return false
return true
L’algoritmo restituisce:
• false, se è stato trovato un ciclo negativo;
• true altrimenti.
Osservazione: l’algoritmo ritorna true se e solo se d[v] ≤ d[u] + w(u, v), ∀(u, v) ∈ E.
Correttezza
Teorema 13.2. Siano G = (V, E, w) un grafo orientato e pesato e s ∈ V il nodo sorgente:
1. se il grafo non contiene cicli negativi raggiungibili da s allora, alla fine di bellmanFord(G, s) si
ha:
(a) per ogni u ∈ V risulta d[u] = δ(s, u);
(b) Gπ è un albero di cammini minimi;
(c) l’algoritmo ritorna true;
2. se il grafo G contiene cicli negativi raggiungibili da s, l’algoritmo ritorna false.
Proof.
Punto 1a. Per ipotesi, G non ha cicli negativi raggiungibili da s. Sia u ∈ V ; dobbiamo provare che
vale d[u] = δ(s, u) al termine dell’esecuzione dell’algoritmo:
1. se u non è raggiungibile da s, si ha δ(s, u) = +∞. Dopo initSingleSource(G, s) vale d[u] =
+∞ = δ(s, u) e, poiché relax non modifica ulteriormente d[u] per il lemma 13.4, al termine vale
ancora d[u] = δ(s, u);
2. se u è raggiungibile da s, si ha δ(s, u) < +∞. Sia p = hx0 , ..., xk i cammino minimo da s a u, con
x0 = s e xk = u; p non ha cicli e, inoltre, il numero di nodi di p, ossia k + 1, è inferiore o uguale alla
cardinalità di V . Consideriamo il predecessore di u nel cammino e chiamiamolo v: se p è cammino
minimo e d[v] = δ(s, v), dopo relax(v, u, w) vale d[u] = δ(s, u).
Proprietà 13.1. Se esiste un cammino minimo da s a u con k archi, allora dopo il k-esimo ciclo
dell’algoritmo (righe 2-4) vale d[u] = δ(s, u) (invariante del ciclo).
Proof. Procediamo per induzione su k.
Caso base: abbiamo k = 0, dunque u = s; si ha d[s] = 0 per initSingleSource e δ(s, s) = 0 per
l’ipotesi dell’assenza di cicli negativi, dunque la proprietà è dimostrata per il caso base.
Ipotesi induttiva: supponiamo che la proprietà valga per i < k.
Passo induttivo: sia k > 0; consideriamo il cammino p = hx0 , ..., xk i, con x0 = s e xk = u. Per
ipotesi induttiva, alla k − 1-esima iterazione, d[xk−1 ] = δ(s, xk−1 ); all’iterazione k-esima, tra le
varie chiamate, viene eseguita relax(xk−1 , xk , w) e, per il lemma 13.5, si ha d[xk ] = δ(s, xk ),
ossia d[u] = δ(s, u).
Dunque, al termine del ciclo, tutti i cammini sono stati sistemati (se non ci sono cicli negativi) e
hanno al più n − 1 archi.
81
Punto 1b. Segue direttamente da relax.
Punto 1c. Dobbiamo dimostrare che d[v] ≤ d[u] + w(u, v), ∀(u, v) ∈ E. Per il lemma 13.2, si ha
δ(s, v) ≤ δ(s, u) + w(u, v); consideriamo (u, v) ∈ E:
d[v] = δ(s, v)
≤ δ(s, u) + w(u, v)
= d[u] + w(u, v)
L’ultima uguaglianza segue dal punto 1a.
Punto 2. Dal punto 1 sappiamo che, se non ci sono cicli negativi raggiungibili da s, allora l’algoritmo
ritorna true. Proviamo ora il viceversa, ossia che, se l’algoritmo ritorna true, non ci sono cicli negativi
raggiungibili da s.
Sia C = hx0 , ..., xk i un ciclo raggiungibile da s; poiché, per ipotesi, l’algoritmo ritorna true, si ha:
d[v] ≤ d[u] + w(u, v)
In particolare, considerando gli archi (xi , xi+1 ) con i = 0..k − 1:
d[xi+1 ] ≤ d[xi ] + w(xi , xi+1 )
k−1
X
d[xi+1 ] ≤
i=0
k−1
X
d[xi ] +
i=0
k−1
X
w(xi , xi+1 )
i=0
d[x1 ] + d[x2 ] + ... + d[xk ] ≤ d[x0 ] + d[x1 ] + ... + d[xk−1 ] +
k−1
X
w(xi , xi+1 )
i=0
d[xk ] ≤ d[x0 ] +
k−1
X
w(xi , xi+1 )
i=0
Poiché C è un ciclo, si ha x0 = xk , ossia d[x0 ] = d[xk ]; segue:
0≤
k−1
X
w(xi , xi+1 ) = w(C)
i=0
Abbiamo cos dimostrato che, se l’algoritmo ritorna true, non esistono cicli negativi raggiungibili da
s; segue dunque che, se ci sono cicli negativi raggiungibili da s, l’algoritmo restituisce false.
Complessità
1. O(n)
2-4. vengono eseguite m · n chiamate a relax, il cui costo è O(1); in totale si ha O(m · n)
5-7. il ciclo viene eseguito m volte ed ogni iterazione ha costo O(1); in totale si paga è O(m)
8. O(1)
Totale: O(n) + O(m · n) + O(m) + O(1) = O(m · n).
L’algoritmo risulta essere computazionalmente più costoso rispetto a quello di Dijkstra, ma può essere
applicato anche se gli archi hanno pesi negativi: la scelta di quale algoritmo utilizzare, dunque, dipende
da come è definita la funzione peso.
Esempio di esecuzione
L’ordine di analisi degli archi segue l’ordine alfabetico con cui sono identificati i vertici: prima tutti gli
archi che partono da a, poi quelli che partono da b, etc. Le figure si riferiscono a:
1. situazione dopo initSingleSource;
2. situazione dopo la prima passata di tutti gli archi;
3. situazione dopo la seconda passata di tutti gli archi (le successive non producono variazioni).
Chiaramente l’algoritmo restituisce true.
82
(∞, NIL)
B
(0, NIL)
A
3
2
3
1
6
(∞, NIL)
(3, A)
C
3
2
6
B
7
5
(0, NIL)
A
3
2
(8, E)
C
3
1
3
(3, A)
2
B
7
5
E
D
6
(∞, NIL)
(∞, NIL)
(0, NIL)
A
3
2
(8, E)
6
C
3
1
3
2
7
5
E
(5, A)
6
D
(11, C)
E
(5, A)
6
Figure 13.2: Esempio di esecuzione dell’algoritmo di Bellman - Ford (nodo sorgente a)
83
D
(10, C)
84
Chapter 14
Cammini minimi fra tutte le coppie
Consideriamo il problema dei cammini minimi fra tutte le coppie in un grafo G = (V, E, w) orientato,
pesato, dove possono essere presenti archi (ma non cicli) di peso negativo; assumiamo, inoltre, che gli n
vertici siano identificati da numeri interi da 1 a n.
14.1
Cammini minimi e moltiplicazione di matrici
Matrice di adiacenza pesata
Si tratta di una matrice n × n che, oltre a specificare se esiste l’arco (i, j), indica anche il peso di tale
arco.


se i = j
0
wi,j = w(i, j) se i 6= j ∧ (i, j) ∈ E


+∞
se i 6= j ∧ (i, j) ∈
/E
Matrice delle distanze
È una matrice n × n, aggiornata dall’algoritmo, dove di,j contiene la stima della distanza tra i e j; al
termine dell’esecuzione dell’algoritmo, vogliamo che di,j = δ(i, j), ∀i, j ∈ V .
Moltiplicazione di matrici
Siano i, j ∈ V ; definiamo:
Ci,j = {p = hx0 , ..., xq i : x0 = i ∧ xq = j ∧ (xl−1 , xl ) ∈ E, l = 1..q}
La distanza tra i e j può essere definita come segue:
δ(i, j) = min w(p)
p∈C(i,j)
Fissiamo m ≥ 1:
(m)
Ci,j = {p = hx0 , ..., xq i : x0 = i ∧ xq = j ∧ (xl−1 , xl ) ∈ E, l = 1..q ∧ q ≤ m}
(m)
di,j = min w(p)
(m)
p∈Ci,j
Sono valide le seguenti relazioni:
(1)
(2)
(m)
(m+1)
Ci,j ⊆ Ci,j ⊆ ... ⊆ Ci,j ⊆ Ci,j
Ci,j =
[
m∈N
85
(m)
Ci,j
⊆ ... ⊆ Ci,j
(1)
(2)
(m)
di,j ≥ di,j ≥ ... ≥ di,j ≥ ... ≥ δ(i, j)
Poiché non sono presenti, per ipotesi, cicli negativi, possiamo assumere che i cammini minimi siano
(n−1)
(n)
(n+1)
cammini semplici, dunque formati al più da n − 1 archi; ossia, si ha di,j
= di,j = di,j
= ... = δ(i, j).
(m)
(n−1)
L’idea è di progettare un algoritmo per il calcolo di di,j
in modo che, quando arriviamo a di,j
otteniamo proprio le distanze; cerchiamo una formula ricorsiva per il calcolo di
(m)
di,j ,
,
per induzione.
Caso base: si ha m = 1


{hii}
(1)
Ci,j = {hi, ji}


∅


0
(1)
di,j = w(i, j)


+∞
se i = j
se i =
6 j ∧ (i, j) ∈ E
se i =
6 j ∧ (i, j) ∈
/E
se i = j
se i =
6 j ∧ (i, j) ∈ E
se i =
6 j ∧ (i, j) ∈
/E
Ipotesi induttiva: supponiamo di essere riusciti a calcolare la matrice delle distanze D(m−1 ).
(m)
Passo induttivo: sia m > 1 e proviamo a calcolare D(m) usando l’ipotesi. Prendiamo i, j ∈ V : p ∈ Ci,j
(m−1)
può essere scomposto come p = p0 , j, con p0 ∈ Ci,k
scomponibili come segue:
(m)
; in generale, i cammini minimi in Ci,j sono
k1
(k1, j)
i
j
k2
(k2, j)
.
.
.
.
.
(kπ, j)
kπ
da cui:
(m)
di,j = min w(p) =
(m)
p∈Ci,j
(m−1)
Sia h ∈ V : se (h, j) ∈
/ E, allora di,h
min
k∈V $k,j)∈E
n
o
(m−1)
di,k
+ w(k, j)
+ w(h, j) = +∞.
La formula cercata è la seguente:
n
o
(m)
(m−1)
di,j = min di,k
+ w(k, j)
k∈V
Per implementare la formula ricorsiva, definiamo la seguente funzione:
funzione extendShortestPath (matrice delle distanze D(m−1) , matrice di adiacenza pesata W ) → matrice delle distanze D(m)
1
2
3
4
5
6
7
8
n = rows ( D )
Dm : matrice [ n ][ n ]
for i = 1 to n do
for j = 1 to n do
Dm [ i ][ j ] = + ∞
for k = 1 to n do
Dm [ i ][ j ] = min ( Dm [ i ][ j ] , D [ i ][ k ] + W [ k ][ j ])
return Dm
86
Complessità: Θ(n3 ).
La funzione definisce un nuovo tipo di prodotto tra matrici D⊗W = D0 , dove D0 è la matrice calcolata
da extendShortestPath:
algoritmo showAllPairsShortestPath (matrice di adiacenza pesata W ) → matrice delle distanze D(n−1)
1
2
3
4
5
n = rows ( W )
D (1) = W
for m = 2 to n - 1 do
D (m) = D (m - 1) ⊗ W
return D (n - 1)
Complessità: Θ(n4 ).
14.2
Algoritmo di Floyd-Warshall
Consideriamo un grafo che goda delle proprietà descritte ad inizio capitolo; vogliamo determinare δ(i, j)
per ogni coppia i, j. Costruiamo Ci,j in maniera incrementale; sia 0 ≤ k ≤ n e definiamo:
(k)
Pi,j = {p = hx0 , ..., xq i : x0 = i ∧ xq = j ∧ (xl−1 , xl ) ∈ E, l = 1..q ∧ xh ≤ k, h = 1..(q − 1)}
(n)
Si tratta dell’insieme dei cammini da i a j i cui nodi interni sono in 1..k; osserviamo che Pi,j = Ci,j
quindi, se definiamo:
(k)
di,j = min w(p)
(k)
p∈Pi,j
si ha:
(n)
di,j = δ(i, j)
(n)
L’obiettivo è calcolare di,j per ogni coppia di vertici in V ; procediamo per induzione su k.
(0)
Caso base: si ha k = 0; Pi,j è l’insieme dei cammini da i a j che non hanno nodi interni, dunque:
(0)


{hii}
= {hi, ji}


∅
(0)


se i = j
0
= w(i, j) se i =
6 j ∧ (i, j) ∈ E


+∞
se i =
6 j ∧ (i, j) ∈
/E
Pi,j
di,j
se i = j
se i =
6 j ∧ (i, j) ∈ E
se i =
6 j ∧ (i, j) ∈
/E
(k)
Se chiamiamo D(k) = (di,j ), si ha D(0) = W .
(k−1)
Ipotesi induttiva: supponiamo di conoscere Pi,j
.
(k)
Passo induttivo: sia k ≥ 1; usando l’ipotesi induttiva, cerchiamo di ottenere Pi,j . Innanzitutto, osn
o
(k−1)
(k)
(k)
(k)
(k−1)
(k)
serviamo che P
⊆ P e definiamo Pb = P −P
= p∈P
: p passa per il vertice k .
i,j
i,j
i,j
i,j
i,j
i,j
Si ha:
(k)
di,j = min w(p)
(k)
p∈Pi,j
!
= min
min
(k−1)
p∈Pi,j
87
w(p), min w(p)
(k)
b
p∈P
i,j
Il primo parametro della funzione minimo è:
(k−1)
min
(k−1)
w(p) = di,j
p∈Pi,j
Per quanto riguarda il secondo, dobbiamo trovare il peso del cammino più leggero da i a j che può
passare per i nodi 1..k e passa sicuramente per k; consideriamo cammini semplici, in quanto non
sono presenti cicli negativi: il cammino che cerchiamo può essere scomposto in due parti:
1. cammino da i a k che può passare per i nodi 1..k − 1;
2. cammino da k a j che può passare per i nodi 1..k − 1.
Chiaramente le due parti dovranno essere entrambe minime, affinché il cammino totale lo sia: il
(k−1)
(k−1)
loro costo lo conosciamo ed è, rispettivamente, di,k e dk,j . Dunque, il secondo parametro della
funzione minimo è:
(k−1)
(k−1)
min w(p) = di,k + dk,j
(k)
b
p∈P
1,j
(k)
La formula ricorsiva per il calcolo di di,j è dunque la seguente:
(
(k)
di,j
=
w(i, j)
(k−1) (k−1)
(k−1)
min(di,j , di,k + dk,j )
se k = 0
se k > 0
L’algoritmo segue direttamente dalla formula:
Complessità
Temporale: Θ(n3 )
Spaziale: Θ(n3 )
Costruzione di un cammino minimo
Un metodo per costruire i cammini minimi usando l’algoritmo di Floyd Warshall consiste nell’usare la
matrice dei pesi di cammino minimo prodotta e costruire, in tempo O(n3 ), la matrice dei predecessori Π.
Un secondo metodo prevede di calcolare Π ”‘in linea”’ con l’algoritmo, o meglio, si calcola una sequenza
(k)
di matrici Π(0) , Π(1) , ..., Π(n) , dove Π = Π(n) e πi,j è definito come il predecessore di j su un cammino
minimo dal vertice i avente tutti i vertici intermedi nell’insieme {1, ..., k}.
(k)
Si può definire induttivamente πi,j ; quando k = 0 si ha:
(
(0)
πi,j
=
NIL
i
se i = j o w(i, j) = ∞
se i =
6 j e w(i, j) < ∞
Per k > 1, se prendiamo il cammino da i a j passante per k, allora il predecessore di j è lo stesso vertice
che avevamo scelto come predecessore di j in un cammino da k con tutti i vertici intermedi nell’insieme
{1, ..., k − 1}; altrimenti, scegliamo lo stesso predecessore di j che avevamo scelto su un cammino minimo
da i con tutti i vertici nell’insieme {1, ..., k − 1}. Formalmente:
(k)
πi,j
(
(k−1)
πi,j
=
(k−1)
πk,j
(k−1)
(k−1)
(k−1)
se di,j
≤ di,k + dk,j
(k−1)
(k−1)
(k−1)
se di,j
> di,k + dk,j
Miglioramento
L’algoritmo che segue è una versione migliorata di Floyd Warshall ed ha complessità spaziale Θ(n2 ),
mentre la complessità temporale non cambia (Θ(n3 )): l’idea consiste nell’utilizzare sempre la stessa
matrice, invece di utilizzarne una diversa per ciascun k.
88
2
3
4
8
1
7
-4
1
2
5
3
6
-5
4
Esempio di esecuzione
Simuliamo l’esecuzione sul

0
∞

D(0) = 
∞
2
∞

0
∞

D(1) = 
∞
2
∞

0
∞

D(2) = 
∞
2
∞

0
∞

D(3) = 
∞
2
∞

0
3

D(4) = 
7
2
8

0
3

D(5) = 
7
2
8
seguente grafo:
3
8
0 ∞
4
0
∞ −5
∞ ∞
3
0
4
5
∞
8
∞
0
−5
∞
3
8
0 ∞
4
0
5 −5
∞ ∞
3
0
4
−1
∞
8
∞
0
−5
∞
3
0
4
−1
5
−1
−4
0
−5
1
1
0
4
−1
5
−3
−4
0
−5
1


NIL
∞ −4
NIL
1
7
 (0) 

∞ ∞
 Π = NIL
 4
0 ∞
NIL
6
0


NIL
∞ −4
NIL
1
7
 (1) 

∞ ∞
 Π = NIL
 4
0 −2
NIL
6
0


NIL
4 −4
NIL
1 7
 (2) 

5 11 
 Π = NIL

 4
0 −2
6 0
NIL


NIL
4 −4
NIL
1 7
 (3) 

5 11 
 Π = NIL
 4

0 −2
NIL
6 0


NIL
4 −4
 4
1 −1
 (4) 

5 3
Π = 4
 4

0 −2
6 0
4


2 −4
NIL
 4
1 −1
 (5) 

5 3
Π = 4

 4
0 −2
6 0
4
89

1
1
NIL
1
NIL NIL
2
2 

3
NIL NIL NIL

NIL
4
NIL NIL
NIL NIL
5
NIL

1
1
NIL
1
NIL NIL
2
2 

3
NIL NIL NIL

1
4
NIL
1 
NIL NIL
5
NIL

1
1
2
1
NIL NIL
2
2 

3
NIL
2
2 

1
4
NIL
1 
NIL NIL
5
NIL

1
1
2
1
NIL NIL
2
2 

3
NIL
2
2 

3
4
NIL
1 
NIL NIL
5
NIL

1
4
2
1
NIL
4
2
1 

3
NIL
2
1 

3
4
NIL
1 
3
4
5
NIL

3
4
5
1
NIL
4
2
1 

3
NIL
2
1 

3
4
NIL
1 
3
4
5
NIL
90
Chapter 15
Reti di flusso
15.1
Introduzione
Definizione 15.1. Una rete di flusso è un grafo orientato G = (V, E) con una funzione c : V × V → R+
detta funzione capacità.
• Assumiamo che c(u, v) > 0 se e solo se (u, v) ∈ E.
• In una rete di flusso, sono presenti due vertici particolari: la sorgente)e il pozzo.
• Per comodità, assumiamo che, per ogni v ∈ V , esista un cammino dalla sorgente al pozzo passante
per v.
Definizione 15.2. Sia G = (V, E) una rete di flusso e c : V × V → R+ funzione capacità; siano inoltre
s e t, rispettivamente, sorgente e pozzo: un flusso in G è una funzione f : V × V → R+ che soddisfa le
seguenti proprietà:
Vincolo di capacità: ∀u, v ∈ V, f (u, v) ≤ c(u, v).
Vincolo di antisimmetria: ∀u, v ∈ V, f (u, v) = −f (v, u).
P
Conservazione del flusso: ∀u ∈ V − {s, t} , vinV f (u, v) = 0.
Definizione 15.3. Sia f un flusso nella rete G con sorgente s: si definisce valore del flusso la quantità:
X
|f | =
f (s, v)
v∈V
Proprietà 15.1. Per ogni u ∈ V, f (u, u) = 0.
Proof. Per la proprietà antisimmetrica, si ha:
f (u, u) = −f (u, u) → 2 · f (u, u) = 0
da cui si ottiene il risultato cercato.
Proprietà 15.2. Se u, v ∈ V, (u, v) ∈
/ E ∧ (v, u) ∈
/ E, allora f (u, v) = f (v, u) = 0.
Proof. Per il vincolo di capacità si può scrivere:
c(u, v) = 0 → f (u, v) ≤ c(u, v) = 0
e, per l’antisimmetria:
f (u, v) = −f (v, u) → −f (u, v) ≤ 0 → f (u, v) ≥ 0
Affinché valgano entrambe le relazioni, si deve avere f (u, v) = 0. Con un ragionamento analogo, si
può provare che anche f (v, u) = 0, il che conclude la dimostrazione.
91
Definizione 15.4. Sia u ∈ V ; il flusso netto positivo entrante in u è definito come:
X
F N P E(u) =
f (v, u)
v∈V
f (v,u)>0
Il flusso netto positivo uscente da u è invece definito come:
X
F N P U (u) =
f (u, v)
v∈V
f (u,v)>0
Lemma 15.1. Per ogni u ∈ V − {s, t} , F N P E(u) = F N P U (u).
Proof. Per la conservazione del flusso, si ha:
X
X
0=
f (u, v) =
f (u, v) +
v∈V
=
v∈V
f (u,v)>0
X
v∈V
f (u,v)>0
f (u, v) =
v∈V
f (u,v)<0
X
f (u, v) −
X
X
f (u, v) +
v∈V
f (u,v)>0
X
f (u, v)
v∈V
f (v,u)>0
f (u, v) = F N P U (u) − F N P E(u)
v∈V
f (v,u)>0
Dunque, si ottiene:
0 = F N P U (u) − F N P E(u)
da cui segue la proprietà che si voleva dimostrare.
Sommatoria estesa
Siano X, Y, Z ⊆ V ; definiamo:
f (X, Y ) =
XX
f (u, v)
u∈X v∈Y
Per rendere più compatta la notazione, definiamo:
• f ({u} , {v}) = f (u, v);
• f ({u} , Y ) = f (u, Y ).
Da questo punto di vista, possiamo definire il valore del flusso come:
|f | =
X
f (s, v) = f (s, V )
v∈V
Proprietà 15.3. f (X, X) = 0.
Proof. Sia X = {x1 , x2 , ..., xn }: costruiamo una matrice M di dimensione n × n, dove Mi,j = f (xi , xj ).
Possiamo scrivere:
n X
n
X
f (X, X) =
Mi,j
i=1 j=1
Gli elementi della diagonale principale sono tutti 0, per la proprietà 15.1; inoltre, per l’antisimmetria,
si ha che Mi,j = −Mj,i , ossia Mi,j + Mj,i = 0. Da questi fatti, segue che f (X, X) = 0, come si voleva
dimostrare.
Proprietà 15.4. f (X, Y ) = −f (Y, X).
Proof.
f (X, Y ) =
XX
f (u, v) =
u∈X v∈Y
=−
XX
XX
−f (v, u) = −
u∈X v∈Y
f (v, u) = −f (Y, X)
v∈Y u∈X
92
XX
u∈X v∈Y
f (v, u)
Proprietà 15.5. Se X ∩ Y = ∅, si ha:
f (X ∪ Y, Z) = f (X, Z) + f (Y, Z)
altrimenti:
f (X ∪ Y, Z) = f (X, Z) + f (Y, Z) − f (X ∩ Y, Z)
Usando queste proprietà, possiamo scrivere:
0 = f (V, V ) = f ({s} ∪ V − {s} , V ) = f (s, V ) + f (V − {s} , V )
= f (s, V ) + f (V − {s, t} ∪ {t} , V ) = f (s, V ) + f (V − {s, t} , V ) +f (t, V )
|
{z
}
=0 per cons. flusso
da cui segue:
f (s, V ) + f (t, V ) = 0 → f (s, V ) = −f (t, V ) → f (s, V ) = f (V, t)
Il valore del flusso può quindi essere definito, in modo equivalente, come:
|f | = f (s, V ) = f (V, t)
Somme di flussi
Siano f1 e f2 due flussi in G; definiamo:
f1 + f2 :V × V → R
(u, v) → f1 (u, v) + f2 (u, v)
• Il vincolo di capacità cade (non è detto che (f1 + f2 )(u, v) ≤ c(u, v)).
• Il vincolo di antisimmetria permane:
(f1 + f2 )(u, v) = f1 (u, v) + f2 (u, v) = −f1 (v, u) − f2 (v, u)
= −(f1 (v, u) + f2 (v, u)) = −(f1 + f2 )(v, u)
• La conservazione del flusso permane; sia u ∈ V − {s, t}:
X
X
X
X
f2 (u, v) = 0
(f1 + f2 )(u, v) =
(f1 (u, v) + f2 (u, v)) =
f1 (u, v) +
v∈V
v∈V
v∈V
v∈V
|
{z
=0
}
|
{z
=0
}
Problema di sorgenti e pozzi multipli
Per risolvere il problema del flusso massimo da più sorgenti a più destinazioni, è sufficiente usare una
supersorgente virtuale, dalla quale partono archi di capacità infinita diretti alle sorgenti reali, e un
superpozzo virtuale, al quale arrivano archi di capacità infinita dai pozzi reali.
15.2
Problema del flusso massimo
Il problema che si vuole affrontare è quello del flusso massimo: data una rete di flusso, trovare un flusso
il cui valore corrisponda al valore massimo tra tutti i flussi possibili.
Siano G = (V, E) rete di flusso, c : V × V → R+ capacità, f : V × V → R flusso e s, t ∈ V .
Definizione 15.5. La capacità residua cf è una funzione definita come segue:
cf :V × V → R+
(u, v) → c(u, v) − f (u, v)
Poiché f è un flusso, si ha f (u, v) ≤ c(u, v), da cui c(u, v) − f (u, v) ≥ 0 per ogni coppia (u, v): dunque
cf è una capacità a tutti gli effetti.
93
Definizione 15.6. Una rete residua è un grafo Gf = (V, Ef ), con Ef = {(u, v) : cf (u, v) > 0}.
Osservazione: Gf con cf è una rete di flusso.
Proposizione 15.1. Siano G = (V, E) rete di flusso, f : V × V → R flusso in G, Gf rete residua e
f 0 : V × V → R flusso in Gf ; definendo:
f + f 0 :V × V → R
(u, v) → f (u, v) + f 0 (u, v)
valgono:
1. f + f 0 è un flusso in G;
2. |f + f 0 | = |f | + |f 0 |.
Proof.
Punto 1. Dimostriamo che valga il vincolo di capacità, in quanto le altre due proprietà sono valide
per la somma di due flussi qualsiasi; vogliamo provare che (f + f 0 )(u, v) ≤ c(u, v), ∀(u, v) ∈ V × V .
(f + f 0 )(u, v) = f (u, v) + f 0 (u, v) ≤ f (u, v) + cf (u, v) = f (u, v) + c(u, v) − f (u, v) = c(u, v)
| {z }
≤cf (u,v)
Punto 2.
|f + f 0 | =
X
(f + f 0 )(s, v) =
v∈V
X
(f (s, v) + f 0 (s, v)) =
v∈V
X
f (s, v) +
v∈V
X
f 0 (s, v) = |f | + |f 0 |
v∈V
Definizione 15.7. Definiamo la capacità di un cammino come la capacità minima tra tutte le capacità
degli archi che compongono il cammino.
Definizione 15.8. Siano G una rete di flusso e f flusso in G:
• un cammino aumentante è un cammino semplice tra la sorgente s e il pozzo t nella rete residua Gf ;
• la capacità residua minima di un cammino aumentante p è definita come:
cf (p) = min {cf (u, v) : (u, v) è arco di p} > 0
Definizione 15.9. Sia p un cammino aumentante; il
definita come:


cf (p)
fp = −cf (p)


0
flusso associato a p è una funzione fp : V × V → R
se (u, v) ∈ p
se (v, u) ∈ p
altrimenti
Lemma 15.2. Sia fp il flusso associato al cammino aumentante p; allora:
• fp è flusso in Gf ;
• |fp | = cf (p) > 0.
Proof.
Prima parte. Verifichiamo che valgono le tre proprietà caratterizzanti un flusso:
Vincolo di capacità: fp (u, v) ≤ cf (u, v).
• se (u, v) ∈ p, fp (u, v) = cf (p) ≤ cf (u, v);
• se (u, v) è tale che (v, u) ∈ p, allora fp (u, v) = −cf (p) < 0 ≤ cf (u, v);
94
• se (u, v) ∈
/ p ∧ (v, u) ∈
/ p, fp (u, v) = 0 ≤ cf (u, v).
Vincolo di antisimmetria: è valido per definizione.
Conservazione del flusso: sia u ∈ V − {s, t}:
• se
/ p allora, per ogni vertice v ∈ V , si ha (u, v) ∈
/ p da cui fp (u, v) = 0; dunque
Pu ∈
v∈V fp (u, v) = 0.
• se u ∈ p, consideriamo il vertice w precedente nel cammino e il vertice v successivo; si ha:
X
fp (u, v) = fp (u, w) + fp (u, v) = 0
| {z } | {z }
v∈V
−cf (p)
poiché
(w,u)∈p
cf (p)
poiché
(u,v)∈p
Seconda parte.
|fp | =
X
fp (s, v) = f (s, V ) = cf (p)
v∈V
Si ha che s 6= t, dunque fra i due esiste almeno un arco; non sono presenti archi entranti in s, poiché
è il nodo sorgente: il contributo, quindi, è dato da f (s, V ).
Algoritmo per il calcolo del flusso massimo
Un generico algoritmo per il calcolo del flusso massimo può essere descritto come segue:
• parti da un flusso banale (f (u, v) = 0, ∀(u, v) ∈ V × V );
• finché non hai un flusso massimo:
– considera la rete residua e cerca un cammino aumentante;
– sfrutta tale cammino per definire un flusso in Gf ;
– aggiungi il flusso trovato a quello di partenza.
15.3
Flusso massimo - taglio minimo
Un altro metodo per il calcolo del flusso massimo si riconduce al problema del taglio minimo.
Definizione 15.10. Siano G una rete di flusso, s sorgente e t pozzo:
• un taglio in G è una coppia (S, T ) con S ⊆ V e T = V − S tali che s ∈ S e t ∈ T ;
• la capacità associata al taglio è:
c(S, T ) =
XX
c(u, v)
u∈S v∈T
• il flusso netto associato al taglio è dato da:
f (S, T ) =
XX
f (u, v)
u∈S v∈T
Si tratta di un flusso ben definito, in quanto f (S, T ) ≤ c(S, T ).
Problema del taglio minimo: data una rete di flusso G, trovare il taglio con capacità minima (è il
duale del problema del flusso massimo: per trovare il flusso massimo occorre trovare la capacità minima
tra tutti i possibili tagli).
Lemma 15.3. Siano G = (V, E) rete di flusso, f flusso in G e (S, T ) taglio in G: allora |f | = f (S, T ).
95
Proof. Osserviamo innanzitutto che:
f (S, V ) = f (S, S ∪ T ) = f (S ∪ (V − S)) = f (S, S) +f (S, V − S) = f (S, V − S)
| {z }
=0
Usando questo fatto, possiamo procedere con la dimostrazione:
f (S, T ) = f (S, V − S) = f (S, V ) = f ({s} ∪ (S − {s}), V ) = f (s, V ) + f (S − {s} , V )
=0
X
= |f | +
zX }| {
f (u, v) = |f |
u∈S−{s} v∈V
|
{z
}
=0
L’ultimo passaggio segue dal principio di conservazione del flusso.
Corollario 15.1. Siano G = (V, E) rete di flusso, f flusso arbitrario in G e (S, T ) taglio arbitrario:
allora |f | ≤ c(S, T ).
Proof.
|f | = f (S, T ) ≤ c(S, T )
Il nostro obiettivo è calcolare f ∗ :
c(S, T)
| f* | = c(S*, T*)
|f|
Siano (S ∗ , T ∗ ) il taglio con capacità minima e f ∗ il flusso tale che |f ∗ | =
c(S ∗ , T ∗ ):
∀(S, T ), c(S ∗ , T ∗ ) ≤ c(S, T )
∀f, |f | ≤ |f ∗ |
Teorema 15.1 (Flusso massimo - taglio minimo). Siano G rete di flusso, s sorgente, t pozzo e f flusso
in G; le seguenti affermazioni sono equivalenti:
(a) f è massimo;
(b) la rete residua Gf non ammette cammini aumentanti;
(c) esiste un taglio (S, T ) in G tale che c(S, T ) = |f |.
Proof. Diamo una dimostrazione ciclica, ossia che a ⇒ b, b ⇒ c e c ⇒ a.
Prima parte: a ⇒ b. Assumiamo, per assurdo, che esista un cammino aumentante p in Gf e sia
cf (p) > 0 la sua capacità residua; possiamo definire fp flusso in Gf , con |fp | = cf (p). f + fp è un flusso
in G e si ha:
|f + fp | = |f | + |fp | > |f |
|{z}
>0
Abbiamo trovato un flusso in G con valore strettamente maggiore del flusso massimo, il che è chiaramente
un assurdo: dunque, non può esistere un cammino aumentante in Gf .
Seconda parte: b ⇒ c. Definiamo:
S = {v : esiste un cammino da s a v in Gf }
T = V − S (i vertici non raggiungibili da s in Gf )
Si ha che s ∈ S e t ∈
/ S per ipotesi, in quanto abbiamo supposto che non esista un cammino aumentante
in Gf . (S, V − S) è un taglio in G e, in Gf , abbiamo la seguente situazione:
96
V-S
S
t
s
u
v
Siano u ∈ S e v ∈ V − S: in Gf non è presente l’arco (u, v) poiché, se esistesse, v sarebbe raggiungibile
da s e dunque dovrebbe trovarsi in V . Per ogni u ∈ S e v ∈ V − S si ha:
cf (u, v) = 0
da cui, per come è definita cf :
c(u, v) − f (u, v) = 0 → c(u, v) = f (u, v)
Da questa relazione, possiamo provare quanto si vuole dimostrare:
X X
X X
C(S, V − S) =
c(u, v) =
f (u, v) = f (S, V − S) = |f |
u∈S v∈V −S
u∈S v∈V −S
Terza parte: c ⇒ a. Consideriamo un flusso f 0 in G:
c(S, T ) ≥ f 0 (S, T ) = |f 0 |
| {z }
=|f |
0
da cui si ottiene |f | ≥ |f |, come si voleva dimostrare.
15.4
Algoritmo di Ford-Fulkerson
Correttezza
Il teorema 15.1 ci dice che, se l’algoritmo termina, allora è corretto; con capacità razionali l’algoritmo si
ferma sicuramente, mentre le capacità irrazionali sono problematiche.
Complessità
1-3. Θ(m)
4. la ricerca di un cammino aumentante viene eseguita mediante una visita del grafo, quindi il costo
è O(m)
5-8. O(m)
Quante volte troviamo un cammino aumentante? Se f ∗ è il flusso massimo, partendo da zero ed
avendo capacità intere, possiamo aggiornare il flusso corrente |f ∗ | volte; dunque la complessità totale è
O(|f ∗ | · m) (complessità pseudopolinomiale).
Esempio di esecuzione
Simuliamo l’esecuzione dell’algoritmo sul seguente grafo:
Dal momento che l’algoritmo non specifica quale tra i tanti cammini aumentanti prendere, nella
simulazione ne saranno presi alcuni senza criteri di scelta particolari; ogni immagine rappresenta la
situazione al termine dell’esecuzione di ciascuna iterazione del ciclo while: a sinistra il grafo di partenza
con i valori di flusso aggiornati, a destra la corrispondente rete residua.
Cammino p1 = hs, v1 , v3 , v2 , v4 , ti , cf (p1 ) = 4
Cammino p2 = hs, v1 , v3 , ti , cf (p2 ) = 8
97
12
v1
v3
20
16
4
9
7
10
s
13
t
4
v2
14
v4
8
4/12
v1
v3
4/16
s
v1
12
0/20
0/4
0/7
0/13
t
4/14
5
s
10
4
7
4
4
v4
t
4
13
4/4
v2
20
4
4/9
0/10
v3
4
v2
v4
10
12/12
v1
v3
8/20
12/16
s
v1
0/4
0/7
0/13
t
4/14
12
5
s
10
4
7 8
4
13
4/4
v2
v3
12
4/9
0/10
12
4
4
4
v4
v2
v4
10
98
t
Cammino p3 = hs, v2 , v4 , v3 , ti , cf (p3 ) = 7
12/12
v1
v3
0/4
7/7
7/13
t
5
11/14
5
s
4/4
v2
v3
12
4/9
0/10
12
4
15/20
12/16
s
v1
10
4
7 15
7
6
4
11
v4
t
4
v2
v4
3
Cammino p4 = hs, v2 , v3 , ti , cf (p4 ) = 4
12/12
v1
v3
0/4
7/7
11/13
t
1
11/14
9
s
10
4
7 19
t
11
4/4
v2
v3
12
0/9
0/10
12
4
19/20
12/16
s
v1
2
v4
4
11
v2
v4
3
Nella rete residua non esistono più cammini aumentanti; infatti, l’arco (v4 , t) è saturo e, anche se
l’arco (v3 , t) ha capacità residua 1, non è saturabile poiché il vertice v3 non è più raggiungibile: il flusso
massimo, dunque, è quello illustrato nell’ultima figura.
Problemi
L’algoritmo, anche con grafi semplici, potrebbe avere costo di esecuzione molto elevato; consideriamo
quello in figura:
v1
100000
s
100000
1
t
100000
100000
v2
Supponiamo che l’algoritmo faccia ripetutamente le seguenti scelte:
• cammino hs, v1 , v2 , ti di capacità 1;
• cammino hs, v2 , v1 , ti di capacità 1.
evidente che si tratta di scelte infelici, che aumentano enormemente il tempo di esecuzione, ma è altrettanto vero che l’algoritmo non vincola sulla scelta del cammino aumentante: un’ottimizzazione, molto
semplice da realizzare, sarà descritta nel prossimo paragrafo.
15.4.1
Ottimizzazione di Edmond-Karp
L’ottimizzazione di Edmond-Karp consiste nello scegliere, ad ogni iterazione, il cammino aumentante di
lunghezza minima tra tutti quelli possibili.
99
Definizione 15.11. Siano dati una rete di flusso G con sorgente s e pozzo t e un flusso f in G; considerato
un nodo u, definiamo df (u) come la distanza (espressa come numero di archi) tra la sorgente s e il nodo
u nella rete residua Gf .
Lemma 15.4. Siano G = (V, E) rete di flusso, s sorgente e t pozzo; siano inoltre f flusso in G, Gf la
corrispondente rete residua, p cammino aumentante in Gf con il minimo numero di archi, g = f + fp
flusso dato dall’algoritmo di Ford-Fulkerson con ottimizzazione di Edmond-Karp e Gg la corrispondente
rete residua: allora, per ogni u ∈ V − {s, t}, si ha dg(u) ≤ df (u).
Proof. Procediamo per assurdo. Supponiamo che esista z ∈ V − {s, t} tale che dg(z) < df (z); allora,
definendo:
W = {z ∈ V − {s, t} : dg(z) < df (z)}
si ha W 6= ∅. Sia v ∈ W tale che dg(v) è minima tra tutti gli elementi di W ; valgono le seguenti proprietà:
1. dg(v) < df (v) poiché v ∈ W ;
2. se u ∈ W , allora dg(v) ≤ dg(u), per quanto detto sopra; riscrivendo, se dg(v) > dg(u), allora
u∈
/ W , quindi dg(u) ≥ df (u) (abbiamo sfruttato il fatto che A ⇒ B è logicamente equivalente a
¬B ⇒ ¬A).
Il fatto che dg(v) < df (v) ci dice che dg(v) è un valore finito (in quanto df (v) può essere infinito), ovvero
v è raggiungibile da s in Gg . Definiamo q cammino minimo da s a u in Gg con il minor numero di archi;
sia u l’ultimo vertice di q prima del vertice v (u esiste poiché v 6= s): poiché q è cammino minimo, allora
anche il sottocammino da s a u è minimo. Quindi:
dg(v) = dg(u) + 1 → dg(u) < dg(v)
Per il punto 2:
dg(u) ≥ df (u)
Riassumendo, abbiamo u, v ∈ V con v 6= s tali che:
(a) dg(u) ≥ df (u);
(b) dg(v) = dg(u) + 1.
Sostanzialmente, abbiamo che u si sta allontanando dalla sorgente e vogliamo provare, per assurdo, che
v si sta avvicinando!
Utilizzando le proprietà a e b proviamo che cf (u, v) < 0 per ottenere un assurdo (in quanto le capacità
residue sono positive per definizione); per fare ciò, dimostriamo che non è possibile che cf (u, v) sia
maggiore oppure uguale a zero.
Caso 1. cf (u, v) > 0. Se cf (u, v) > 0, allora nella rete residua Gf esiste l’arco (u, v) e, in particolare,
si ha questa situazione:
u
cammino minimo
da s a u
Per la disuguaglianza triangolare, si ottiene:
s
df (v) ≤ df (u) + 1 ≤per a dg(u) + 1 =per b dg(v)
cammino minimo
da s a v
v
Dunque si ha df (v) ≤ dg(v); ma poiché v ∈ W , otteniamo un assurdo: dunque non può essere
cf (u, v) > 0.
Caso 2. cf (u, v) = 0. Se cf (u, v) = 0, allora (u, v) ∈
/ Ef , ossia (u, v) non è arco di Gf ; ricordiamo,
inoltre, che (u, v) ∈ Eg , quindi la situazione può essere schematizzata come segue:
100
u
Gg
u
s
Gf
s
v
v
cammino q
L’arco è apparso da Gf a Gg ; l’unico modo per apparire è che ci sia stato un decremento di flusso
lungo (u, v), ossia un incremento di flusso lungo (v, u). (v, u) è un arco del cammino aumentante e, per
come è fatto l’algoritmo, visto che il cammino aumentante è minimo, l’arco (v, u) si trova su un cammino
minimo da s a u.
u
Gf
s
Segue che:
t
df (u) = df (v) + 1
→ df (v) = df (u) − 1
cammino minimo
da s a v
v
df (v) = df (u) − 1 ≤per a dg(u) − 1 =per b dg(v) − 2
Da questa relazione otteniamo df (v) < dg(v); ma poiché v ∈ W , otteniamo un assurdo: dunque non può
essere cf (u, v) = 0.
Abbiamo dimostrato che non possiamo avere cf (u, v) > 0 e cf (u, v) = 0, dunque si ha necessariamente
cf (u, v) < 0 e abbiamo trovato l’assurdo; quindi W = ∅ e la dimostrazione del lemma è conclusa.
Corollario 15.2. Se g è calcolato dopo f da Ford-Fulkerson con ottimizzazione di Edmond-Karp, allora
dg(u) ≥ df (u), ∀u ∈ V − {s, t}.
Teorema 15.2. Il numero totale di cicli dell’algoritmo di Ford-Fulkerson con ottimizzazione di EdmondKarp è dell’ordine di O(m · n).
Proof. La dimostrazione si basa sul concetto di arco critico.
Definizione 15.12. Un arco critico è un arco (u, v) su un cammino aumentante p dove cf (u, v) = cf (p),
ovvero cf (u, v) è minima su p.
Ad ogni iterazione ci sarà almeno un arco critico, quello che viene saturato. Quante volte un arco
può diventare critico? Consideriamo la rete residua Gf e prendiamo il cammino aumentante (con il
numero minimo di archi); sia (u, v) arco critico: in Gf si ha df (v) = df (u) + 1, mentre in Gg l’arco (u, v)
scompare. Affinché (u, v) sia nuovamente critico, deve ricomparire in una rete residua successiva, ovvero
l’arco (v, u) deve far parte di un cammino aumentante; immediatamente prima della riapparizione di
(u, v), la situazione è la seguente:
u
Gh
s
t
v
dh(u) = dh(v) + 1
101
Dal corollario, si ottiene dh(v) ≥ df (v), da cui dh(v) + 1 ≥ df (v) + 1; segue che:
dh(u) = dh(v) + 1 ≥ df (v) + 1 = df (u) + 2
da cui:
dh(u) ≥ df (u) + 2
Affinché un arco sia raggiungibile, la distanza dev’essere al più n, dunque un arco può diventare critico
al più n/2 volte: il numero di cammini aumentanti, quindi, è al massimo m · n/2.
Usando il teorema e considerando che ogni ciclo costa O(m), si ottiene che la complessità dell’algoritmo
di Ford-Fulkerson con ottimizzazione di Edmond-Karp è O(m2 · n).
15.5
Abbinamento massimo nei grafi bipartiti
Definizione 15.13. Un grafo non orientato G = (V, E) si dice bipartito se:
• V = L ∪ R, con L ∩ R = ∅;
• E ⊆ L × R.
Definizione 15.14. Dato G = (V, E) grafo bipartito, un abbinamento in G è un insieme M di archi
(M ⊆ E) tale che, per ogni (u, v), (w, z) ∈ M , si ha u 6= w se e solo se v 6= z.
Definizione 15.15. Un abbinamento M è massimo se, per ogni abbinamento M 0 , si ha |M | ≥ |M 0 |.
Assumiamo che ogni vertice in G ne abbia almeno uno adiacente: possiamo ridurre il problema
dell’abbinamento massimo al problema del flusso massimo aggiungendo supersorgente e superpozzo, dove:
• la supersorgente è collegata ai nodi di L con archi di capacità 1;
• i vertici di R sono collegati al superpozzo via archi di capacità 1.
Il flusso deve essere a valori interi, altrimenti si possono inviare valori frazionari e alcuni flussi costruibili
non rispettano la definizione di abbinamento.
Definizione 15.16. Dato un grafo bipartito G = (V, E) con L ∪ R = V , definiamo la rete di flusso
G0 = (V 0 , E 0 ) associata:
Vertici: V 0 = V ∪ {s, t};
Archi: E 0 = {(s, u) : u ∈ L} ∪ {(u, v) : u ∈ L ∧ v ∈ R ∧ (u, v) ∈ E} ∪ {(v, t) : v ∈ R};
Capacità:
(
1
c(x, y) =
0
se (x, y) ∈ E 0
altrimenti
Osservazioni
• E ⊆ E 0 → |E| ≤ |E 0 |;
• |E 0 | = |E| + |L| + |R|; poiché abbiamo assunto che ogni vertice abbia almeno un arco incidente, si
ha |E| ≥ |V | /2, da cui |V | ≤ 2 · |E|: dunque |E 0 | ≤ 3 · |E|.
Da questi due punti si ha che |E 0 | = Θ(|E|).
Definizione 15.17. Un flusso f in una rete di flusso G = (V, E) è a valori interi se f (u, v) è un valore
intero per ogni (u, v) ∈ V × V .
Lemma 15.5. Siano G un grafo bipartito e G0 la corrispondente rete di flusso:
(a) se M è abbinamento in G, allora esiste un flusso f in G0 tale che |M | = |f |;
(b) se f è un flusso a valori interi in G0 , allora esiste un abbinamento M in G tale che |f | = |M |.
102
Proof.
Punto a. Sia M abbinamento in G; definiamo f 0 : V × V → R come segue:
• se (u, v) ∈ M , poniamo:
– f (s, u) = f (u, v) = f (v, t) = 1;
– f (t, v) = f (v, u) = f (u, s) = −1;
• altrimenti f (u, v) = 0.
Valgono:
1. f è flusso in G0 ;
2. |f | = |M |.
Proviamo il seconda punto dal momento che, per quanto riguarda il primo, è facile verificare che su f
valgono le proprietà che caratterizzano un flusso.
Consideriamo il taglio ({s} ∪ L, R ∪ {t}) in G0 ; si ha:
• f ({s} ∪ L, R ∪ {t}) = |M | poiché gli archi che danno contributo unitario al flusso sono tutti e soli
gli archi di M ;
• f ({s} ∪ L, R ∪ {t}) = |f | per il teorema 15.1.
Punto b. Sia f flusso in G0 a valori interi; valgono le seguenti proprietà:
1. f (u, v) > 0 ⇒ f (u, v) = c(u, v) = 1;
2. se u ∈ L:
•
•
•
•
f (u, s) ∈ {0, −1};
f (u, L) = 0;
se v ∈ R, f (u, v) ∈ {0, 1};
f (u, t) = 0;
3. se v ∈ R:
•
•
•
•
f (v, s) = 0;
f (v, R) = 0;
se u ∈ L, f (v, u) ∈ {0, 1};
f (u, t) ∈ {0, 1};
4. f (s, t) = 0.
Definiamo:
M = {(u, v) : u ∈ L ∧ v ∈ R ∧ f (u, v) = c(u, v) = 1}
Dobbiamo verificare che:
1. M ⊆ E;
2. M è abbinamento;
3. |f | = |M |.
Punto 1. Per ogni (u, v) ∈ M , si ha c(u, v) = 1, quindi (u, v) ∈ E per definizione di G0 : segue quindi
che M ⊆ E.
Punto 2. Per verificare che M è abbinamento, dobbiamo mostrare che, se (u, v), (w, z) ∈ M , allora
u 6= w e v 6= z; procediamo per assurdo, assumendo che la condizione non sia verificata, ovvero che u = w
o v = z. Evitiamo di considerare il caso u = w e v = z, in quanto significa che abbiamo considerato due
volte lo stesso arco; rimangono due possibilità:
• u = w e v 6= z;
• u 6= w e v = z.
103
Consideriamo il primo punto; la situazione può essere schematizzata come segue:
1/1
v
1/1
z
?/1
s
f (u, V 0 ) =
X
v∈V 0
u
f (u, v) = f (u, s) + f (u, L) +f (u, R) + f (u, t) ≥ −1 + f (u, R)
| {z } | {z }
| {z }
≥1
=0
=0
= −1 + f (u, v) + f (u, z) + f (u, R − {v, z}) ≥ −1 + 1 + 1 = 1
{z
}
| {z } | {z } |
=1
≥0
=1
Otteniamo f (u, V 0 ) ≥ 1, contro il principio di conservazione del flusso; per quanto riguarda il secondo punto, si può procedere in maniera analoga. Abbiamo dunque trovato l’assurdo, quindi M è un
abbinamento.
Punto 3. Consideriamo il taglio (L ∪ {s} , R ∪ {t}):
|f | = f (L ∪ {s} , R ∪ {t}) = f (L, R) + f (L, t) + f (s, R) + f (s, t) = f (L, R)
| {z } | {z } | {z }
=0
=0
=0
X
X
X
X
X
=
f (u, v) +
f (u, v) =
f (u, v) +
f (u, v) +
u∈L
v∈R
u∈L
v∈R
f (u,v)>0
{z
|
=
X
u∈L
v∈R
f (u,v)<0
u∈L
v∈R
f (u,v)=0
=0
}
|
{z
=0
1
u∈L
v∈R
f (u,v)=1
}
1 = |M |
(u,v)∈M
Corollario 15.3. Un abbinamento massimo in un grafo bipartito G corrisponde a un flusso massimo a
valori interi nella rete di flusso associata G0 e viceversa.
Possiamo dunque risolvere il problema dell’abbinamento massimo usando un algoritmo di flusso massimo, a patto che quest’ultimo produca flussi a valori interi.
Teorema 15.3 (Teorema di integralità). Se la funzione di capacità assume solo valori interi e f è il
flusso massimo calcolato dall’algoritmo di Ford-Fulkerson, allora:
• |f | è intero;
• f (u, v) è intero per ogni (u, v) ∈ E.
La dimostrazione può essere fatta per induzione sul numero di interazioni: si parte con f = 0 e si
aggiorna con la capacità minima sul cammino aumentante, dove tale capacità è intera.
Complessità
Consideriamo la rete di flusso G0 = (V 0 , E 0 ) associata al grafo bipartito G = (V, E): come detto precedentemente, si ha V 0 = L ∪ R ∪ {s, t} e |E 0 | = Θ(|E|). Sappiamo che:
• ogni iterazione del ciclo costa O(|E 0 |) = O(|E|);
• il numero di iterazioni eseguite è min(|L| , |R|): infatti, ad ogni ciclo il flusso è incrementato almeno
di un’unità fino a raggiungere il valore massimo (e il numero massimo di archi dell’abbinamento è
al più min(|L| , |R|) ≤ |V |).
La complessità è quindi O(|V | · |E|).
104
15.6
Caso di studio: segmentazione di immagini
Data un’immagine, si vuole suddividere i pixel che la compongono in due gruppi: quelli che costituiscono
il soggetto in primo piano e quelli che fanno parte dello sfondo. Consideriamo l’immagine da analizzare
come un grafo non orientato dove:
• V è l’insieme dei pixel che compongono l’immagine;
• E è l’insieme delle coppie di pixel “vicini”.
Possiamo tenere la relazione di vicinanza vaga, in modo da risolvere il problema nella sua forma più
generale; solitamente si ha:
Pixel: sono una rete di punti sul piano;
Vicinanza: i vicini di un punto sono i punti contigui nella rete.
Per ogni pixel u, inoltre, siano date:
• a(u): probabilità che u stia in primo piano;
• b(u): probabilità che u stia nello sfondo.
Non è necessario che a(u) e b(u) siano complementari; assumiamo inoltre che le probabilità siano valori
arbitrari non negativi e siano dati come parte del problema. In condizioni di isolamento, diremmo che:
• se a(u) > b(u), allora u è in primo piano;
• se b(u) > a(u), allora u è in secondo piano.
In generale, le decisioni che prendiamo sui vicini di u potrebbero influenzare la decisione su dove posizionare lo stesso u; l’idea è di rendere le etichette (sfondo / primo piano) uniformi, minimizzando
i passaggi tra primo piano e sfondo. Per fare ciò definiamo, per ogni coppia (u, v) ∈ E, la quantità
p(u, v) ≥ 0, detta separation penalty, che si deve pagare per piazzare uno tra u e v in primo piano e l’altro
sullo sfondo.
Segmentation problem: trovare una partizione dell’insieme dei pixel V negli insiemi A e B dove:
• A contiene i pixel in primo piano;
• B contiene i pixel sullo sfondo;
in modo da massimizzare la quantità:
X
X
q(A, B) =
a(u) +
b(v) −
u∈A
|
v∈B
{z
p(u, v)
(u,v)∈E
(u∈A∧v∈B)
∨(u∈B∧v∈A)
}
aumenta per
grandi probabilità
X
|
{z
}
penalizza le coppie in cui
un elemento è in primo piano
e l’altro sullo sfondo
Possiamo cercare di ricavare la partizione (A, B) riducendoci a un problema di taglio minimo; ci sono
però alcune differenze:
1. la funzione obiettivo dev’essere massimizzata e non minimizzata;
2. il grafo non ha né sorgente né pozzo;
3. il grafo non è orientato.
Punto 1. Definiamo Q come segue:
X
X
X
Q=
(a(u) + b(u)) =
(a(u) + b(u)) +
(a(u) + b(u))
u∈V
=
X
u∈A
u∈A
a(u) +
X
u∈A
b(u) +
u∈B
X
a(u) +
u∈B
105
X
u∈B
b(u)
da cui:
X
a(u) +
u∈A
X
b(u) = Q −
u∈B
X
X
b(u) −
u∈A
a(u)
u∈B
Riprendiamo la funzione quantità q:
q(A, B) = Q −
X
b(u) −
u∈A
X
X
a(u) −
u∈B
(u,v)∈E
(u∈A∧v∈B)
∨(u∈B∧v∈A)
Se definiamo:
q 0 (A, B) =
X
b(u) +
u∈A
X
X
a(u) +
u∈B
(u,v)∈E
(u∈A∧v∈B)
∨(u∈B∧v∈A)
possiamo scrivere:
q(A, B) = Q − q 0 (A, B)
Possiamo concludere che massimizzare q equivale a minimizzare q 0 .
Punto 2. Aggiungiamo una supersorgente s che rappresenta lo sfondo, un superpozzo t che rappresenta il primo piano e consideriamo V 0 = V ∪ {s, t}.
Punto 3. Trasformiamo ogni arco non orientato in una coppia di archi orientati.
Costruiamo la rete di flusso associata G0 = (V 0 , E 0 ), con:
• V 0 = V ∪ {s, t};
• E 0 = {(u, v) : (u, v) ∈ E} ∪ {(v, u) : (u, v) ∈ E} ∪ {(s, u) : u ∈ V } ∪ {(u, t) : u ∈ V };
• c : E 0 → R+ definita come segue:
– per ogni (u, v) ∈ E:
∗ c(u, v) = p(u, v)
∗ c(v, u) = p(u, v)
– per ogni u ∈ V :
∗ c(s, u) = b(u)
∗ c(u, t) = a(u)
Sia (B ∪ {s} , A ∪ {t}) un taglio in G0 :
c(B ∪ {s} , A ∪ {t}) = c(B, A) + c(B, t) + c(s, A) + c(s, t)
| {z }
=0
XX
X
X
=
c(u, v) +
c(u, t) +
c(s, u)
=
u∈B v∈A
u∈B
XX
X
p(u, v) +
u∈B v∈A
0
u∈B
u∈A
a(u) +
X
b(u)
u∈A
= q (A, B)
Minimizzare q(A, B) equivale a minimizzare q 0 (A, B) che, a sua volta, equivale a trovare un taglio
minimo (B ∪ {s} , A ∪ {t}) in G0 .
Teorema 15.4. La soluzione al problema della segmentazione in un grafo G può essere ottenuta con un
algoritmo di taglio minimo nella corrispondente rete di flusso G0 . Ovvero, un taglio minimo (B ∪ {s} , A ∪
{t}) in G corrisponde alla partizione (A, B) che massimizza il valore di q(A, B).
106
Chapter 16
String matching
16.1
Introduzione
Informalmente, il problema del pattern matching consiste nel trovare le occorrenze di una stringa di
caratteri all’interno di un testo; definiamo:
• testo: array[1..n];
• pattern: array[1..m], con m ≤ n.
Gli elementi di T e P appartengono ad un alfabeto Σ e sono spesso chiamati caratteri.
Definizione 16.1. Il pattern P occorre nel testo T con uno spostamento s se 0 ≤ s ≤ n − m e ∀i = 1..m
si ha T [s + i] = P [i].
Definizione 16.2. Se P occorre in T con spostamento s, allora s si dice spostamento valido.
Alla luce di queste definizioni, il problema dello string matching può essere visto nel seguente modo:
dato un pattern P e un testo T , trovare tutti gli spostamenti validi con cui P occorre in T .
Notazione e definizioni
• Σ: alfabeto finito;
• Σ∗ : insieme di tutte le stringhe di lunghezza finita formate con i caratteri in Σ;
• : stringa nulla ( ∈ Σ∗ ).
• data x ∈ Σ∗ , |x| indica la lunghezza della stringa, ovvero il numero di caratteri che la compongono
(|| = 0);
• date x, y ∈ Σ∗ , xy è la concatenazione delle stringhe x e y: vale |xy| = |x| + |y|;
• date w, x ∈ Σ∗ , w è prefisso di x (w < x) se x = wy, con y ∈ Σ∗ ; w è suffisso di x (w = x) se
x = yw, con y ∈ Σ∗ .
Osservazioni
Siano w, x ∈ Σ∗ con |w| = m e |x| = n:
• w < x se e solo se w occorre in x con spostamento 0;
• w = x se e solo se w occorre in x con spostamento n − m.
Se x = S[1..n], allora
• per ogni m, con 1 ≤ m ≤ n si ha:
S[1..m] < S[1..n]
S[m..n] = S[1..n]
107
• per ogni m, k con 1 ≤ m ≤ k ≤ n si ha:
S[1..m] < S[1..k]
S[k..n] = S[m..n]
Proprietà
• w < x ⇒ |w| ≤ |x|;
• w = x ⇒ |w| ≤ |x|;
• < x, ∀x ∈ Σ∗ ;
• = x, ∀x ∈ Σ∗ ;
• x < y ⇔ ax < ay;
• x = y ⇔ xa = ya;
• le relazioni < e = sono riflessive, antisimmetriche e transitive.
Lemma 16.1 (Sovrapposizione dei suffissi). Siano x, y ∈ Σ∗ tali che x = z e y = z; allora:
1. |x| < |y| ⇒ x = y;
2. |x| > |y| ⇒ y = x;
3. |x| = |y| ⇒ x = y;
Proof.
x
x
x
z
z
z
y
y
y
I grafici riassumono quanto si voleva dimostrare: da sinistra verso destra, primo, secondo e terzo
caso.
In maniera speculare si può enunciare e dimostrare il lemma della sovrapposizione dei prefissi; alla luce
di quanto è stato appena dimostrato, possiamo ridefinire nuovamente il problema dello string matching:
dato il pattern P [1..m] e il testo T [1..n], con m ≤ n, trovare tutti gli spostamenti s nell’intervallo
0 ≤ s ≤ n − m tali che P = Ts+m .
Assumiamo l’esistenza di una primitiva di confronto tra stringhe, che restituisce true se due stringhe
x, y ∈ Σ∗ sono uguali e false altrimenti; se il controllo viene fatto da sinistra verso destra e z è il più
lungo prefisso comune tra x e y, allora la complessità di tale funzione è Θ(|z| + 1).
16.2
Algoritmo ingenuo
L’idea sulla quale si basa l’algoritmo è di trovare gli spostamenti validi in un ciclo che verifica se P [1..m] =
T [s + 1..s + m] per ogni s tale che 0 ≤ s ≤ n − m.
algoritmo stringMatcherIngenuo (testo T, pattern P ) → void
1
2
3
4
5
n = length ( T )
m = length ( P )
for s = 0 to n - m do
if ( P [1.. m ] = T [ s + 1.. s + m ]) then
stampa ‘‘occorrenza del pattern con spostamento s’’
108
Complessità
Il ciclo viene fatto n − m + 1 volte e ad ogni iterazione pago t, dove t è la lunghezza del più lungo prefisso
comune tra P e la parte di testo considerata; nel caso pessimo si ha, ogni volta, t = m, da cui segue una
complessità di Θ((n − m) · m):
• se m << n, si ha Θ(n);
• se m ≈ n, si ha Θ(n);
• se m ≈ n/2, si ha Θ(n2 ).
16.3
String matching con automa a stati finiti
La caratteristica degli algoritmi di questo tipo è che esaminano ogni carattere del testo esattamente una
volta sola, da cui segue un tempo di matching pari a O(n); è però necessaria una fase di preelaborazione
in cui viene esaminato il pattern per costruire l’automa, che risulta essere costosa se Σ è grande.
16.3.1
Automi a stati finiti
Definizione 16.3. Un automa a stati finiti è una quintupla M = (Q, q0 , A, Σ, δ) con:
• Q insieme finito di stati;
• q0 ∈ Q stato iniziale;
• A ⊆ Q insieme finito degli stati accettanti;
• Σ alfabeto finito in input;
• δ : Q × Σ → Q funzione di transizione.
L’automa:
• parte dallo stato q0 ;
• legge i caratteri (in Σ) della stringa di input, uno alla volta;
• se si trova nello stato q e legge il carattere a, passa allo stato δ(q, a).
Se lo stato corrente sta in A, allora si dice che la macchina ha accettato la stringa letta finora; se lo stato
corrente non sta in A, allora l’input letto finora è rifiutato.
Un automa induce una funzione φ : Σ∗ → Q sulle stringhe di input detta funzione stato finale; sia
w ∈ Σ∗ : φ(w) è lo stato in cui finisce l’automa dopo aver letto w partendo dallo stato iniziale.
Definizione 16.4. Se φ(w) ∈ A, allora si dice che l’automa accetta la stringa w.
Esempio 16.1. Il seguente è un automa che accetta la stringa “abaa”, con alfabeto {a, b}.
b
a
a
a
a
b
a
b
b
b
Definizione 16.5. Fissato il pattern P [1..m], definiamo la funzione suffisso σ : Σ∗ → {0, 1, ..., m}
associata a P come:
σ(w) = max {k : Pk = w}
Esempio 16.2. Consideriamo il pattern P = “aab00 :
σ() = 0
σ(“ccaba00 ) = 1 σ(“cabaa00 ) = 2 σ(“cabaab00 ) = 3
Proprietà 16.1. Dati il pattern P [1..m] e la funzione σ, valgono le seguenti proprietà:
109
1. σ(w) = m se e solo se Pm = w;
2. se w < v, allora σ(w) ≤ σ(v);
3. σ(Pk ) = max {h : Ph < Pk } = k.
Definizione 16.6. Dato il pattern P [1..m], il corrispondente automa di string matching è definito come
segue:
• Q = {0, 1, ..., m};
• q0 = 0;
• A = {m};
• Σ è l’alfabeto su cui è costruito P ;
• δ(q, a) = σ(Pq a).
16.3.2
L’algoritmo
Sfruttiamo ora l’automa di string matching per eseguire la fase di matching: diamo la stringa T da leggere
all’automa e, ogni volta che passiamo sullo stato m, segnaliamo un occorrenza del pattern; in pratica,
calcoliamo φ(Ti ) per ogni prefisso Ti del testo T e, quando si ha φ(Ti ) = m, è stata trovata un’occorrenza.
algoritmo finiteAutomaMatcher (testo T, funzione δ, intero m) → void
1
2
3
4
5
6
n = length ( T )
q = 0
for i = 1 to n do
q = δ(q , T [ i ])
if ( q = m ) then
stampa ’’occorrenza del pattern con spostamento i - m’’
Complessità: O(n).
Invariante del ciclo: q = φ(Ti ).
Correttezza
Per provare la correttezza dell’algoritmo, dobbiamo dimostrare che φ(Ti ) = σ(Ti ).
Lemma 16.2 (Disuguaglianza della funzione suffisso). Per ogni w ∈ Σ∗ e per ogni a ∈ Σ, si ha σ(wa) ≤
σ(w) + 1.
Proof. Sia r = σ(wa):
• se r = 0, σ(wa) = 0 ≤ σ(w) +1
| {z }
≥0
• se r > 0, Pr = wa, dunque Pr = Pr−1 a con Pr−1 = w; dalla proprietà 2 segue che σ(Pr−1 ) ≤ σ(w)
e dalla proprietà 3 si ha σ(Pr−1 ) = r − 1. Unendo queste due relazioni, si ottiene r ≤ σ(w) + 1, da
cui σ(wa) ≤ σ(w) + 1.
Lemma 16.3 (Ricorsione della funzione suffisso). Dati w ∈ Σ∗ , a ∈ Σ e posto q = σ(w), si ha σ(wa) =
σ(Pq a).
Proof. Per provare l’uguaglianza, dimostriamo prima che σ(wa) ≥ σ(Pq a) e poi che σ(wa) ≤ σ(Pq a).
Prima parte: σ(wa) ≥ σ(Pq a).
q = σ(w) ⇒ Pq = w
⇒ Pq a = wa
⇒ σ(Pq a) ≤ σ(wa)
110
Seconda parte: σ(wa) ≤ σ(Pq a). Sia r = σ(wa):
1. per il lemma 16.2 si ha σ(wa) ≤ σ(w) + 1, ossia r ≤ q + 1;
2. per definizione, si ha Pr = wa;
3. Pq a = wa (prima parte della dimostrazione);
4. dal punto 1, poiché |Pr | = r e |Pq a| = |Pq | + 1 = q + 1, si ottiene |Pr | ≤ |Pq a|.
Unendo tutti questi fatti, si ottiene Pr = Pq a; dalla proprietà 2:
σ(Pr ) ≤ σ(Pq a)
| {z }
=σ(wa)
Poiché abbiamo provato σ(wa) ≥ σ(Pq a) e σ(wa) ≤ σ(Pq a), si ottiene σ(wa) = σ(Pq a).
Teorema 16.1 (Teorema fondamentale degli automi di string matching). Siano M l’automa di string
matching associato al pattern P [1..m] e φ la funzione stato finale indotta da M ; dato il testo T [1..n]
allora, per ogni i = 1..n, vale φ(Ti ) = σ(Ti ).
Proof. Procediamo per induzione su i.
Caso base: si ha i = 0, ossia T0 = : φ() = σ() = 0.
Ipotesi induttiva: supponiamo φ(Tk ) = σ(Tk ) per k < i.
Passo induttivo: sia i > 0; per ipotesi si ha φ(Ti−1 ) = σ(Ti−1 ). Poniamo q = φ(Ti−1 ) = σ(Ti−1 ):
φ(Ti ) = δ(φ(Ti−1 ), a)
per def. di φ, con a = T [i]
= δ(q, a)
= σ(Pq a)
= σ(Pσ(Ti−1 ) a)
= σ(Ti−1 a)
per il lemma 16.3
= σ(Ti )
16.3.3
per def. di δ nell’automa
per ipotesi e per def. di q
poiché a = T [i]
Costruzione dell’automa
Osservazione: viene scelto il minimo tra m + 1 e q + 2 per non sforare nell’array; inoltre, entrambi sono
incrementati di una unità perché usiamo il ciclo repeat until.
Complessità
• il ciclo repeat until ha complessità O(m2 ) (ciclo innestato “nascosto” dalla notazione);
• complessivamente, ogni iterazione del for each costa O(|Σ| · m2 );
• in totale, l’algoritmo ha complessità O(|Σ| · m3 ).
111
16.4
Algoritmo di Knuth - Morris - Pratt
L’idea che sta alla base dell’algoritmo è la seguente: sapendo che i caratteri P [1..q] coincidono con
T [s + 1..s + q], ci interessa sapere qual è lo spostamento minimo s0 > s tale che P [1..k] = T [s0 + 1..s0 + k]
con s0 + k = s + q; in particolare, l’intero k è il massimo valore tale per cui Pk = Pq , con k < q.
Definizione 16.7. Dato il pattern P [1..m], la funzione prefisso π : {1, 2, ..., m} → {0, 1, ..., m − 1} è
definita come:
π(q) = max {k : k < q ∧ Pk = Pq }
Esempio. Consideriamo il pattern P = “ababababca” e calcoliamone la funzione prefisso:
i
P [i]
π(i)
1
a
0
2
b
0
3
a
1
4
b
2
5
a
3
6
b
4
7
a
5
8
b
6
9
c
0
10
a
1
Calcoliamo la funzione prefisso usando la programmazione dinamica. Supponiamo di voler ricavare il
valore di π(q); sia k = π(q − 1):
• se P [q] = P [k + 1], allora π(q) = k + 1;
• altrimenti, sia k 0 = π(k): se P [q] = P [k 0 + 1], allora π(q) = k 0 + 1;
• altrimenti, sia k 00 = π(k 0 ) e continua con il ragionamento... se arrivi a 0, allora π(q) = 0.
L’algoritmo che calcola la funzione prefisso è il seguente:
algoritmo computePrefixFunction (pattern P ) → funzione prefisso
1
2
3
4
5
6
7
8
9
10
m = length ( P )
π[ i ] = 0
k = 0
for q = 2 to m do
while ( k > 0 && P [ k + 1] != P [ q ]) do
k = π[ k ]
if ( P [ k + 1] == P [ q ]) then
k = k + 1
π[ q ] = k
return π
Complessità (analisi “classica”)
all’iterazione q-esima, k può decrementare q − 1 volte; in totale, q iterazioni costano
PmNel caso pessimo,
2
q
=
O(m
).
q=2
Complessità (analisi amortizzata)
Il ciclo for viene ripetuto m − 1 volte in totale; per i = 0, 1, ..., m − 1, sia ki il valore di k all’uscita
dell’i-esima iterazione: definiamo come potenziale φ(i) = ki . Per verificare che φ è un buon potenziale,
dobbiamo provare c he φ(i) ≥ φ(0), ∀i = 0..m − 1: all’inizio del ciclo for si ha φ(0) = 0 e, all’interno
del ciclo, k viene decrementato fintanto che k > 0 (dunque non scende mai sotto lo zero) e può essere
incrementato al più di un’unità; da questo fatto, si ha φ(i) ≥ 0 = φ(0)∀i = 0..m − 1.
Ĉi + φ(i − 1) = Ci + φ(i) → Ĉi = Ci + φ(i) − φ(i − 1)
= Ci + ki − ki−1
Nel ciclo while, ki−1 viene decrementato si volte (con si < ki−1 ), poi nell’if viene incrementato una
volta:
Ci ≤ si + 1 ≤ ki−1 + 1
Inoltre si ha:
ki ≤ ki−1 − si + 1
da cui:
Ĉi ≤ si + 1 + ki−1 − si + 1 − ki−1 = 2
Scegliamo come costo amortizzato Ĉ = 2 = O(1): dunque m iterazioni ci vengono a costare O(m).
112
Correttezza
Prima di procedere con la dimostrazione, definiamo:
(
q
se i = 0
π (i) (q) =
(i−1)
π(π
(q)) se i > 0
e sia:
n
o
π ∗ (q) = π (1) (q), π (2) (q), ..., π (t) (q) = 0
Lemma 16.4 (Iterazione della funzione prefisso). Sia P [1..m] il pattern e π la funzione prefisso; preso
q = 1..m, π ∗ (q) = {k : k < q ∧ Pk = Pq }.
Proof. Suddividiamo la dimostrazione in due parti: prima proviamo che π ∗ (q) ⊆ {k : k < q ∧ Pk = Pq } e
poi che π ∗ (q) ⊇ {k : k < q ∧ Pk = Pq }.
Prima parte. Per dimostrare che π ∗ (q) ⊆ {k : k < q ∧ Pk = Pq } ci basta provare che Pπ(i) (q) = Pq
per ogni i = 1..t; procediamo per induzione su i.
Caso base: si ha i = 1:
Pπ(1) (q) = Pq → Pπ(q) = Pq
La relazione è verificata per definizione di π.
Ipotesi induttiva: supponiamo la proprietà valida per i − 1, ossia Pπ(i−1) (q) = Pq .
Passo induttivo: sia i > 1:
Pπ(i) (q) = Pπ(π(i−1) (q)) =per def.
di π
Pπ(i−1) (q) = Pq
Seconda parte. Proviamo ora che π ∗ (q) ⊇ {k : k < q ∧ Pk = Pq }. Procediamo per assurdo, assumendo che {k : k < q ∧ Pk = Pq } − π ∗ (q) 6= ∅; sia j il più grande valore presente in tale insieme: per
definizione, π(q) = max {k : k < q ∧ Pk = Pq }, quindi j 6= π(q) poiché π(q) ∈ π ∗ (q) e j < π(q). Sia j 0 il
più piccolo intero in π ∗ (q) tale che j < j 0 :
j
j'
π(q) q
j ∈ {k : k < q ∧ Pk = Pq } ⇒ Pj = Pq
j 0 ∈ π ∗ (q) ⊆ {k : k < q ∧ Pk = Pq } ⇒ Pj 0 = Pq
π*(q)
Pj
dunque si ha:
Pj = Pj 0
Inoltre:
Pq
π(j ) = max {k : k < q ∧ Pk = Pq } ⇒ j ≤ π(j )
0
0
Pj'
e, da questa relazione:
j ≤ π(j 0 ) < j
possiamo concludere che:
j = π(j 0 )
in quanto π(j 0 ) non può essere maggiore di j per definizione di j 0 e non si può avere π(j 0 ) < j; abbiamo
trovato l’assurdo, poiché π(j 0 ) = j ∈ π ∗ (q), contrariamente a quanto avevamo assunto.
Avendo dimostrato le due inclusioni, si ottiene l’uguaglianza desiderata.
Lemma 16.5. Siano P [1..m] pattern e π funzione prefisso: preso q = 1, 2, ..., m, se π(q) > 0, allora
π(q) − 1 ∈ π ∗ (q − 1).
Proof. Sia r = π(q) > 0: allora si ha r < q e Pr = Pq ; possiamo inoltre dire che r − 1 < q − 1 e
Pr−1 = Pq−1 . Per il lemma 16.4 si ha r − 1 ∈ π ∗ (q − 1) e, per definizione di r, si ottiene la relazione che
si voleva dimostrare.
113
Definizione 16.8. Sia q = 1, 2, ..., m; definiamo Eq−1 ⊆ π ∗ (q−1), ossia l’insieme su cui lavora l’algoritmo,
come segue:
Eq−1 = {k ∈ π ∗ (q − 1) : P [k + 1] = P [q]}
= {k : k < q − 1 ∧ Pk = Pq−1 ∧ P [k + 1] = P [q]}
= {k : k + 1 < q ∧ Pk+1 = Pq }
In altri termini, Eq−1 è composto dai valori k ∈ π ∗ (q − 1) per i quali è possibile estendere Pk a Pk+1
e ottenere un suffisso proprio di Pq .
Corollario 16.1. Siano P [1..m] pattern e π funzione prefisso: preso q = 2, ..., m vale
(
0
se Eq−1 = ∅
π(q) =
1 + max{k ∈ Eq−1 } altrimenti
Proof.
• Se Eq−1 = ∅, allora {k : k < q ∧ Pk+1 = Pq } = ∅ da cui si ottiene π(q) = 0.
• Se Eq−1 6= ∅:
– preso k ∈ Eq−1 si ha k + 1 < q e Pk+1 = Pq ; quindi, se k 0 = max{k ∈ Eq−1 si ottiene che
k 0 + 1 < q e Pk0 +1 = Pq ovvero k 0 ≤ π(q): utilizzando questa relazione, possiamo concludere
che max{k ∈ Eq−1 } + 1 ≤ π(q);
– sia r = π(q): abbiamo P [r] = P [q], che è equivalente a dire P [(r − 1) + 1] = P [q]; per il punto
precedente si ha π(q) > 0, ovvero r > 0 e, per il lemma 16.5, si ha r − 1 ∈ π ∗ (q − 1). Da questi
due fatti, si ricava che r − 1 ∈ Eq−1 , ossia che r − 1 ≤ max{k ∈ Eq−1 } e, per definizione di r,
possiamo scrivere π(q) ≤ max{k ∈ Eq−1 } + 1.
Da questi due punti si ottiene l’uguaglianza cercata.
Fase di matching
Lo pseudocodice dell’algoritmo che realizza la fase di matching è il seguente:
algoritmo kmpMatcher (testo T, pattern P )
1
2
3
4
5
6
7
8
9
10
11
12
n = length ( T )
m = length ( P )
π = c o m p u t ePr efix Funct ion ( P )
q = 0
for i = 1 to n do
while ( q > 0 && P [ q + 1] != T [ i ]) do
q = π[ q ]
if ( P [ q + 1] == T [ i ]) then
q = q + 1
if ( q == m ) then
stampa ‘‘ Occorrenza di P con spostamento i - m ’ ’
q = π[ q ]
Complessità (analisi “classica”)
Nel caso pessimo, il ciclo while (righe 6-7) costa O(m), dunque approssimiamo la complessità a
O(m · n).
Complessità (analisi amortizzata)
Consideriamo il ciclo for alle righe 5 - 11; esso viene eseguito, in totale, n + 1 volte. Definiamo come
potenziale:
φ(i) = valore di q all’uscita dell’i-esima iterazione.
Affinché φ sia un buon potenziale, si deve avere φ(i) ≥ φ(0), ∀i = 0..n. All’inizio del ciclo si ha φ(0) = 0
e, all’interno, q viene decrementato, ma non scende mai sotto lo zero, e poi può essere incrementato al
più di un’unità; da questo fatto, si ha φ(i) ≥ 0 = φ(0)∀i = 0..n, dunque φ è un buon potenziale.
114
All’iterazione i-esima, nel ciclo while, qi−1 viene decrementato esattamente si volte (con si ≤ qi−1 ),
poi alla riga 8 può essere incrementato una volta, dunque otteniamo:
qi ≤ qi−1 − si + 1
L’iterazione i-esima costa:
Ci ≤ si + 1
Calcoliamo il costo amortizzato:
Ĉi + φ(i − 1) = Ci + φ(i)
ossia:
Ĉi = Ci + φ(i) − φ(i − 1)
= Ci + qi − qi−1
≤ si + 1 + qi−1 − si + 1 − qi−1
=2
Il costo amortizzato dell’i-esima iterazione è Ĉ = 2 = O(1), dunque n iterazioni del ciclo for ci vengono
a costare O(n).
La complessità totale dell’algoritmo di Knuth-Morris-Pratt risulta essere O(n + m).
Relazione con gli automi a stati finiti
δ(q, T [i]) = 0
oppure
δ(q, T [i]) − 1 ∈ π ∗ (q)
Proof. Sia k = δ(q, T [i]); per definizione, σ(Pq T [i]) = k, da cui Pk = Pq T [i]. Se k = 0 ci troviamo nel
primo caso, altrimenti, se k > 0, si ha Pk−1 = Pq , dunque k − 1 ∈ π ∗ (q); dalla definizione di k si ottiene
la relazione cercata.
115
116
Chapter 17
Geometria computazionale
Definizione 17.1. Una combinazione convessa di due punti p1 = (x1 , y1 ) e p2 = (x2 , y2 ) è un qualsiasi
punto p3 = (x3 , y3 ) tale che
x3 = α · x1 + (1 − α) · x2
y3 = α · y1 + (1 − α) · y2
con α ∈ [0, 1].
Definizione 17.2. Dati p1 e p2 , il segmento di retta p1 p2 è l’insieme delle combinazioni convesse tra p1
e p2 . Se l’ordine di successione degli estremi è rilevante, si parla di segmento orientato p1~p2 . Se p1 è
l’origine del piano cartesiano, consideriamo il segmento orientato p1~p2 con il vettore p~2 .
Prodotto vettoriale (in R2 )
y
y2
p~1 × p~2 = det
x1 = |p~1 | · cos α
x1
y1
x2
y2
= x1 · y2 − x2 · y1
y1 = |p~1 | · sin α
x2 = |p~2 | · cos(α + θ) y2 = |p~2 | · sin(α + θ)
y1
θ
0
α
x2
x1
x
Sostituendo nella formula per il calcolo del determinante:
x1 x2
det
= |p~1 | · cos α · |p~2 | · sin(α + θ) − |p~2 | · cos(α + θ) · |p~1 | · sin α
y1 y2
= |p~1 | · |p~2 | · (sin(α + θ) · cos α − cos(α + θ) · sin α) = |p~1 | · |p~2 | · sin θ
Il valore calcolato non è altro che l’area del parallelogramma compreso tra p~1 e p~2 di base |p~1 | e altezza
|p~2 | · sin θ. Assumiamo che i due vettori siano non nulli; possono verificarsi tre casi:
• |p~1 | · |p~2 | · sin θ = 0: allora si ha sin θ = 0, ossia θ = 0, π, dunque p~1 e p~2 sono collineari (hanno la
stessa direzione);
117
• |p~1 | · |p~2 | · sin θ > 0: poiché i moduli sono non negativi per definizione, si ha sin θ > 0, ossia 0 < θ π:
per sovrapporre p~1 a p~2 con rotazione minima, si deve ruotare p~1 in senso antiorario (o p~2 in senso
orario);
• |p~1 | · |p~2 | · sin θ < 0: si ha sin θ < 0, ossia π < θ 2 · π: per sovrapporre p~1 a p~2 con rotazione minima,
si deve ruotare p~1 in senso orario (o p~2 in senso antiorario).
In generale, se p~1 e p~2 non hanno inizio nell’origine degli assi bens in P0 , indichiamo con
p1 − p0 = (x1 − x0 , y1 − y0 )
p2 − p0 = (x2 − x0 , y2 − y0 )
una tralsazione che porta l’origine in P0 ; ora è sufficiente calcolare il prodotto (p1 − p0 ) × (p2 − p0 ) e
procedere come sopra.
Il problema della svolta
Si vuole determinare se due segmenti consecutivi svoltano a destra oppure a sinistra.
p2
p1
p1
θ
p0
p0
sin θ > 0
θ
p2
sin θ < 0
Calcoliamo (p1 − p0 ) × (p2 − p0 ):
• se è positivo, la svolta è a sinistra;
• se è negativo, la svolta è a destra;
• se è nullo, vado dritto (o torno indietro).
17.1
Intersezione tra segmenti
Proprietà 17.1. Due segmenti si intersecano se e solo se una o entrambe le seguenti condizioni sono
verificate:
1. ogni segmento taglia la retta che contiene l’altro (questo succede se i due estremi non stanno nello
stesso semipiano dei due individuati dalla retta);
2. un estremo di un segmento giace sull’altro segmento.
Consideriamo i segmenti p1 p2 e p3 p4 :
• Per ogni punto pi , calcola di che è definita come la direzione di pi rispetto all’altro segmento:
d1 = (p1 − p3 ) × (p4 − p3 )
d2 = (p2 − p3 ) × (p4 − p3 )
d3 = (p3 − p1 ) × (p2 − p1 )
d4 = (p4 − p1 ) × (p2 − p1 )
• Se 0 ∈
/ {d1 , d2 , d3 , d4 }, d1 e d2 di segno opposto, d3 e d4 di segno opposto, allora i segmenti si
intersecano (caso “normale”).
118
• Se d1 = 0, x1 compreso tra x3 e x4 e y1 compreso tra y3 e y4 , allora i segmenti si intersecano (p1
giace sull’altro segmento).
• Procedi in maniera analoga per d2 , d3 e d4 .
• Altrimenti i segmenti non si intersecano.
Complessità: O(1)
17.1.1
Ricerca di un’intersezione tra diversi segmenti
Dati n segmenti nel piano, si vuole determinare se almeno due si intersecano, sotto le seguenti ipotesi
semplificative:
• nessun segmento è verticale;
• non esistono tre segmenti che si intersecano nello stesso punto.
Per risolvere il problema utilizziamo la tecnica di sweeping verticale: una retta immaginaria, detta retta
di sweeping, passa attraverso l’insieme degli oggetti geometrici da sinistra verso destra; in questo modo,
essi vengono ordinati dal passaggio della retta. L’algoritmo considera tutti gli estremi dei segmenti
ordinatamente da sinistra a destra e, ogni volta che ne trova uno, verifica se c’è stata un’intersezione.
Ordinare i segmenti
La retta verticale scrorre e interseca i segmenti; poiché non ce ne sono di verticali, ciascuno interseca la
retta di sweeping in un solo punto: possiamo ordinare i segmenti in funzione delle ordinate dei punti di
intersezione.
Definizione 17.3. Data una retta verticale r e due segmenti s1 e s2 :
• s1 e s2 sono confrontabili in r se r interseca sia s1 che s2 ;
• s1 è sopra s2 in r (s1 >r s2 ) se s1 e s2 sono confrontabili in r e l’intersezione di s1 con r sta sopra
l’intersezione di s2 con r.
Osservazione: >r definisce un ordine totale tra i segmenti che intersecano r.
Verificare l’ordine
Siano r retta verticale e p1 p2 , p3 p4 segmenti: se questi non si intersecano, il loro ordine relativo è lo stesso
per ogni retta di sweeping verticale. Poiché p1 p2 e p3 p4 non sono verticali, scegliamo p1 e p3 come gli
estremi più a sinistra; sia p1 , tra i due, quello più a sinistra:


> 0 se p1 p2 >r p3 p4
(p3 − p1 ) × (p2 − p1 ) < 0 se p3 p4 >r p1 p2


0
non si verifica
p3
p2
Se due segmenti si intersecano, quando la retta di sweeping supera il punto di intersezione, le posizioni dei segmenti
nell’ordine generato si invertono:
a >r b
p1
r
s
b >s a
p4
119
Poiché non ci possono essere tre segmenti che si intersecano in un punto, deve esistere qualche retta
verticale per la quale i due segmenti diventano consecutivi secondo l’ordine indetto.
Spostare la retta di sweeping
Gestiamo due insiemi di dati:
1. stato della retta di sweeping: relazioni tra gli oggetti intersecati dalla retta;
2. lista dei punti evento: insieme di coordinate x ordinate da sinistra a destra che definisce le posizioni
di arresto della retta di sweeping.
Lo stato della retta di sweeping può cambiare solo nei punti evento.
In questo caso fissiamo i punti evento in maniera statica:
• punti evento: ogni estremo di un segmento;
• ordine: ordiniamo gli estremi dei segmenti per coordinata x, da sinistra a destra. Se due estremi
sono sulla stessa verticale, mettiamo gli estremi sinistri prima di quelli destri: tra due estremi
entrambi sinistri (o entrambi destri) sulla stessa verticale, mettiamo prima quello con ordinata
minore;
• inseriamo un segmento nella retta di sweeping quando questa incontra il suo estremo sinistro;
• togliamo un segmento dalla retta di sweeping quando questa incontra il suo estremo destro.
Quando due estremi diventano consecutivi nell’ordine indotto dalla retta di sweeping, verifichiamo se si
intersecano.
Stato della retta di sweeping
Ordine totale T che supporta le seguenti operazioni:
insert(T, s) → void
Inserisce il segmento s in T .
delete(T, s) → void
Cancella il segmento s da T .
above(T, s) → segmento
Restituisce il segmento che si trova immediatamente sopra s.
below(T, s) → segmento
Restituisce il segmento che si trova immediatamente sotto s.
Implementando T con un albero AVL, tutte le operazioni hanno costo logaritmico al numero di nodi
dell’albero.
Confronto tra chiavi
Per eseguire il confronto tra le chiavi utilizziamo il prodotto vettoriale, avente complessità O(1).
Algoritmo
L’algoritmo che realizza la ricerca di un’intersezione all’interno di un’insieme di segmenti utilizzando la
retta di sweeping è il seguente:
120
algoritmo anySegmentsIntersect (insieme dei segmenti S ) → boolean
1
2
3
4
5
6
7
8
9
10
11
12
T = ∅
Ordina gli estremi dei segmenti in S da sinistra a destra ( risolvi i conflitti
come gi \ ‘{ a } detto )
foreach p ∈ lista ordinata degli estremi do
if ( p \ ‘{ e } estremo sinistro di un segmento s ) then
insert (T , s )
if ( above (T , s ) esiste ed interseca s || below (T , s ) esiste ed interseca s )
then
return true
if ( p \ ‘{ e } estremo sinistro di un segmento s ) then
if ( esiste t = above (T , s ) e u = below (T , s ) e t interseca u ) then
return true
delete (T , s )
return false
Correttezza
Teorema 17.1. La chiamata anySegmentsIntersect(S) restituisce true se e solo se esiste un’intersezione
tra i segmenti di S.
Proof. Procediamo dimostrando i due versi dell’implicazione.
Prima parte: ⇒
Ovvio: l’algoritmo restituisce true quando trova effettivamente l’intersezione.
Seconda parte: ⇐
Supponiamo ci sia un’intersezione e sia p il punto di intersezione più a sinistra (per risolvere eventuali
uguaglianze sulla coordinata x, scegliamo il punto con ordinata minore); siano inoltre a e b i segmenti
che si intersecano in p. Prima di p non ci sono intersezioni, quindi l’ordine dato da T è corretto; poiché
non ci sono tre segmenti che si incontrano in p, esiste una retta di sweeping k per la quale a e b diventano
consecutivi. La retta k sta a sinistra di p o passa per p: sia q l’evento corrispondente alla retta k. Ci
possono essere due possibilità per le azioni intraprese per il punto d’arresto q:
• se a o b viene inserito in T , l’altro segmento si trova effettivamente sopra o sotto a quello inserito
e l’intersezione viene rilevata al primo controllo;
• se a e b sono già presenti in T e viene cancellato un segmento tra essi nell’ordine totale, a e b
diventano consecutivi e l’intersezione viene trovata al secondo controllo.
Complessità
Sia |S| = n:
1. O(1)
2. O(n · log n)
3-11. ogni iterazione costa O(log n), per un totale di O(n · log n)
12. O(1)
Totale: O(n · log n)
17.2
Inviluppo convesso
Definizione 17.4. Un poligono è una regione del piano delimitata da una successione ordinata di punti
p1 , ..., pn detti vertici e segmenti p1 p2 , ..., pn p1 detti lati, con la proprietà che due lati consecutivi si
intersecano solo in corrispondenza del vertice comune.
121
Definizione 17.5. Un poligono è semplice se ogni coppia di lati non consecutivi ha intersezione vuota
(ossia non ci sono lati non consecutivi che si incrociano).
Teorema 17.2 (Teorema della curva di Jordan). Un poligono semplice partiziona il piano in due regioni
distinte:
• interna (limitata);
• esterna (illimitata).
Percorrendo la frontiera del poligono in senso antiorario, l’interno rimane sempre a sinistra.
Rappresentiamo un poligono con la sequenza dei suoi vertici p1 , ..., pn ordinata in senso antiorario.
Definizione 17.6. Un poligono semplice è convesso se la regione interna è convessa ovvero, presi due
dei suoi punti interni, il segmento che li congiunge è tutto contenuto nella regione interna.
Definizione 17.7. Dato un insieme di punti P, l’involucro convesso di P (indicato con CH(P)) è il più
piccolo poligono convesso C tale che ogni punto di P si trova sulla frontiera o all’interno di C.
Proprietà 17.2. I vertici di CH(P) sono punti di P.
Usiamo questa proprietà per costruire l’algoritmo; definiamo P = {p1 , p2 , ..., pn }.
17.2.1
Algoritmo “brute force”
Si basa sull’idea che la retta che passa per due punti consecutivi del perimetro dell’inviluppo convesso
lascia tutti gli altri punti del poligono dalla stessa parte:
• per ogni coppia di punti (pi , pj ), se pi pj × pi pk è sempre maggiore o minore di zero, per ogni k
diverso da i e j, allora pi pj ∈ CH(P);
• se ci sono più punti allineati, tengo solo quelli più distanti;
• ordino i punti per coordinata polare rispetto a un punto fissato.
Complessità
Ci sono n2 coppie possibili e, per ogni coppia, occorre eseguire n − 2 confronti: la complessità totale è
O(n3 ).
17.2.2
Algoritmo di Jarvis
5
1
4
Si tratta di un algoritmo di complessità O(n · h), dove n è la cardinalità di P e h il numero di punti che faranno parte dei vertici
dell’involucro convesso.
Si basa su rotazioni con fulcro variabile, a partire da un punto che
sicuramente farà parte dell’inviluppo (quello più a sinistra o più a
destra, più in alto o più in basso).
2
3
122
17.2.3
Algoritmo di Graham
L’algoritmo utilizza la tecnica di sweeping di rotazione: elabora i vertici nell’ordine degli angoli polari
che formano con un vertice di riferimento. Mantiene una pila S dei punti candidati a stare nell’involucro
convesso: ogni punto di P viene inserito una ed una sola volta in S e i vertici che non stanno in CH(P)
vengono rimossi da S; al termine si ha S = CH(P) e contiene i vertici in ordine antiorario (se letta dal
basso verso l’alto).
La pila S, oltre alle operazioni tradizionali, deve offrire una funzione nextToTop che restituisce
l’elemento della pila che sta “sotto” il top.
Come primo punto p0 scegliamo quello con ordinata minima (se ci sono più punti con la stessa
ordinata, prendiamo quello più a sinistra); allora p0 ∈ CH(P) in quanto non ha punti né sotto, né a
sinistra. Ordiniamo poi i punti in P − {p0 } in funzione degli angoli polari rispetto a p0 (confronto tra
vettori con il prodotto vettoriale): nel caso siano presenti più punti con lo stesso angolo polare, considero
solo quello più distante da p0 .
Siano p1 , p2 , ..., pm , con m ≤ n, i punti della sequenza ordinata ottenuta secondo l’ordine polare:
osserviamo che p1 e pm appartengono sicuramente all’inviluppo convesso di P.
L’algoritmo che calcola l’inviluppo convesso di un insieme di punti P, di cardinalità maggiore o uguale
a 3, è il seguente:
algoritmo grahamScan (insieme di punti P) → inviluppo convesso
1
2
3
4
5
6
7
8
9
10
Sia p0 il punto di P con ordinata minima ( se pi \ ‘{ u } punti hanno la stessa
ordinata minima , prendi quello pi \ ‘{ u } a sinistra )
Siano p1 , p2 , ..., pm i restanti punti di P ordinati come detto precedentemente
push (p0 , S )
push (p1 , S )
push (p2 , S )
for i = 3 to m do
while ( l ’ angolo formato da nextToTop ( S ) , top ( S ) e pi effettua una svolta non a
sinistra ) do
pop ( S )
push (pi , S )
return S
Correttezza
Teorema 17.3. Se grahamScan viene eseguito su un insieme di punti P, con |P| ≥ 3, alla fine S
contiene, dal basso verso l’alto, esattamente i vertici di CH(P) in ordine antiorario.
Proof. Dopo l’esecuzione della riga 2 abbiamo la sequenza p1 , ..., pm ; definiamo Pi = {p0 , p1 , ..., pi }:
P − Pm è l’insieme dei punti rimossi perché avevano la stessa angolazione polare di un punto di Pm e,
tra tutti, Pm contiene il più distante, dunque si ha CH(P) = CH(Pm ).
Dobbiamo provare che, al termine, S contiene tutti i vertici di CH(Pm ) disposti in senso antiorario.
Osserviamo inoltre che, per ogni i compresa tra 2 e m vale la seguente proprietà:
p0 , p1 , pi ∈ CH(Pi )
Per la dimostrazione, proviamo il seguente invariante del ciclo for:
All’inizio di ogni iterazione i, S contiene, dal basso verso l’alto, esattamente i vertici di
CH(Pi−1 ) in ordine antiorario.
Inizializzazione: si ha i = 3: Pi−1 = {p0 , p1 , p2 } = CH(P2 ) e appaiono in ordine antiorario dal
basso verso l’alto.
Mantenimento: sia i > 3; all’inizio dell’iterazione, il punto in cima alla pila è pi−1 . Sia pj il primo
punto che dà una svolta a sinistra: dopo il ciclo while, la pila contiene esattamente gli stessi elementi
che conteneva dopo l’iterazione j del ciclo for, ossia contiene i vertici di CH(Pj ) in ordine antiorario
dal basso verso l’alto. Dopo l’inserimento di pi , S contiene esattamente i vertici di CH(Pj ∪ {pi }) in
123
senso antiorario: dobbiamo provare che CH(Pj ∪ {pi }) = CH(Pi ). Sia pt un punto che è stato rimosso
durante l’iterazione i-esima del ciclo for: quando pt è stato rimosso, si trovava nella parte interna di
4
p0 pr pi , dunque CH(Pi − {pt }) = CH(Pi ). Ripetendo il ragionamento per tutti i vertici tolti, si ha
CH(Pi − {vertici tolti}) = CH(Pi ), dunque S = CH(Pj ∪ {pi }) = CH(Pi ).
Terminazione: al termine i = m + 1, dunque S = CH(Pm ) = CH(P) in ordine antiorario.
Complessità
Sia |P| = n:
1. O(1)
2. O(n · log n)
3-5. O(1)
6-9. per ogni iterazione si esegue una push, quindi in totale si fanno al massimo n pop, dunque la
complessità del ciclo è O(n)
10. O(1)
Totale: O(n · log n)
17.2.4
Algoritmo Quick Hull
• Si considerano i punti p1 e pn che hanno l’ascissa minima e massima: la retta che li congiunge
partiziona l’insieme dei punti in due insiemi che consideriamo uno alla volta.
• Sia S l’insieme dei punti che stanno sopra la retta che congiunge p1 e pn .
• Sia ph il punto di massima distanza dalla retta p1 pn (ph appartiene all’inviluppo convesso).
• Consideriamo le due rette orientate p1~ph e ph~pn :
– non ci sono punti a sinistra di entrambe;
4
– i punti a destra di entrambe sono interni a p1 ph pn e possono non essere considerati;
– i punti che giacciono a destra di una retta e a sinistra dell’altra costituiscono i due insiemi S1
e S2 che sono esterni rispetto l’attuale involucro.
• La procedura si attiva ricorsivamente prendendo S1 e S2 separatamente come S.
Complessità
Caso pessimo: O(n2 )
Caso medio: O(n · log n)
124
Chapter 18
Teoria della NP-completezza
18.1
Complessità di problemi decisionali
In generale, possiamo pensare ad un problema P come a una relazione P ⊆ I × S, dove I è l’insieme
delle istanze di ingresso e S quello delle soluzioni ; possiamo inoltre immaginare di avere un predicato
che, presa un’istanza x ∈ I ed una soluzione s ∈ S, restituisca 1 se (x, s) ∈ P (ovvero s è soluzione di P
sull’istanza x), 0 altrimenti. Usando questa terminologia, possiamo definire una prima classificazione dei
problemi:
Problemi di decisione: sono problemi che richiedono una risposta binaria, dunque S = {0, 1}; in
particolare, richiedono di verificare se l’istanza x soddisfa una certa proprietà.
Problemi di ricerca: data un’istanza x, questi problemi chiedono di restituire una soluzione s tale che
(x, s) ∈ P .
Problemi di ottimizzazione: data un’istanza x, si vuole trovare la migliore soluzione s∗ tra tutte le
possibili soluzioni s per cui (x, s) ∈ P ; la bontà della soluzione è valutata secondo un criterio
specificato dal problema stesso.
I principali concetti della teoria della complessità computazionale sono stati definiti in termini dei
problemi di decisione: si osservi, comunque, che è possibile esprimere problemi di altre categorie in
forma decisionale (e la difficoltà della forma decisionale è al più pari a quella del problema iniziale),
quindi caratterizzare la complessità di quest’ultimo permette di dare almeno una limitazione inferiore
alla complessità del primo.
necessario fissare un po’ di notazione; dopo aver stabilito una codifica binaria efficiente dell’input,
definiamo un problema decisionale P come una funzione, nel seguente modo:
∗
P : {0, 1} → {0, 1}
Inoltre, dato un problema decisionale P , definiamo il suo linguaggio come:
∗
L = x ∈ {0, 1} : P (x) = 1
18.1.1
Classi di complessità
Dati un problema di decisione P ed un algoritmo A, diciamo che A risolve P se A restituisce 1 su
un’istanza x se e solo se (x, 1) ∈ P . Inoltre, diciamo che A risolve P in tempo t(n) e spazio s(n) se il
tempo di esecuzione e l’occupazione di memoria di A sono, rispettivamente, t(n) e s(n).
Definizione 18.1. Data una qualunque funzione f (n), chiamiamo Time(f (n)) e Space(f (n)) gli insiemi
dei problemi decisionali che possono essere risolti, rispettivamente, in tempo e spazio O(f (n)).
Definizione 18.2. La classe P è la classe dei problemi risolvibili in tempo polinomiale nella dimensione
n dell’istanza di ingresso:
125
P=
∞
[
Time(nc )
c=0
La classe PSpace è la classe dei problemi risolvibili in spazio polinomiale nella dimensione n dell’istanza
di ingresso:
∞
[
PSpace =
Space(nc )
c=0
La classe ExpTime è la classe dei problemi risolvibili in tempo esponenziale nella dimensione n
dell’istanza di ingresso:
ExpTime =
∞
[
c
Time(2n )
c=0
Si hanno le seguenti relazioni tra le classi: P ⊆ PSpace ⊆ ExpTime; non è noto se le inclusioni siano
proprie o meno, in quanto sono quesiti di teoria della complessità ancora aperti.
Inoltre, la classe P è considerata come una classe soglia tra problemi trattabili e intrattabili.
18.2
La classe NP
Spesso, in caso di risposta affermativa ad un problema di decisione, si richiede anche di fornire un qualche
oggetto y, dipendente dall’istanza x e dal problema specifico, che possa certificare il fatto che x soddisfi
la proprietà richiesta: tale oggetto è noto come certificato. Un certificato è definito come una stringa
binaria y ed una funzione verificatrice
∗
∗
g : {0, 1} × {0, 1} → {0, 1}
tale che x ∈ L se e solo se esiste y tale per cui g(x, y) = 1.
Se g lavora in tempo polinomiale e y è di lunghezza polinomiale rispetto alla lunghezza di x, allora L è
verificabile in tempo polinomiale e per questo definiamo la classe di complessità NP: informalmente, essa
è la classe dei problemi decisionali che ammettono certificati verificabili in tempo polinomiale.
18.2.1
Non determinismo
Negli algoritmi visti finora, il passo successivo è sempre univocamente determinato dallo stato della
computazione: per tale motivo, sono detti deterministici ; un algoritmo non deterministico, invece, oltre
alle normali istruzioni, può eseguirne alcune del tipo indovina z ∈ {0, 1}, ovvero può ”‘indovinare”’ un
valore binario per z facendo proseguire la computazione in una ”‘giusta”’ direzione.
Definizione 18.3. Data una qualunque funzione f (n), chiamiamo NTime(f (n)) l’insieme dei problemi
decisionali che possono essere risolti da un algoritmo non deterministico in tempo O(f (n)). La classe NP
è la classe dei problemi risolvibili in tempo polinomiale non deterministico nella dimensione n dell’istanza
di ingresso:
NP =
∞
[
NTime(nc )
c=0
Un algoritmo non deterministico può essere suddiviso in due fasi:
Fase non deterministica di costruzione: sfruttando la funzione indovina, l’algoritmo costruisce un
certificato per l’istanza del problema.
Fase deterministica di verifica: l’algoritmo verifica che il certificato prodotto sia effettivamente una
soluzione del problema per l’istanza data.
126
18.2.2
La gerarchia
facile osservare che P ⊆ NP, poiché un algoritmo deterministico è un caso particolare di uno non
deterministico, in cui la funzione indovina non viene mai utilizzata. Inoltre, la fase deterministica di
verifica può essere condotta in tempo polinomiale solo se il certificato ha dimensione polinomiale, da cui
NP ⊆ PSpace; si congettura, ma nessuno è ancora riuscito a dimostrarlo, che entrambe le inclusioni
siano proprie, ovvero:
P ⊂ NP ⊂ PSpace ⊂ ExpTime
PSpace
ExpTime
NP
P
Si osservi che la gerarchia abbozzata, in realtà, è ben più complessa e vasta: infatti, esistono anche problemi indecidibili, ovvero non risolubili indipendentemente dalla quantità di tempo e memoria a
disposizione, come il problema della fermata.
18.3
Riducibilità polinomiale
Un linguaggio L1 è riducibile in tempo polinomiale a L2 se esiste una funzione calcolabile in tempo
polinomiale
∗
∗
r : {0, 1} → {0, 1}
∗
tale che, per ogni x ∈ {0, 1} , x ∈ L1 se e solo se r(x) ∈ L2 . r è detta funzione di riduzione e scriviamo
L1 ≤p L2 .
∗
Lemma 18.1. Siano L1 , L2 ⊂ {0, 1} linguaggi tali che L1 ≤p L2 : allora L2 ∈ P implica L1 ∈ P .
Proof. Visto che la funzione di riduzione lavora in tempo polinomiale, r(x) ha lunghezza polinomiale
rispetto alla lunghezza di x; ma un polinomio di un polinomio è ancora un polinomio, per cui r(x) ∈ L2
viene deciso in tempo polinomiale rispetto alla lunghezza di x.
18.4
Problemi NP-completi
Definizione 18.4. Un problema si dice NP-completo se:
• L ∈ NP;
• L0 ≤p L per ogni L0 ∈ NP.
Se vale solo la seconda condizione, L si dice essere NP-difficile; la classe dei problemi NP-completi si
chiama NPC.
Consideriamo i seguenti problemi:
127
Satisfiability: una formula booleana in forma congiuntiva consiste in una serie di clausole congiunte,
in cui ogni clausola è una serie di disgiunzioni di variabili, possibilmente negate; se ogni clausola
contiene meno di k variabili si dice k − CN F . Il problema è trovare un assegnamento booleano che
renda vera la formula: definiamo 3-SAT = {x : x è una formula in forma 3-CNF che ha almeno un
assegna mento che la rende vera}.
Clique: è un sottinsieme di vertici di un grafo per cui ognuno è connesso ad ogni altro del sottinsieme.
Ovviamente le coppie di vertici connesse for- mano delle clique, ma potrebbero esserci insiemi più
grandi a cui siamo interessati. Il linguaggio corrispondente è CLIQUE = {hG, ki : G è un grafo con
almeno una clique di dimensione k}.
Independent set: è un sottinsieme di vertici che non sono collegati l’uno con l’altro. Definiamo INDEPENDENT SET = {hG, ki : G è un grafo con almeno un independent set di dimensione k}.
Assumiamo che 3-SAT sia NP-completo e vediamo come si può ridurre il problema in tempo polinomiale a CLIQUE, e quest’ultimo ridurlo in tempo polinomiale a INDIPENDENT SET.
Prima parte: 3-SAT ≤p CLIQUE.
Presa una formula in forma 3-CNF con m clausole, costruiamo un grafo G che abbia un vertice per ogni
variabile (o variabile negata) all’interno di ogni clausola, quindi un grafo con 3 · m vertici. Colleghiamo
tra loro due vertici se e solo se appartengono a clausole diverse e non rappresentano la stessa variabile
negata, ossia non sono una coppia della forma (x, x). La funzione r(φ) restituisce una coppia hG, mi
lavora in tempo polinomiale, e ci chiediamo se φ ∈ 3-SAT se e solo se hG, mi ∈ CLIQUE.
Se G contenesse una clique di dimensione m, questa avrebbe un vertice in ogni clausola e ci consentirebbe
di trovare un’assegnazione che soddisfa φ senza incappare in una contraddizione. Viceversa se avessimo
un’assegnazione che soddisfa φ, potremmo scegliere da ogni clausola una variabile con valore ”‘vero”’, dal
momento che le m variabili scelte hanno tutte lo stesso valore, quindi dovrebbero essere collegate l’una
con l’altra nel grafo, formando una clique.
Seconda parte: CLIQUE ≤p INDEPENDENT SET.
Dato un grafo G, consideriamo il suo complementare G: è sufficiente osservare che un sottoinsieme di
vertici è una clique in G se e solo se è un independent set in G.
18.5
La classe coNP e la relazione tra P e NP
∗
Definizione 18.5. Il complemento di un linguaggio L ⊂ {0, 1} è l’insieme delle stringhe x ∈
/ L.
Definizione 18.6. La classe coNP consiste di linguaggi il cui complemento appartiene a NP:
coN P = L : L ∈ N P
Si ha P ⊆ coN P e P = coP , da cui P ⊆ N P ∩ coN P ; questo, a sua volta, implica P = N P ⇒ coP =
coN P ⇒ N P = coN P . Quindi N P 6= coN P ⇒ P 6= N P . Ci sono quattro possibilità:
• P = N P = coN P ;
• P ⊂ N P = coN P ;
• P = coN P ∩ N P, coN P ∩ N P 6= N P, N P 6= coN P ;
• P ⊂ coN P ∩ N P, coN P ∩ N P 6= N P, N P 6= coN P .
Nonostante la ricerca sia ancora aperta, generalmente si ritiene che P 6= N P , in quanto si ritiene
strano che cercare una risposta sia tanto facile quanto riconoscerla.
128
Chapter 19
Appendice
19.1
Serie aritmetica
Lemma 19.1. La somma dei primi n numeri consecutivi ha il seguente valore:
n
X
i=
i=1
n · (n + 1)
2
Proof. Definiamo
Sn =
n
X
i
i=1
Possiamo scrivere:
Sn = 1 + 2 + ... + n
Sn = n + (n − 1) + ... + 1
Sommando membro a membro, si ottiene 2·Sn = n·(n+1), da cui segue quanto si vuole dimostrare.
19.2
Serie geometrica
Lemma 19.2. La serie geometrica di ragione q, i cui addendi sono caratterizzati dall’avere una base
costante q 6= 1 ed un esponente variabile, ha il seguente valore:
k
X
qi =
i=0
q k+1 − 1
q−1
Proof. Sia
Sk (q) =
k
X
qi
i=0
Moltiplicando ambo i membri per q, si ha:
q · Sk (q) =
k+1
X
qi
i=1
Sottraendo Sk (q) ad entrambi i membri, si ottiene:
(q − 1) · Sk (q) =
k+1
X
qi −
i=1
da cui
129
k
X
i=0
q i = q k+1 − 1
Sk (q) =
q k+1 − 1
q−1
come volevamo dimostrare.
19.3
Calcolo di somme per integrazione
Sono valide le seguenti disuguaglianze:
• se f (x) è una funzione non decrescente:
Z
b
f (x)dx ≤
a−1
b
X
b+1
Z
f (i) ≤
f (x)dx
a
i=a
• se f (x) è una funzione non crescente:
Z
b+1
f (x)dx ≤
a
19.4
b
X
Z
b
f (i) ≤
i=a
f (x)dx
a−1
Goniometria e trigonometria
Ricordiamo alcune relazioni goniometriche utili per il capitolo di geometria computazionale:
sin(x + y) = sin x · cos y + sin y · cos x
sin(x − y) = sin x · cos y − sin y · cos x
cos(x + y) = cos x · cos y − sin x · sin y
cos(x − y) = cos x · cos y + sin x · sin y
e un paio di equazioni di trigonometria:
β
c
a = c · sin α = c · cos β
a
b = c · cos α = c · sin β
α
b
130
Bibliography
[1] C. Demetrescu, I. Finocchi, G.F. Italiano, Algoritmi e strutture dati 2/ed. McGraw-Hill, 2008.
[2] T.H. Cormen, C.E. Leiserson, R.L. Rivest, C. Stein, Introduzione agli algoritmi e strutture dati 3/ed,
McGraw-Hill, 2010.
131