Sei sulla pagina 1di 125

ALGORITMI E PROGRAMMAZIONE AVANZATA - G.MACCANTI@STUDENTS.UNINETTUNOUNIVERSITY.

NET

INGEGNERIA INFORMATICA

CORSO DI “ALGORITMI E PROGRAMMAZIONE AVANZATA”

a cura di Giacomo Maccanti

anno 2016

g.maccanti@students.uninettunouniversity.net

Giacomo Maccanti – g.maccanti@students.uninettunouniversity.net 2


ALGORITMI E PROGRAMMAZIONE AVANZATA - G.MACCANTI@STUDENTS.UNINETTUNOUNIVERSITY.NET

Sommario
ALGORITMI E PROGRAMMAZIONE AVANZATA – PARTE 1 - RICORSIONE E ALGORITMI RICORSIVI................ 13
ALGORITMO RICORSIVO .............................................................................................................................. 13
ALGORITMO ITERATTIVO............................................................................................................................. 13
RICORSIONE DIRETTA .................................................................................................................................. 13
RICORSIONE INDIRETTA ............................................................................................................................... 13
PERCHE’ SCRIVIAMO FUNZIONI RICORSIVE? ............................................................................................... 13
ANALISI DI UNA FUNZIONE RICORSIVA: ...................................................................................................... 13
PUNTATORI E INDIRIZZI ................................................................................................................................... 14
PUNTATORI.................................................................................................................................................. 14
INDIRIZZI ...................................................................................................................................................... 14
OPERAZIONI SUI PUNTATORI ...................................................................................................................... 14
PROGRAMMA DI ESEMPIO PUNTATORI E INDIRIZZI ................................................................................... 15
ALLOCAZIONE DINAMICA DELLA MEMORIA ................................................................................................... 16
ISTRUZIONE MALLOC() ................................................................................................................................ 16
ISTRUZIONE FREE() ...................................................................................................................................... 16
PROGRAMMA DI ESEMPIO: MALLOC .......................................................................................................... 16
TYPEDEF .......................................................................................................................................................... 17
PROGRAMMA DI ESEMPIO: TYPEDEF .......................................................................................................... 17
LISTE LINEARI ................................................................................................................................................... 18
IMPLEMENTAZIONE .................................................................................................................................... 18
STATO DELLA LISTA...................................................................................................................................... 18
PRIMITIVE DI GESTIONE DELLE LISTE ........................................................................................................... 19
Inizializzazione di una lista: codice c........................................................................................................ 19
Ricerca in una lista: definizione ............................................................................................................... 19
Ricerca in una lista: codice c .................................................................................................................... 19
Visita di una lista: codice c ...................................................................................................................... 19
Inserimento in una lista: definizione........................................................................................................ 20
Inserimento in testa in una lista: codice c ............................................................................................... 20
Elimina elemento di una lista .................................................................................................................. 22
Elimina elemento di una lista: codice c.................................................................................................... 22
PROGRAMMA GESTIONE PRIMITIVE LISTE: CODICE C ................................................................................. 23
TIPI DI DATO ASTRATTI – PARTE 1: PILE E CODE ............................................................................................. 25
TIPI DI DATO ASTRATTI ................................................................................................................................ 25

Giacomo Maccanti – g.maccanti@students.uninettunouniversity.net 3


ALGORITMI E PROGRAMMAZIONE AVANZATA - G.MACCANTI@STUDENTS.UNINETTUNOUNIVERSITY.NET

ADT (Abstract Data Type) ............................................................................................................................ 25


IMPLEMENTARE UN ADT IN C - ESEMPIO .................................................................................................... 25
ADT PILA (STACK)......................................................................................................................................... 25
IMPLENTAZIONE DI UNA PILA TRAMITE VETTORE ...................................................................................... 26
Implementazione di una pila tramite vettore: definizione del tipo ......................................................... 27
PRIMITIVE DI GESTIONE DI UNA PILA .......................................................................................................... 27
Inizializzazione di una pila: codice c ........................................................................................................ 27
Pila vuota: codice c .................................................................................................................................. 27
Pila piena: codice c .................................................................................................................................. 28
Top di una pila: codice c .......................................................................................................................... 28
Pop di una pila: codice c .......................................................................................................................... 28
Visualizza elementi di una pila: codice c .................................................................................................. 28
PROGRAMMA DI ESEMPIO: ADT PILA ......................................................................................................... 29
IMPLENTAZIONE DI UNA PILA TRAMITE LISTA ............................................................................................ 30
Implementazione di una pila tramite lista: definizione del tipo .............................................................. 30
ADT CODA (QUEUE) ..................................................................................................................................... 30
PRIMITIVE DI UNA CODA ............................................................................................................................. 30
IMPLEMENTAZIONE DI UNA CODA TRAMITE VETTORE .............................................................................. 31
Implementazione di una coda tramite vettore: definizione del tipo........................................................ 32
PRIMITIVE DI GESTIONE DI UNA CODA ....................................................................................................... 32
Inizializzazione di una coda: codice c....................................................................................................... 32
Coda vuota: codice c ................................................................................................................................ 32
Coda piena: codice c ................................................................................................................................ 32
Enqueue: codice c .................................................................................................................................... 32
Dequeue: codice c .................................................................................................................................... 33
TIPI DI DATO ASTRATTI – PARTE 2: CODE A PRIORITA’ E ALBERI ..................................................................... 34
ADT CODA A PRIORITA’ ............................................................................................................................... 34
HEAP ............................................................................................................................................................ 34
Heap Property ......................................................................................................................................... 34
Shape Property ........................................................................................................................................ 34
Heap: a cosa serve? ................................................................................................................................. 35
PRIMITIVE DI GESTIONE DI UNA CODA A PRIORITA’ ................................................................................... 35
Heapify: codice c ...................................................................................................................................... 35
Heapify: esempio ..................................................................................................................................... 36

Giacomo Maccanti – g.maccanti@students.uninettunouniversity.net 4


ALGORITMI E PROGRAMMAZIONE AVANZATA - G.MACCANTI@STUDENTS.UNINETTUNOUNIVERSITY.NET

BuildHeap: codice c ................................................................................................................................. 37


Estrazione del massimo in una coda a priorità: codice c ......................................................................... 37
Inserimento in una coda a priorità: codice c ........................................................................................... 37
ADT ALBERO ................................................................................................................................................ 39
ADT albero: codice c ................................................................................................................................ 39
ADT albero binario: codice c .................................................................................................................... 39
ADT albero: operazioni ............................................................................................................................ 39
ALGORITMI E PROGRAMMAZIONE AVANTAZA – PARTE II – ALGORITMI........................................................ 40
CONCETTO DI ALGORITMO.......................................................................................................................... 40
PERCHE’ SONO IMPORTANTI? ..................................................................................................................... 40
ESEMPI DI ALGORITMI: ................................................................................................................................ 40
NON SOLO ALGORITMI ................................................................................................................................ 40
PROGETTO DI UN ALGORITMO.................................................................................................................... 40
PARADIGMI ALGORITMICI ........................................................................................................................... 40
Paradigmi generali .................................................................................................................................. 40
Paradigmi per problemi di ottimizzazione ............................................................................................... 40
DIVIDE AND CONQUER (DIVIDE-ET-IMPERA) – DIVIDI E RISOLVI ................................................................ 41
RICERCA ED ENUMERAZIONE ...................................................................................................................... 41
PROGRAMMAZIONE DINAMICA .................................................................................................................. 41
PARADIGMA GREEDY (AVIDO) .................................................................................................................... 41
ANALISI DEGLI ALGORITMI .............................................................................................................................. 42
ANALISI DI COMPLESSITA’: DEFINIZIONI ..................................................................................................... 42
REQUISITI..................................................................................................................................................... 42
IMPORTANZA DELLA COMPLESSITA’ ........................................................................................................... 43
ANALISI DI COMPLESSITA’ ........................................................................................................................... 43
NOTAZIONE O (“O GRANDE”) ...................................................................................................................... 44
NOTAZIONE Ω (“OMEGA GRANDE”)............................................................................................................ 44
NOTAZIONE Θ (“TETA GRANDE”) ................................................................................................................ 44
PROPRIETA’ DELLA NOTAZIONE ASINTOTICA .............................................................................................. 46
Analogica con confronti numerici: ........................................................................................................... 46
Transitività .............................................................................................................................................. 46
Simmetria ................................................................................................................................................ 46
Simmetria trasposta ................................................................................................................................ 46
Riflessività ............................................................................................................................................... 46

Giacomo Maccanti – g.maccanti@students.uninettunouniversity.net 5


ALGORITMI E PROGRAMMAZIONE AVANZATA - G.MACCANTI@STUDENTS.UNINETTUNOUNIVERSITY.NET

COMPLESSITA’ DELL’INSERTIONSORT ......................................................................................................... 46


Complessità dell’Insertion Sort: tempo di esecuzione. ........................................................................... 48
ANALISI DI PROGRAMMI RICORSIVI: RICORRENZE .......................................................................................... 49
RISOLVERE UNA RICORRENZA ..................................................................................................................... 49
SERIE E SOMMATORIE ................................................................................................................................. 49
Serie aritmetica ....................................................................................................................................... 49
Serie geometrica...................................................................................................................................... 49
SOLUZIONE DELLE RICORRENZE .................................................................................................................. 49
METODO DELLA SOSTITUZIONE .................................................................................................................. 49
METODO ITERATIVO .................................................................................................................................... 49
ALGORITMI DI ORDINAMENTO ....................................................................................................................... 51
PERCHE L’ORDINAMENTO? ......................................................................................................................... 51
ORDINAMENTO ........................................................................................................................................... 51
Simmetrica .............................................................................................................................................. 51
Transitiva ................................................................................................................................................. 51
Anti riflessiva ........................................................................................................................................... 51
ASSUNZIONI ................................................................................................................................................ 51
CLASSI DI ALGORITMI DI ORDINAMENTO ................................................................................................... 51
HEAPSORT ................................................................................................................................................... 52
Heap: costo delle primitive ...................................................................................................................... 52
Heap e ordinamento ............................................................................................................................... 52
Heap: implementazione .......................................................................................................................... 52
Heapsort: pseudocodice .......................................................................................................................... 52
Heapsort: esempio .................................................................................................................................. 53
Heapsort: analisi della complessità asintotica ........................................................................................ 54
QUICKSORT .................................................................................................................................................. 55
Quicksort: pseudocodice .......................................................................................................................... 55
Quicksort: partition ................................................................................................................................. 56
Quicksort partition: pseudocodice ........................................................................................................... 56
Quicksort: esempio .................................................................................................................................. 56
Quicksort: analisi ..................................................................................................................................... 58
Analisi del Quicksort: caso migliore ......................................................................................................... 58
Analisi del Quicksort: caso medio ............................................................................................................ 58
Analisi del Quicksort: caso peggiore ........................................................................................................ 58

Giacomo Maccanti – g.maccanti@students.uninettunouniversity.net 6


ALGORITMI E PROGRAMMAZIONE AVANZATA - G.MACCANTI@STUDENTS.UNINETTUNOUNIVERSITY.NET

ALGORITMI CON COMPLESSITA’ LINEARE ....................................................................................................... 59


COUNTING SORT (ORDINAMENTO PER CONTEGGIO) ................................................................................. 59
Counting sort: principio ........................................................................................................................... 59
Counting sort: strutture dati.................................................................................................................... 59
Counting sort: pseudocodice ................................................................................................................... 59
Counting sort: esempio............................................................................................................................ 60
Counting sort: analisi............................................................................................................................... 60
STABILITA’ DI UN ALGORITMO DI ORDINAMENTO ..................................................................................... 61
CONCLUSIONI ALGORITMI DI ORDINAMENTO ............................................................................................ 61
INSIEMI DINAMICI E DIZIONARI ...................................................................................................................... 62
OPERAZIONI ................................................................................................................................................ 62
Interrogazioni .......................................................................................................................................... 62
Operazioni di modifica ............................................................................................................................ 62
PRIMITIVE DI GESTIONE DEGLI INSIEMI ...................................................................................................... 62
Ricerca in un BST: definizione .................................................................................................................. 62
Inserimento in un BST: definizione .......................................................................................................... 62
Minimum in un BST: definizione .............................................................................................................. 62
Predecessor in un BST: definizione .......................................................................................................... 63
Successor in un BST: definizione .............................................................................................................. 63
Isempty in un BST: definizione ................................................................................................................. 63
Clear in un BST: definizione ..................................................................................................................... 63
Size in un BST: definizione ....................................................................................................................... 63
List in un BST: definizione ........................................................................................................................ 63
DIZIONARI .................................................................................................................................................... 64
COME IMPLEMENTARE UN INSIEME DINAMICO? ....................................................................................... 64
ALBERI BINARI DI RICERCA (BST) ................................................................................................................. 65
PROPRETA’ DI UN BST ................................................................................................................................. 65
PRIMITIVE DI GESTIONE DI UN BST ............................................................................................................. 65
Ricerca di un BST ..................................................................................................................................... 65
Ricerca di un BST (ricorsiva): pseudocodice ............................................................................................. 66
Ricerca di un BST (iterativa): pseudocodice ............................................................................................. 66
Ricerca di un BST (iterativa): costo .......................................................................................................... 66
Ricerca di un BST: esempio ...................................................................................................................... 66
Visita di un BST ........................................................................................................................................ 67

Giacomo Maccanti – g.maccanti@students.uninettunouniversity.net 7


ALGORITMI E PROGRAMMAZIONE AVANZATA - G.MACCANTI@STUDENTS.UNINETTUNOUNIVERSITY.NET

Visita inorder (ordine infisso) di un BST: pseudocodice ........................................................................... 67


Visita preorder (prefisso) di un BST: pseudocodice .................................................................................. 67
Visita postorder (ordine postfisso) di un BST: pseudocodice ................................................................... 67
Visita di un BST: esempio ......................................................................................................................... 67
Inserimento in un BST: definizione .......................................................................................................... 67
Inserimento in un BST: pseudocodice ...................................................................................................... 67
Inserimento in un BST: esempio .............................................................................................................. 68
Inserimento in un BST: analisi di complessità .......................................................................................... 69
Cancellazione in un BST: definizione ........................................................................................................ 69
Cancellazione in un BST: caso 1 ............................................................................................................... 69
Cancellazione in un BST: caso 2 ............................................................................................................... 69
Cancellazione in un BST: caso 3 ............................................................................................................... 69
Tree-Min in un BST: pseudocodice ........................................................................................................... 69
Tree-Max in un BST: pseudocodice .......................................................................................................... 69
Successore in un BST: definizione ............................................................................................................ 69
Successore in un BST: pseudocodice ........................................................................................................ 70
Successore in un BST: esempio ................................................................................................................ 70
Cancellazione in un BST: pseudocodice ................................................................................................... 71
Cancellazione in un BST: analisi di complessità ....................................................................................... 71
ADT BST in c: codice c .............................................................................................................................. 71
Ricerca in un BST: codice c ...................................................................................................................... 71
TABELLE HASH ................................................................................................................................................. 72
TABELLE AD ACCESSO DIRETTO ................................................................................................................... 72
Tabelle ad accesso diretto: notazione ..................................................................................................... 72
Tabelle ad accesso diretto: esempio ....................................................................................................... 72
Tabelle ad accesso diretto: problemi ....................................................................................................... 73
TABELLA HASH ............................................................................................................................................. 73
FUNZIONI DI HASH ...................................................................................................................................... 74
Funzioni di HASH: requisiti....................................................................................................................... 74
GESTIONE DELLE COLLISIONI ....................................................................................................................... 74
Gestione delle collisioni: concatenazione ............................................................................................... 75
Gestione delle collisioni: indirizzamento aperto ...................................................................................... 75
PRIMITIVE DI GESTIONE DELLE TABELLE HASH ........................................................................................... 76
LINEAR PROBING ......................................................................................................................................... 76

Giacomo Maccanti – g.maccanti@students.uninettunouniversity.net 8


ALGORITMI E PROGRAMMAZIONE AVANZATA - G.MACCANTI@STUDENTS.UNINETTUNOUNIVERSITY.NET

PARADIGMI ALGORITMICI – PARTE I – PROGRAMMAZIONE DINAMICA ........................................................ 77


PROGETTO DI UN ALGORITMO ................................................................................................................... 77
Paradigmi generali .................................................................................................................................. 77
Paradigmi per problemi di ottimizzazione ............................................................................................... 77
QUALE STRATEGIA SCEGLIERE? ............................................................................................................... 77
PROGRAMMAZIONE DINAMICA .................................................................................................................. 77
Divide-et-impera e programmazione dinamica ....................................................................................... 77
Programmazione dinamica, quando e come? ......................................................................................... 78
IL CAMBIO DI MONETE ................................................................................................................................ 78
Il cambio di monete: algoritmo più intuitivo ........................................................................................... 78
Il cambio di monete: soluzione intuitiva non ottima ............................................................................... 78
Il cambio di monete: soluzione non intuitiva ma ottima ......................................................................... 79
Il cambio di monete: applicazione della programmazione dinamica ...................................................... 79
Il cambio di monete: esempio di applicazione alla programmazione dinamica ...................................... 80
PARADIGMI ALGORITMICI – PARTE II – ALGORITMI GREEDY .......................................................................... 81
IL PARADIGMA GREEDY ............................................................................................................................... 81
ALGORITMI GREEDY: STRUTTURA GENERALE ............................................................................................. 81
Algoritmi greedy: struttura generale – schema 1 .................................................................................... 81
Algoritmi greedy: struttura generale – schema 2 .................................................................................... 81
Il problema dello zaino ............................................................................................................................ 82
Selezione di attività ................................................................................................................................. 83
Selezione di attività: pseudocodice .......................................................................................................... 83
Selezione di attività: esempio .................................................................................................................. 84
STRATEGIA GREEDY E OTTIMALITA’ ............................................................................................................ 84
Sottostruttura ottima .............................................................................................................................. 84
PARADIGMI ALGORITMICI – PARTE III – BACKTRACKING ................................................................................ 85
Ricerca esplicita (brute force) .................................................................................................................. 85
Ricerca implicita ...................................................................................................................................... 85
SPAZIO DELLE SOLUZIONI ............................................................................................................................ 85
ALGORITMO DI BACKTRACKING .................................................................................................................. 86
I GRAFI – PARTE I ............................................................................................................................................. 89
I GRAFI ......................................................................................................................................................... 89
DEFINIZIONI E PROPRIETA’ .......................................................................................................................... 89
Definizioni e proprietà: grafi orientati ..................................................................................................... 89

Giacomo Maccanti – g.maccanti@students.uninettunouniversity.net 9


ALGORITMI E PROGRAMMAZIONE AVANZATA - G.MACCANTI@STUDENTS.UNINETTUNOUNIVERSITY.NET

Definizioni e proprietà: grafi non orientati.............................................................................................. 90


Definizioni e proprietà: incidenza ............................................................................................................ 90
Definizioni e proprietà: adiacenza ........................................................................................................... 90
Definizioni e proprietà: incidenza e adiacenza ........................................................................................ 90
Definizioni e proprietà: grado di un vertice ............................................................................................ 91
Definizioni e proprietà: grafi pesati ......................................................................................................... 91
Definizioni e proprietà: sottografo .......................................................................................................... 91
Definizioni e proprietà: sottografo indotto ............................................................................................. 92
Definizioni e proprietà: percorso o cammino .......................................................................................... 92
Definizioni e proprietà: ciclo ................................................................................................................... 93
Definizione e proprietà: raggiungibilità ................................................................................................... 93
Definizione e proprietà: alberi ................................................................................................................ 94
RAPPRESENTAZIONE DI GRAFI .................................................................................................................... 95
Rappresentazione di grafi: matrice di adiacenza .................................................................................... 95
Rappresentazione di grafi: lista di adiacenza .......................................................................................... 95
Rappresentazione di grafi: efficienza ...................................................................................................... 95
ADT GRAFO .................................................................................................................................................. 95
Implementazione di un grafo tramite lista di adiacenza ......................................................................... 96
Implementazione di un grafo tramite lista di adiacenza: definizione del tipo ......................................... 96
PRIMITIVE DI GESTIONE DEI GRAFI ............................................................................................................. 97
Visite di un grafo: visita in ampiezza ....................................................................................................... 98
Visite di un grafo: visita in profondità ..................................................................................................... 99
Visite di un grafo: algoritmi di visita...................................................................................................... 100
Visite di un grafo - algoritmi di visita generico - inizializzazione: pseudocodice ................................... 100
Visite di un grafo - algoritmi di visita generico: pseudocodice .............................................................. 100
Visite di un grafo: visita in ampiezza (BFS) ............................................................................................ 101
Visite di un grafo (BFS) in ampiezza: pseudocodice ............................................................................... 101
Visite di un grafo (BFS) in ampiezza: proprietà ......................................................................................... 103
Visite di un grafo: visita in profondità (DFS) .......................................................................................... 103
Visite di un grafo (DFS) in profondità: pseudocodice............................................................................. 103
ETICHETTE TEMPORALI ............................................................................................................................. 106
Alberi di copertura minima: applicazione .............................................................................................. 106
Alberi di copertura minima: algoritmo generico ................................................................................... 108
Alberi di copertura minima: pseudocodice ............................................................................................ 108

Giacomo Maccanti – g.maccanti@students.uninettunouniversity.net 10


ALGORITMI E PROGRAMMAZIONE AVANZATA - G.MACCANTI@STUDENTS.UNINETTUNOUNIVERSITY.NET

Alberi di copertura minima: individuazione degli archi sicuri ................................................................ 109


Alberi di copertura minima: analisi dell’algoritmo. ............................................................................... 109
ALGORITMO DI KRUSKAL........................................................................................................................... 111
Algoritmo di Kruskal: pseudocodice ....................................................................................................... 111
Algoritmo di Kruskal: analisi................................................................................................................... 111
Percorsi minimi in un grafo: definizioni e proprietà ................................................................................ 111
Percorsi minimi in un grafo: applicazioni pratiche ................................................................................. 112
Percorsi minimi in un grafo: archi con peso negativo ............................................................................. 113
Percorsi minimi in un grafo: algoritmi .................................................................................................... 113
Percorsi minimi in un grafo: rilassamento .............................................................................................. 113
Percorsi minimi in un grafo – rilassamento: pseudocodice.................................................................... 114
Percorsi minimi in un grafo – rilassamento: esempio ............................................................................. 114
ALGORITMO DI DIJKSTRA ........................................................................................................................... 114
ALGORITMO DI BELLMAN-FORD ............................................................................................................... 118
Algoritmo di Bellman-Ford: pseudocodice.............................................................................................. 118
TEORIA DELLA COMPLESSITÀ ........................................................................................................................ 119
PROBLEMI INTRATTABILI ........................................................................................................................... 119
Problemi trattabili e intrattabili ............................................................................................................. 119
Problemi intrattabili ............................................................................................................................... 119
Proprietà di chiusura degli algoritmi polinomiali.................................................................................... 119
Tutti gli algoritmi sono polinomiali?....................................................................................................... 119
PROBLEMI DI DECISIONE ........................................................................................................................... 120
Ottimizzazione VS Decisione .................................................................................................................. 120
NON DETERMINISMO ................................................................................................................................ 120
DETERMINISMO VS NON DETERMINISMO................................................................................................. 120
Determinismo VS non determinismo: istruzione choiche() ..................................................................... 120
Determinismo VS non determinismo: struttura di un algoritmo non deterministico .............................. 120
NP COMPLETEZZA E ALGORITMO APPROSSIMATI ........................................................................................ 121
RIDUCIBILITA’ ............................................................................................................................................ 121
PROBLEMI NP- completi E NP-hard ........................................................................................................... 121
Problemi NP-hard .................................................................................................................................. 121
Problemi NP-completi ............................................................................................................................ 122
DIMOSTRAZIONE DI NP-completezza ........................................................................................................ 122
TEOREMA DI COOK E PROBLEMA SAT ....................................................................................................... 122

Giacomo Maccanti – g.maccanti@students.uninettunouniversity.net 11


ALGORITMI E PROGRAMMAZIONE AVANZATA - G.MACCANTI@STUDENTS.UNINETTUNOUNIVERSITY.NET

ALGORITMO NON DETERMINISTICO ......................................................................................................... 122


ALTRI PROBLEMI NP .................................................................................................................................. 123
PROBLEMI NP-completi CLASSICI .............................................................................................................. 123
Problemi NP-completi classici: problema della massima clique ............................................................ 123
Problemi NP-completi classici: problema della copertura dei vertici ...................................................... 124
Problemi NP-completi classici: problema del commesso viaggiatore ..................................................... 124
ALGORITMI PSEUDOPOLINOMIALI ............................................................................................................ 124
Algoritmi approssimati: approssimazioni ............................................................................................... 124
Algoritmi approssimati: euristiche ......................................................................................................... 124
RINGRAZIAMENTI ...................................................................................................................................... 125

N.B. All’interno del presente documento ci sono svariati collegamenti ipertestuali che sono
riconducibili alla forma ALGORITMI E PROGRAMMAZIONE AVANZATA – PARTE 1 - RICORSIONE E
ALGORITMI RICORSIVI. Cliccandovisi sopra (ctrl+click) potrete aprirli.

Giacomo Maccanti – g.maccanti@students.uninettunouniversity.net 12


ALGORITMI E PROGRAMMAZIONE AVANZATA - G.MACCANTI@STUDENTS.UNINETTUNOUNIVERSITY.NET

ALGORITMI E PROGRAMMAZIONE AVANZATA – PARTE 1 - RICORSIONE E


ALGORITMI RICORSIVI
ALGORITMO RICORSIVO
In informatica viene detto algoritmo ricorsivo un algoritmo espresso in termini di se stesso, ovvero in cui
l'esecuzione dell'algoritmo su un insieme di dati comporta la semplificazione o suddivisione dell'insieme di
dati e l'applicazione dello stesso algoritmo agli insiemi di dati semplificati.

ALGORITMO ITERATTIVO
In informatica viene detto algoritmo iterativo un algoritmo che esprime un certo numero di volte, un certo
numero di operazioni. Un esempio è il ciclo for o il ciclo while, i quali eseguono un certo numero di volte le
operazioni contenute al loro interno.

RICORSIONE DIRETTA
Si verifica quando una funzione, all’interno del suo corpo, richiama sé stessa.

RICORSIONE INDIRETTA
Si verifica quando una funzione, all’interno del suo corpo, esegue una chiamata ad una funzione diversa, la
quale, direttamente o indirettamente, richiama il metodo principale.

PERCHE’ SCRIVIAMO FUNZIONI RICORSIVE?


Perché sono molto potenti e permettono di esprimere un insieme infinito per mezzo di un’istruzione finita.
Inoltre, molti problemi, si prestano naturalmente ad una formulazione ricorsiva.

ANALISI DI UNA FUNZIONE RICORSIVA:


1. TOP-DOWN: analisi delle chiamate per come effettivamente avvengono e avviene
attraverso la costruzione di un albero di ricorsione che ha come primo elemento (radice) il
valore corrispondente alla chiamata. Questo tipo di analisi è molto tedioso e lungo.
2. BOTTOM-UP: analisi partendo dai casi terminali, cioè quei problemi elementari.

N.B. quando scriviamo funzioni ricorsive è essenziale gestire il problema della terminazione.

Giacomo Maccanti – g.maccanti@students.uninettunouniversity.net 13


ALGORITMI E PROGRAMMAZIONE AVANZATA - G.MACCANTI@STUDENTS.UNINETTUNOUNIVERSITY.NET

PUNTATORI E INDIRIZZI
PUNTATORI
Il puntatore è un tipo, cioè una variabile, che contiene un indirizzo di memoria.
Come sappiamo bene, in C, è possibile utilizzare le funzioni (sottoprogrammi) solamente passando loro un
valore, quindi il contenuto di una variabile e non la variabile stessa. Per fare un esempio: facciamo finta che
io, nel metodo main() abbia un vettore V[n], sarebbe impossibile pensare di passare quel vettore ad una
funzione valorizzaVettore() affinché questa esegua la valorizzazione dello stesso per poi tornare il tutto al
metodo main chiamante; non perché non lo si voglia fare, ma perché il linguaggio stesso non lo permette.
Il C, quindi, per far si di lavorare dinamicamente su alcune strutture, ha messo a disposizione i puntatori
che, effettivamente, lavorano per contenuti e questi contenuti, molto furbescamente, non sono altro che
gli indirizzi di memoria di una data variabile. Io posso, quindi, passare ad una funzione, anziché il vettore, il
suo indirizzo di memoria, così che la mia funzione ci possa lavorare sopra tranquillamente senza incorrere
in problemi.

Per poter dichiarare un puntatore è necessario utilizzare l’operatore di indirezzione ‘*’.

Puntatore: <tipo> *identificatore;

Per ogni tipo di variabile esiste un tipo di puntatore:

int x; int *px;


int y; int *py;
double z; double *pz;

INDIRIZZI
Per accedere al contenuto di una variabile o, per meglio dire, per accedere alla locazione di memoria di una
specifica variabile, è necessario utilizzare l’operatore di indirizzo ‘&’.

Indirizzo: & <variabile>;

Quindi, quando vogliamo associare ad un puntatore, l’indirizzo di una variabile, sarà necessario dichiarare
una variabile di tipo puntatore (*px) e, successivamente, associare alla variabile puntatore, l’indirizzo della
variabile da puntare (px = &x).

int x; int *px;


px= &x;

‘*’: operatore unario di indirezione; ritorna il valore contenuto dal puntatore (cioè dalla variabile puntata).
‘&’: operatore di indirizzo; opera su una variabile e ne ritorna l’indirizzo.

OPERAZIONI SUI PUNTATORI


N.B. l’incremento e il decremento, agiscono sulla variabile a cui fa riferimento il puntatore.
Incremento: pa++; pa +=2;
Decremento: pa--; pa -=2;
Sottrazione: pa – pb;
Assegnazione: pa = pb;
Confronto: pa == pb;

Giacomo Maccanti – g.maccanti@students.uninettunouniversity.net 14


ALGORITMI E PROGRAMMAZIONE AVANZATA - G.MACCANTI@STUDENTS.UNINETTUNOUNIVERSITY.NET

PROGRAMMA DI ESEMPIO PUNTATORI E INDIRIZZI


#include <stdio.h>
#include <stdlib.h>

int main()
{
//DICHIARO UNA VARIABILE GENERICA A
int a;

//DICHIARO UNA VARIABILE GENERICA PUNTATORE; IN QUESTO CASO PUNTERA' AD A


int *pa;

//ASSOCIO ALLA VARIABILE A, IL VALORE DI 10


a = 10;

//DICO AL PUNTATORE DI PUNTARE ALL'INDIRIZZO DELLA VARIABILE A


pa = &a;

//STAMPO IL COTENUTO DELLA VARIABILE A E IL CONTENUTO DELLA VARIABILE PA, CHE, IN QUESTO
CASO, È L'INDIRIZZO DI MEMORIA DI A
printf("\n a: %d --> pa: %p \n", a, pa);

//STAMPO IL CONTENUTO DI A, E IL CONTENUTO CHE TROVO ALL'INTERNO DELL'INDIRIZZO DI MEMORIA


PUNTATO DA PA
printf("\n a: %d --> pa: %d \n", a, *pa);

return 0 ;
}

Giacomo Maccanti – g.maccanti@students.uninettunouniversity.net 15


ALGORITMI E PROGRAMMAZIONE AVANZATA - G.MACCANTI@STUDENTS.UNINETTUNOUNIVERSITY.NET

ALLOCAZIONE DINAMICA DELLA MEMORIA


Quando parliamo di allocazione della memoria, dobbiamo fare una prima distinzione tra allocazione statica
e allocazione dinamica:
• Allocazione statica: la memoria resta occupata dall’attivazione del programma fino al suo termine;
inoltre, è definita a priori, quindi non è possibile variarne la grandezza. Un vettore v[10] non potrà
mai diventare, runtime, v[5] o v[15]; per farlo sarà necessario modificare il programma e
rieseguirlo.
• Allocazione dinamica: l’allocazione di memoria è gestita direttamente dal programmatore. Poter
allocare memoria dinamicamente, significa:
o Allocare un’area di memoria di grandezza esplicita (varia in base al bisogno);
o Ingrandire l’area di memoria già dichiarata per poterla espandere;
o Diminuire l’area di memoria già allocata;
o Liberare del tutto l’area di memoria.

ISTRUZIONE MALLOC()
L’istruzione malloc() sta per MEMORY ALLOCATION ed è l’istruzione che ci permette di allocare
dinamicamente la memoria.
Strumenti: Void * malloc(<dimensione>); //LA <DIMENSIONE> è il numero di byte da allocare.

char *p;
p=(char*)malloc(10); //DIECI CARATTERI MEMORIZZATI A PARTIRE DALL’INDIRIZZO P.

Oppure:
int *px;
px(int*)malloc10*sizeof(int);

È buona pratica controllare, sempre, l’effettiva allocazione dinamica della memoria:


char *p;
p=(char*)malloc(10);
if(p==NULL){
printf(“ERRORE, ALLOCAZIONE NON RIUSCITA!”);
else
printf(“OK, ALLOCAZIONE RIUSCITA!”);
}

ISTRUZIONE FREE()
La memoria allocata, che non serve più, andrebbe liberata; almeno è buona pratica farlo. Per poter liberare
memoria precedentemente allocata, si utilizza l’istruzione free().
Void free(<puntatore>); //<puntatore> E’ IL PUNTATORE PRECEDENTEMENTE ALLOCATO CON MALLOC()

PROGRAMMA DI ESEMPIO: MALLOC


#include <stdio.h>
#include <stdlib.h>
int main(){
int N, *v;
printf("INDICA QUANTI ELEMENTI DEVE CONTENERE IL VETTORE:\n");
scanf("%d", &N);
v = (int*)malloc(N*sizeof(int));
printf("P = %p\n", v);
if(v!=NULL){
v[0] = 1;
printf("V[0] = %d\n", v[0]); }
return 0;}

Giacomo Maccanti – g.maccanti@students.uninettunouniversity.net 16


ALGORITMI E PROGRAMMAZIONE AVANZATA - G.MACCANTI@STUDENTS.UNINETTUNOUNIVERSITY.NET

TYPEDEF
In C esiste il typedef che ha la proprietà di definire nuovi nomi per i tipi di dati; questo permette di rendere
il codice più portabile e più leggibile.

Sintassi: typedef <tipo> <nome>

In tal modo non si crea un nuovo tipo, ma si definisce un nuovo tipo e un nome ad esso associato. La
sintassi è la seguente:

PROGRAMMA DI ESEMPIO: TYPEDEF


typedef float tipo;
tipo variabile;
permette, inoltre, di definire assegnare un nome ad un tipo struttura, unione ed enumerazione.
Esempio:
typedef struct tm
{ int ore;
int min;
int sec
}time;
/*time è il nome di un tipo */

struct tm var;
time var;

struct tm *punt;
time *punt;

Giacomo Maccanti – g.maccanti@students.uninettunouniversity.net 17


ALGORITMI E PROGRAMMAZIONE AVANZATA - G.MACCANTI@STUDENTS.UNINETTUNOUNIVERSITY.NET

LISTE LINEARI
Una lista lineare è una collezione di oggetti omogenei (dello stesso tipo), allocata dinamicamente e
memorizzata in maniera non contigua. Occupa, in memoria, una posizione qualsiasi che, tra l’altro, può
essere cambiata dinamicamente durante l’utilizzo della stessa. La sua dimensione non è nota a priori e può
variare nel tempo (l’opposto dell’array, in cui la dimensione è ben nota e non modificabile. Una lista può
contenere uno o più campi contenenti informazioni e, necessariamente, deve contenere un puntatore per
mezzo del quale è legato all’elemento successivo.

IMPLEMENTAZIONE
Una particolarità delle liste (a differenza, ad esempio, degli array) è che sono costituite da due funzioni del
C (strutture e puntatori), quindi non sono un tipo di dato nuovo, basato su implementazioni particolari, ma
solo il risultato di un uso sapiente di tali costrutti.
La lista base ha solo un campo informazione ed un puntatore, come mostrato di seguito:

Elemento = informazione + puntatore

Tradotto, in linguaggio C, una lista è definita come di seguito:

struct EL{
int Info; //CONTIENE I DATI, QUINDI E’ LA CHIAVE (KEY). VERRA’ UTILIZZATA PER ESEGUIRE
RICERCHE SUGLI ELEMENTI DELLA LISTA
struct EL *Prox; //E’ UN PUNTATORE A UN TIPO (struct EL). QUINDI NEL PUNTATORE E’
CONTENUTO L’INDIRIZZO DI UN ALTRO ELEMENTO DELLO STESSO TIPO. ECCO COME FACCIAMO A MANTENERE I
COLLEGAMENTI TRA I VARI ELEMENTI
};

Per semplicità e per evitare di scrivere troppo codice, definiamo due nuovi tipi, uno per l’elemento e uno
per il collegamento, come di seguito:

typedef struct EL ElemLista;


typedef ElemLista *PElemLista;

Quindi, ogni volta che definiamo una variabile di tipo ElemLista, in realtà è una struct con i campi Info, ecc.
La nostra forma definitiva, usando i nuovi tipi, quindi, sarà:

struct{
TipoElemento info;
PElemLista Prox;
};

STATO DELLA LISTA


La nostra lista ha bisogno di avere uno stato, cioè un elemento che indichi se questa contiene qualche cosa.
Una specie di sentinella, quindi.
Questa sentinella è, ovviamente, una variabile, in particolare un puntatore, che assume il nome di “testa”
della lista. Questa “testa” è, sia il primo elemento da cui noi partiremo per eseguire la scansione di una
lista, sia l’indicatore di valorizzazione. Se ci pensiamo bene, infatti, essendo che la testa dovrà avere un
puntamento all’elemento successivo della lista, nel momento in cui non dovesse avere elementi, ci dirà
anche che la lista è vuota.

Inizialmente, una lista sarà sempre vuota (NULL); la sua dichiarazione sarà:
PElemLista Head = NULL;

Giacomo Maccanti – g.maccanti@students.uninettunouniversity.net 18


ALGORITMI E PROGRAMMAZIONE AVANZATA - G.MACCANTI@STUDENTS.UNINETTUNOUNIVERSITY.NET

PRIMITIVE DI GESTIONE DELLE LISTE


Indicano le operazioni che si possono eseguire sulle liste; esse, sono:
• INIZIALIZZAZIONE;
• INSERIMENTO DI UN ELEMENTO;
• CANCELLAZIONE DI UN ELEMENTO;
• RICERCA ELEMENTI;
• VISITA;

Inizializzazione di una lista: codice c


Non fa altro che assegnare il valore NULL alla testa della lista. Per fare questo, in maniera parametrica,
dovremmo scrivere una funzione che passi il parametro “testa” (puntatore che rappresenta il parametro
testa della lista) per indirizzo alla nostra funzione Inizializza.

void Inizializza(PElemLista *p){


*p = NULL; //SIGNIFICA: IL VALORE A CUI P PUNTA, METTIAMOLO A NULL
}

Ricerca in una lista: definizione


Siamo nel caso in cui dobbiamo scorrere una lista alla ricerca di un elemento. Siamo coscienti del fatto che
per cercare un elemento all’interno di una lista, nel caso peggiore, dobbiamo scorrerla tutta.
Per cercare un elemento all’interno di una lista, dobbiamo:
• Scandire la lista a partire dalla testa;
• Confrontare il generico elemento della lista con quello che stiamo cercando.
La funzione di ricerca, potrà quindi tornare i seguenti stati:
• Vero, se l’elemento è stato trovato all’interno della lista;
• Falso, se arriviamo alla fine della lista e non lo abbiamo trovato.

Ricerca in una lista: codice c


int Ricerca(PElemLista p, int x) //PASSO TESTA DELLA LISTA ED ELEMENTO DA RICERCARE
PElemLista q; //CREO UNA NUOVA STRUTTURA LISTA CHE CHIAMO q
q = p; //DICO CHE q È UGUALE ALLA TESTA DELLA LISTA
while(q != NULL){ //Q PUÒ ESSERE A NULL SE LA LISTA È VUOTA O SE HO SCORSO TUTTI GLI ELEMENTI
if(q->Info == x){ //SE IL CAMPO INFO DELLA STRUTTURA È UGUALE A X (ELEMENTO CERCATO)
return 1;} //TORNO SUBITO 1, POICHÉ HO TROVATO L’ELEMENTO
q = q ->Prox; //ALTRIMENTI, AGGIORNO IL PUNTATORE Q, CON L’ELEMENTO SUCCESSIVO
}
Return 0; //SE ARRIVO QUI, NON HO TORNATO NULLA PRIMA, QUINDI NON HO TROVATO NULLA
}

INVOCAZIONE: Ricerca(Head, x); //TESTA ED ELEMENTO DA RICERCARE

Visita di una lista: codice c


La visita è l’operazione che permette di eseguire una certa operazione su tutti gli elementi della lista. Ad
esempio, voglio che tutti gli elementi siano moltiplicati per due, ecc.
int Visita(PElemLista p){
PElemlista q;
q = p;
while(q!=NULL){
/*Opero su q->info*/
q = q->Prox;
}
return 0;}

Giacomo Maccanti – g.maccanti@students.uninettunouniversity.net 19


ALGORITMI E PROGRAMMAZIONE AVANZATA - G.MACCANTI@STUDENTS.UNINETTUNOUNIVERSITY.NET

INVOCAZIONE: Visita(Head); //TESTA, POICHE’ SI PARTE DAL PRIMO ELEMENTO DELLA LISTA

Inserimento in una lista: definizione


L’inserimento di un elemento in una lista, è fondamentale. Uno dei problemi più comuni, quando si parla di
una lista, è capire dove verrà effettivamente inserito questo nuovo elemento; in merito, ci sono diverse
opzioni:
• In testa;
• In coda;
• In ordine (se la lista è ordinata);
• Dopo un certo elemento.

Inserire un nuovo elemento, ovviamente, significa creare nuovo spazio nella lista e, per fare ciò, dovremmo
avvalerci dell’istruzione malloc.

Il primo inserimento che dobbiamo fare, ovviamente, è un inserimento “in testa”, poiché abbiamo una lista
vuota.

Inserimento in testa in una lista: codice c


void InserisciInTesta(PElemLista *p, int val)
{
PElemLista q;
q =(PElemLista)malloc(sizeof(ElemLista)); //q SARA' UN NUOVO ELEMENTO DI TIPO LISTA, MA
NON ANCORA PARTE DELLA LISTA STESSA
q->Info = val; //NEL CAMPO Info, ANDRO' AD INSERIRE UN VALORE
q->Prox = *p; //FACCIO PUNTARE Prox ALLA TESTA DELLA LISTA (VEDIAMO CHE MI E' STATA
PASSATA DAL MAIN)
*p = q; //ORA LA TESTA PUNTA A q
}

INVOCAZIONE: InserisciInTesta(&Head, 0); //TESTA, POICHE’ SI PARTE DAL PRIMO ELEMENTO DELLA
LISTA PIU’ L’ELEMENTO DA INSERIRE

Stato della lista prima dell’inserimento “in testa”:

Giacomo Maccanti – g.maccanti@students.uninettunouniversity.net 20


ALGORITMI E PROGRAMMAZIONE AVANZATA - G.MACCANTI@STUDENTS.UNINETTUNOUNIVERSITY.NET

Inserisco q; questo è il risultato


dell’istruzione malloc.

Stato della lista, al momento della creazione dell’elemento q, valorizzato a 0, in questo caso:

Valorizzo q: q->info = val;

in questo caso, assumerà valore 0.

Stato della lista dopo il collegamento di q alla “testa”:

Collego q alla testa della lista:

q->Prox = *p;

Stato della lista dopo il collegamento di q alla “testa”:

Collego la testa a q:

*p = q;

Giacomo Maccanti – g.maccanti@students.uninettunouniversity.net 21


ALGORITMI E PROGRAMMAZIONE AVANZATA - G.MACCANTI@STUDENTS.UNINETTUNOUNIVERSITY.NET

Elimina elemento di una lista


La cancellazione di un elemento della lista, all’interno di una stessa funzione, ha due implementazioni
particolari:
• Esegue la cancellazione nel caso in cui l’elemento da cancellare sia la testa della lista;
• Esegue la ricerca dell’elemento da cancellare e lo cancella fisicamente (caso generico).

Perché, dunque, due implementazioni? Perché, come abbiamo predetto, si potrebbe verificare il caso in cui
in una lista, l’elemento da cancellare, sia proprio la testa e, a quel punto, cancellandolo dobbiamo essere in
grado però di non perdere la concatenazione con gli altri elementi.

Elimina elemento di una lista: codice c


int eliminaElementoLista(PElemLista *h, int delElem){

PElemLista p, q; //DICHIARO DUE PUNTATORI

p = q = *h; //ASSOCIO AI DUE PUNTATORI, LA TESTA DELLA LISTA; QUINDI ENTRAMBI SONO INIZIALIZZATI
ALLA TESTA

if(p->Info == delElem) //SE IL PRIMO ELEMENTO (TESTA DELLA LISTA) E' L'ELEMENTO DA CANCELLARE...
{
*h = p->Prox; //ALLORA ALLA TESTA ASSOCIO SOLO IL PUNTATORE ALL'ELEMENTO SUCCESSIVO
free(p); //CANCELLO LA TESTA DELLA LISTA
return(1); //TORNO 1 PERCHE' HO ESEGUITO L'OPERAZIONE DI CANCELLAZIONE
}

//SE L'ELEMENTO DA CANCELLARE NON È LA TESTA, ALLORA FACCIO LA RICERCA...


while(q != NULL){ //CICLO TUTTA LA LISTA
p = q; //P È POSTO UGUALE A Q
q = q->Prox; //Q VIENE FATTO AVANZARE, COSI' DA AVERE UN PUNTATORE ALL'ELEMENTO PRECEDENTE
(p) E UNO AL SUCCESSIVO (q)
if(q->Info == delElem){ //L'ELEMENTO SUCCESSIVO E' DA CANCELLARE?
p->Prox = q->Prox; //SE SI', ALLORA DICO CHE p PUNTERA' ALL'ELEMENTO PUNTATO DOPO q;
COSI' COPRO IL BUCO
free(q); //LIBERO q
return(1); //TORNO 1, PER INDICARE CHE LA CANCELLAZIONE E' ANDATA A BUON FINE
}
}
return 0; //TORNO 0, A QUESTO PUNTO, POICHE' SE SONO ARRIVATO QUI L'ELEMENTO NON È STATO
ELIMINATO
}

INVOCAZIONE: eliminaElementoLista(&Head, 0); //TESTA, POICHE’ SI PARTE DAL PRIMO ELEMENTO DELLA
LISTA PIU’ L’ELEMENTO DA INSERIRE

Giacomo Maccanti – g.maccanti@students.uninettunouniversity.net 22


ALGORITMI E PROGRAMMAZIONE AVANZATA - G.MACCANTI@STUDENTS.UNINETTUNOUNIVERSITY.NET

PROGRAMMA GESTIONE PRIMITIVE LISTE: CODICE C


#include <stdio.h>
typedef struct el ElemLista;
typedef ElemLista *PElemLista;
struct el{
int Info;
PElemLista Prox;
};
void inizializzaLista(PElemLista *p){
*p = NULL;
}

void inserimentoInTesta(PElemLista *p, int val){


PElemLista q;
q = (PElemLista)malloc(sizeof(ElemLista));
q ->Info = val;
q ->Prox = *p;
*p = q;
}
void visualizzaLista(PElemLista p){
PElemLista q;
q = p;
printf("|--------------LISTA--------------|\n");
while(q!=NULL){
printf("|---- %d ----|\n", q->Info);
q = q->Prox;
}
printf("|---------------------------------|\n");
}
int main(){
PElemLista *Head;
int wanted =0, resultFind = 0, delElem = NULL, resultDelete = 0;
inizializzaLista(&Head);
inserimentoInTesta(&Head, 4);
visualizzaLista(Head);
visitaLista(Head, 1);
visualizzaLista(Head);
inserimentoDopoElemento(Head, 3, 5);
visualizzaLista(Head);
printf("CHE ELEMENTO DESIDERI RICERCARE? ");
scanf("%d", &wanted);
resultFind = ricercaElementoLista(Head, wanted);
if (resultFind == 0){
printf("SPIACENTI, ELEMENTO NON PRESENTE NELLA LISTA\n");
} else{
printf("ELEMENTO TROVATO NELLA LISTA\n");
}
printf("CHE ELEMENTO DESIDERI ELIMINARE? ");
scanf("%d", &delElem);
resultDelete = eliminaElementoLista(&Head, delElem);
if(resultDelete == 0){
printf("L'ELEMENTO NON E' STATO ELIMINATO\n");
}else{
printf("L'ELEMENTO E' STATO ELIMINATO\n");
}
visualizzaLista(Head);
return 0;
}
int visitaLista(PElemLista p, int val){
PElemLista q;
q = p;
while(q != NULL){
//SOMMO VAL A TUTTI GLI ELEMENTI

Giacomo Maccanti – g.maccanti@students.uninettunouniversity.net 23


ALGORITMI E PROGRAMMAZIONE AVANZATA - G.MACCANTI@STUDENTS.UNINETTUNOUNIVERSITY.NET

q->Info = q->Info + val;


//PASSO AL PROSSIMO
q = q ->Prox;
}
return 0;
}
int inserimentoDopoElemento(PElemLista p, int val, int after){
PElemLista q, h;
q = p;
h = (PElemLista)malloc(sizeof(ElemLista));
while(q != NULL){
if(q->Info == after){
h->Info = val; //NUOVO VALORE DA INSERIRE
h ->Prox = q ->Prox;
q ->Prox = h;
}
q = q->Prox;
}
return 0;
}
int ricercaElementoLista(PElemLista p, int wanted){
PElemLista q;
q = p;
while(q != NULL){
if(q->Info == wanted){
return 1;}
q = q->Prox;
}
return 0;
}
int eliminaElementoLista(PElemLista *h, int delElem){
PElemLista q, p;
q = p = *h;
if (p->Info == delElem){
*h = p->Prox;
free(p);
return(1);}
while(q != NULL){
p = q;
q = q->Prox;
if(q->Info == delElem){
p->Prox = q ->Prox;
free(q);
return(1);
}
return 0;
}
}

Giacomo Maccanti – g.maccanti@students.uninettunouniversity.net 24


ALGORITMI E PROGRAMMAZIONE AVANZATA - G.MACCANTI@STUDENTS.UNINETTUNOUNIVERSITY.NET

TIPI DI DATO ASTRATTI – PARTE 1: PILE E CODE


TIPI DI DATO ASTRATTI
La progettazione di un programma consiste di due fasi:
• Scelta di un algoritmo;
• Scelta di una adeguata struttura dati per:
o Codifica dei dati;
o Manipolazione delle informazioni.
La scelta della struttura dati è spesso la decisione più importante, poiché può influenzare in modo critico la
complessità dei programmi.

Tecnicamente, un tipo di dato, consiste nella definizione di:


• Un insieme di valori – sul quale si opera;
• Operazioni su questi valori.
Esempio: una stringa è un’entità che si può concatenare, troncare, etc.

Fino ad ora abbiamo visto due tipi di dato:


• Tipi di dato base: int, double, char, etc.;
• Tipi di dato definiti dall’utente: vettori e struct.

Vediamo, ora, un terzo tipo di dato, il tipo di dato astratti.

ADT (Abstract Data Type)


un tipo di dato astratto o ADT (Abstract Data Type) è un tipo di dato le cui istanze possono essere
manipolate con modalità che dipendono esclusivamente dalla semantica del dato e non dalla sua
implementazione.
In pratica separiamo nettamente definizione e implementazione, affinché le operazioni sui dati siano
accessibili solo tramite una opportuna interfaccia di programmazione.
Un vero ADT prevede operazioni che funzionino indipendentemente dall’implementazione del tipo.

IMPLEMENTARE UN ADT IN C - ESEMPIO


In C, un ADT, viene implementato nel seguente modo:
• Definizione del tipo usando l’istruzione typedef;
• Definendo tutte le funzioni necessarie ad eseguire le operazioni sui dati, per esempio:
inizializzazione, ordinamento, ecc.
Esempio di ADT - implementazione di un tipo contatore:
• Definizione del tipo: typedef int cont;
• Definizione delle operazioni: Reset(cont*) e Incrementa(cont*);

ADT PILA (STACK)


La pila è un ADT in cui l’inserimento e la cancellazione sono possibili solo in una specifica posizione,
chiamata cima (top) dello stack.
La pila è gestita seguendo un meccanismo di tipo LIFO (Last In First Out) ovvero l’ultimo elemento ad essere
inserito è anche il primo ad essere estratto. Pensiamo ad una pila di libri, uno sopra l’altro, è impensabile
affrontare la pila dal primo posato, quindi da quello sotto tutti gli altri, ma inizieremo dall’ultimo inserito,
quello più in alto e di facile accessibilità.

Giacomo Maccanti – g.maccanti@students.uninettunouniversity.net 25


ALGORITMI E PROGRAMMAZIONE AVANZATA - G.MACCANTI@STUDENTS.UNINETTUNOUNIVERSITY.NET

IMPLENTAZIONE DI UNA PILA TRAMITE VETTORE


L’implementazione di una pila tramite vettore è relativamente semplice.
Innanzitutto abbiamo bisogno di un vettore di N elementi allocati staticamente, poiché, come sappiamo, al
contrario delle liste, per i vettori dobbiamo conoscere in anticipo la quantità di dati su cui andremo a
lavorare. In secondo luogo, procederemo all’inserimento degli elementi nel vettore, dovremmo essere
attenti a rispettare le seguenti regole:
• Gli elementi vengono inseriti nel vettore a partire dall’indice 0;
• Un nuovo elemento viene inserito nell’elemento del vettore di indice successivo a quello
dell’ultimo elemento inserito.
Ricordiamoci, a questo punto, che una delle implementazioni fondamentali per la gestione delle liste era la
funzione top() che permetteva di ritornare la posizione dell’ultimo dato inserito. E’ logico pensare, quindi,
che per quanto riguarda la gestione tramite vettori, una variabile top indicherà sempre l’indice del vettore
contenente l’ultimo elemento e che quindi, ad ogni nuovo inserimento, sarà necessario aggiornare la
variabile (top).

Giacomo Maccanti – g.maccanti@students.uninettunouniversity.net 26


ALGORITMI E PROGRAMMAZIONE AVANZATA - G.MACCANTI@STUDENTS.UNINETTUNOUNIVERSITY.NET

Implementazione di una pila tramite vettore: definizione del tipo


Typedef struct{
Int top; //RAPPRESENTA L’ULTIMO ELEMENTO INSERITO, DI FATTO NE CONTIENE L’INDICE
Tipobase info[N]; //E’ IL VETTORE VERO E PROPRIO
}Stack;

N.B. sarà necessario definire una dimensione massima di N.

Abbiamo bisogno, infine, di una quantità o per meglio dire, un valore speciale, che ci permetta di
rappresentare uno stack vuoto:

#define VUOTO -1

Noi diciamo che lo stack è vuoto quando top = VUOTO.

PRIMITIVE DI GESTIONE DI UNA PILA


• void Inizializza(Stack *) – INIZIALIZZA LA PILA;
• int PilaVuota(Stack) – CI DICE SE LA PILA E’ VUOTA;
• int PilaPiena(Stack) – CI DICE SE LA PILA E’ VUOTA;
• int Top(Stack) – CI DICE QUAL E’ L’ELEMENTO IN CIMA ALLA LISTA (IL TOP) MA NON LO PRELAVA;
• void VisualizzaPila(Stack) – VISUALIZZA GLI ELEMENTI DELLA PILA;
• void Pop(Stack*) – ELIMINA UN ELEMENTO DALLA PILA;
• void Push(Stack*, Tipobase) – INSERISCE UN ELEMENTO NELLA PILA.

Passo lo stack per indirizzo (&s) quando ho bisogno di agire su di esso, quindi modificarne i valori

s = stack;
x= elemento dello stesso tipo di quelli presenti nella pila.

Inizializzazione di una pila: codice c


void Inizializza(Stack *s)
{
s->top = VUOTO;
}

INVOCAZIONE: Inizializza(&Stack);

Pila vuota: codice c


int PilaVuota(Stack s)
{
return(s.top == VUOTO); //SCRIVERE s.top O s->top E’ LA STESSA COSA
}

N.B. IL RETURN RITORNA IL VALORE DEL CONFRONTO SE s.top e VUOTO SONO UGUALI. IL TEST, NEL MAIN,
SARA’:
if(!PilaVuota(s)) //LEGGIAMO: SE LA PILA NON E’ PIENA
{
printf(“NON VUOTA!”);
}else{
printf(“VUOTA”);
}

INVOCAZIONE: PilaVuota(s);

Giacomo Maccanti – g.maccanti@students.uninettunouniversity.net 27


ALGORITMI E PROGRAMMAZIONE AVANZATA - G.MACCANTI@STUDENTS.UNINETTUNOUNIVERSITY.NET

Pila piena: codice c


int PilaPiena(Stack s) //MI VIENE PASSATO LO STACK
{
return(s.top==N-1); //SE IL top CORRISPONDE A N-1 (FACCIAMO FINTA CHE IO ABBIA 10
ELEMENTI, SAPPIAMO CHE IL VETTORE ANDREBBE A 0 A 9, QUINDI N-1) ALLORA HO RIEMPITO IL VETTORE,
QUINDI LA PILA
}

INVOCAZIONE: PilaPiena(s);

Top di una pila: codice c


int Top(Stack s) //MI VIENE PASSATO LO STACK
{
If(!PilaVuota(s)) //SE LA PILA NON E’ VUOTA
{
return(s.info[s.top]); //PRIMA PRENDO s.top CHE E’ L’INDICE E, SUCCESSIVAMENTE,
PRENDO L’INFORMAZIONE DEL VETTORE ASSOCIATA A QUELL’INDICE. IN PRATICA MI STO POSIZIONANDO
SULL’ELEMENTO DEL VETTORE, COME SE IO SCRIVESSI V[3] DOVE 3 E’ RAPPRESENTATO DA s.top E V[] DA
s.info.
}
}

INVOCAZIONE: Top(s);

N.B. Per stampare a video il Top(s) si può anche scrivere la funzione:


printf(“Top: %d\n”, Top(s));

Push di una pila: codice c


void Push(Stack *s, int x) //PASSO LO STACK PER INDIRIZZO POICHE’ LO VOGLIO MODIFICARE
{
If(!PilaPiena(*s)) //SE LA PILA NON E’ PIENA, QUINDI POSSO ANCORA INSERIRE ELEMENTI
{
s->top++; //FACCIO AVANZARE L’INDICE DI UNA POSIZIONE
s->Info[s->top]=x; //MEMORIZZO NEL VETTORE L’ELEMENTO CHE MI E’ STATO PASSATO
}
}

INVOCAZIONE: Push(&s, 5); //5 E’ IL VALORE CHE DESIDERO INSERIRE

Pop di una pila: codice c


void Pop(Stack *s) //PASSO LO STACK PER INDIRIZZO POICHE’ VOGLIO MODIFICARE
{
If(!PilaPiena(*s)) SE LA PILA NON E’ PIENA
{
s->top--; //CANCELLO L’ULTIMO ELEMENTO, BASTA DECREMENTARE IL VETTORE DI 1, POICHE’
SAPPIAMO CHE LA PILA HA UNA GESTIONE LIFO (LAST IN FIRST OUT) DOVE L’ULTIMO ELEMENTO AD ESSERE
INSERITO E’ IL PRIMO AD ESSERE ESTRATTO, IN QUESTO CASO L’ESTRAZIONE E’ SINONIMO DI CANCELLAZIONE
}
}

INVOCAZIONE: Pop(&s);

Visualizza elementi di una pila: codice c


void visualizzaPila(Stack s)
{
int i;

Giacomo Maccanti – g.maccanti@students.uninettunouniversity.net 28


ALGORITMI E PROGRAMMAZIONE AVANZATA - G.MACCANTI@STUDENTS.UNINETTUNOUNIVERSITY.NET

for(i=0; i<=s.top; i++)


{
printf("PILA: %d\n", s.Info[i]);
}
}

PROGRAMMA DI ESEMPIO: ADT PILA


#include <stdio.h>
#include <stdlib.h>
#define VUOTO -1
#define N 10

typedef struct {
int top;
int Info[N];
}Stack;

void Inizializza(Stack *s){


s->top = VUOTO;}

void visualizzaPila(Stack s){


int i;
for(i=0; i<=s.top; i++) {
printf("PILA: %d\n", s.Info[i]);}
Pop(&s);
}
int main(){
Stack s;
int prova = 0;
Inizializza(&s);
if (!PilaVuota(s)){
printf("PILA NON VUOTA\n");
}else{
printf("PILA VUOTA\n"); };
Push(&s, 5);
Push(&s, 3);
Push(&s, 2);
printf("Top: %d\n", Top(s));
visualizzaPila(s);
Push(&s, 1);
Push(&s, 12);
Push(&s, 33);
Push(&s, 244);
Push(&s, 255);
visualizzaPila(s);
Pop(&s);
visualizzaPila(s);
return 0;}

int PilaVuota(Stack s){


return(s.top == VUOTO);}

int PilaPiena(Stack s){


return(s.top == N-1);}

int Push(Stack *s, int x){


if(!PilaPiena(*s)) {

Giacomo Maccanti – g.maccanti@students.uninettunouniversity.net 29


ALGORITMI E PROGRAMMAZIONE AVANZATA - G.MACCANTI@STUDENTS.UNINETTUNOUNIVERSITY.NET

s->top++;
s->Info[s->top]=x;
}else{
printf("PILA SATURA\n");}}

int Top(Stack s){


if(!PilaVuota(s)) {
return(s.Info[s.top]); }}

int Pop(Stack *s){


if(!PilaVuota(*s)) {
s->top--; }}

IMPLENTAZIONE DI UNA PILA TRAMITE LISTA


L’implementazione di una pila tramite lista, invece, potrebbe risultare ancora più semplice, poiché abbiamo
a disposizione, di base, una serie di strumenti che ci permettono di emulare le primitive di base. Il top, per
esempio, potrebbe essere visto come la testa della lista, questo è facile da capire se pensiamo che il verso
di inserimento avviene sempre nello stesso senso a partire dalla testa della lista. Questo significa che le
operazioni Push() e Pop() non sono altro che operazioni di inserimento e cancellazione in testa alla lista.
Un altro vantaggio, rispetto all’implementazione tramite vettore, è che non dobbiamo più preoccuparci
dell’overflow dello stack. Prima avevamo un vettore allocato staticamente che poteva anche essere
occupato per intero; con la gestione tramite lista, invece, sappiamo che l’allocazione è dinamica e, quindi,
questo problema non si verifica. Per capirci meglio, la primitiva di PilaPiena() non è più necessaria.
Per implementare una pila tramite lista, si seguono i seguenti principi di base:
• Gli elementi della pila vengono memorizzati in una lista concatenata in ordine inverso: il primo
elemento inserito sarà in fondo alla lista, e la cima della pila in testa alla lista;
• Una pila ha un'unica variabile d'istanza, chiamata top. Concettualmente, top, è un puntatore al
primo elemento (testa) della lista;
• Una pila è vuota se e solo se top vale NULL;
• Quando si inserisce un elemento nella pila lo si mette in un nuovo nodo, che diventa il primo della
lista;
• Quando si toglie un elemento, si cambia top in modo da puntare all'elemento successivo;
• Tutte le operazioni hanno complessità costante nel caso pessimo, O(1).

Implementazione di una pila tramite lista: definizione del tipo


typedef struct elemPila{
TipoBase info;
struct elemPila *next;
}*stack;

ADT CODA (QUEUE)


La coda è un ADT il cui inserimento e cancellazione sono consentite solo in specifiche posizioni:
• L’inserimento è consentito solo ad un estremo, detto rear (parte posteriore - coda);
• L’estrazione è consentita solo all’altro estremo, detto front (parte anteriore – testa),
La coda ha una specifica gestione detta FIFO (First In First Out) ovvero, il primo elemento inserito è anche il
primo ad essere estratto. Pensiamo ad una normale coda alle poste, è ovvio che il primo arrivato allo
sportello (il primo che si è inserito) sarà il primo ad essere servito (estratto).

PRIMITIVE DI UNA CODA


• Inizializza(q) – INIZIALIZZA LA CODA;
• CodaVuota(q) – CI DICE SE LA CODA E’ VUOTA;

Giacomo Maccanti – g.maccanti@students.uninettunouniversity.net 30


ALGORITMI E PROGRAMMAZIONE AVANZATA - G.MACCANTI@STUDENTS.UNINETTUNOUNIVERSITY.NET

• Front(q) – CI DICE QUAL E’ IL PRIMO ELEMENTO DELLA CODA;


• Enqueue(q) – INSERISCE UN ELEMENTO NELLA CODA;
• Dequeue(q) – ELIMINA UN ELEMENTO DALLA CODA.

q = generica coda;
x= elemento dello stesso tipo di quelli presenti nella coda.

L’implementazione di una coda può avvenire tramite vettore o tramite lista.

IMPLEMENTAZIONE DI UNA CODA TRAMITE VETTORE


Implementando un ADT coda con l’ausilio dei vettori è ovvio, quindi, che ci occorrerà un vettore. Avremo,
in definitiva, una struct con un vettore di N elementi e, questa volta, ben due indici poiché dobbiamo
memorizzare sia il front (da dove prelevo) che il rear (da dove inserisco). La presenza di questi due indici
complica lievemente l’implementazione poiché i miei elementi vengono memorizzati nelle posizioni di
indice che vanno da front a rear.
Immaginiamo per un momento di avere una coda senza elementi, quindi sia front che rear saranno
inizializzati a VUOTO, successivamente inizio a valorizzare il mio vettore, come nel caso in figura:

Ad un certo punto, poi, elimino uno degli elementi, il primo (ricordiamo la gestione FIFO), come nel caso in
figura:

Vediamo subito che sia il front che il rear, all’interno del nostro vettore, hanno creato un buco, poiché ho 6
posizioni, quattro elementi, e due posti disponibili.
Bene, ora aggiunto un ulteriore elemento:

Teoricamente ora ho riempito la coda, giusto? Sì, perché se pensiamo che la coda sia piena quando rear =
N-1, allora è vero. Visivamente, però, vediamo che ho un buco libero, buco che potrebbe essere sfruttato
per inserire un altro elemento.
Per fare questo (inserire a ruota gli elementi) abbiamo bisogno dell’operatore %, quello che ci ha permette
di raccogliere il resto di un’operazione. In questo modo, essendo il rear = 5 (poiché abbiamo riempito tutti i
buchi dal front alla fine del vettore), aggiungerò un nuovo elemento, quindi 5+1 = 6, essendo che il nostro
vettore è di N elementi (in questo caso 6), avrò che rear/N = 0 (6/6), quindi ricomincerò da capo con
l’inserimento di un nuovo elemento. Forzerò il rear all’elemento 0, ed inserirò lo stesso nell’apposito spazio
del vettore. Non ci facciamo ingannare, l’elemento in testa non è 2, ma è -1 poiché è indicato dal nostro
front.

Giacomo Maccanti – g.maccanti@students.uninettunouniversity.net 31


ALGORITMI E PROGRAMMAZIONE AVANZATA - G.MACCANTI@STUDENTS.UNINETTUNOUNIVERSITY.NET

Tutte le volte che faccio Enqueue() incrementerò il rear e tutte le volte che faccio Dequeue() incrementerò il
front; sempre utilizzando l'operatore modulo, per cui 5+1 = 0 (5+1 = 6 -> 6 mod 6 = 0).

Implementazione di una coda tramite vettore: definizione del tipo


Typedef struct{
int front; //RAPPRESENTA IL FRONT DELLA NOSTRA CODA
int rear; //RAPPRESENTA LA PARTE POSTERIORE DELLA NOSTRA CODA
Tipobase info[N]; //E’ IL VETTORE VERO E PROPRIO
}queue;

N.B. sarà necessario definire una dimensione massima di N.

Abbiamo bisogno, infine, di una quantità o per meglio dire, un valore speciale, che ci permetta di
rappresentare uno stack vuoto:

PRIMITIVE DI GESTIONE DI UNA CODA

Inizializzazione di una coda: codice c


void InizializzaCoda(queue *q){
q->front = q->rear = CODAVUOTA;}

INVOCAZIONE: InizializzaCoda(&q);

Coda vuota: codice c


int CodaVuota(queue q)
{
return(q.front==CODAVUOTA);
}

INVOCAZIONE: CodaVuota(q);

Coda piena: codice c


int CodaPiena(queue q)
{
return(q.front==((q.rear+1)%N)); //E’ PIENA QUANDO IL FRONT E IL REAR SONO UGUALI. IN
PRATICA, QUANDO ABBIAMO CHE L’ELEMENTO DOPO AL REAR, E’ IL FRONT STESSO
}

INVOCAZIONE: CodaPiena(q);

Enqueue: codice c
void Enqueue(queue *q, int val)
{
if(!CodaPiena(*q)) //SE LA CODA NON E’ PIENA...
{
q->rear=(q->rear+1)%N; //IN PRATICA FACCIO AVVANZARE REAR DI UNA POSIZIONE
q->Info[q->rear]=val; //ASSOCIO IL VALORE AL VETTORE

Giacomo Maccanti – g.maccanti@students.uninettunouniversity.net 32


ALGORITMI E PROGRAMMAZIONE AVANZATA - G.MACCANTI@STUDENTS.UNINETTUNOUNIVERSITY.NET

if (q->front==CODAVUOTA) //SE E’ IL PRIMO INSERIMENTO...


{
q->front=q->rear; //PONGO IL FRONT UGUALE AL REAR
}
}
}

INVOCAZIONE: Enqueu(&q, val); //VAL E’ IL VALORE CHE VOGLIO INSERIRE NELLA PILA, TIPO 5 O 6

Vengono utilizzate due variabili, front e rear, che indicano rispettivamente la posizione del primo elemento
della coda e dell'ultimo elemento della coda.
Inizialmente, quando la coda è vuota si assume che front=rear=-1.
Quando viene inserito il primo elemento, rear viene posto a 0 e l'elemento viene inserito nel vettore nella
componente di indice rear (ossia 0). Anche front viene assegnato a 0, in quanto è necessario rappresentare
la presenza dell’unico elemento in coda.
Quando un nuovo elemento deve essere inserito successivamente, rear viene incrementato di 1 e
l'elemento viene inserito nella nuova posizione fornita da rear. Si noti che l'incremento dell’indice rear è
circolare.

Dequeue: codice c
void Dequeue(queue *q){
if(!CodaPiena(*q)){
if(q->front==q->rear){
InizializzaCoda(q);
}else {
q->front=(q->front+1)%N; } } }

INVOCAZIONE: Dequeu(&q);

Per quanto riguarda la cancellazione dell'elemento di posizione front, essa viene semplicemente realizzata
incrementando la variabile front. Dopo aver effettuato la cancellazione, è ovviamente necessario verificare
che la coda non diventi vuota, perché in tal caso bisogna aggiornare la variabile rear. Se la coda diviene
vuota, sia front che rear divengono pari a -1. La condizione di coda vuota viene controllata prima della
cancellazione dell'elemento front. Se front e rear sono coincidenti, significa che la coda possiede un solo
elemento, cioè quello che deve essere eliminato. Ciò significa che dopo l'inserimento la coda diverrà vuota.
L’incremento della variabile front deve avvenire in modo circolare, analogamente a quanto visto per la
variabile rear. Ad esempio si consideri la seguente cancellazione di un elemento.

Giacomo Maccanti – g.maccanti@students.uninettunouniversity.net 33


ALGORITMI E PROGRAMMAZIONE AVANZATA - G.MACCANTI@STUDENTS.UNINETTUNOUNIVERSITY.NET

TIPI DI DATO ASTRATTI – PARTE 2: CODE A PRIORITA’ E ALBERI


ADT CODA A PRIORITA’
E’ una normalissima coda, solo che appare, in aggiunta, la parola “priorità”; infatti è una coda sulla quale è
definita una funzione di priorità p(S). Questo significa, ed implica, una diversa semantica rispetto alla coda
tradizionale. Nella coda tradizionale noi abbiamo un verso di inserimento e di prelievo obbligato
(front/testa per il prelievo e rear/coda per l’inserimento) qui, invece, l’inserimento o il prelievo avvengono
secondo una determinata priorità:
• Prelievo in testa, ma inserimento in base alla priorità;
• Inserimento in coda, ma prelievo in base alla priorità.
Questo ci permette di sottolineare i suoi vantaggi e svantaggi:
• Rilevane nelle applicazioni pratiche;
• Non tutti gli elementi hanno in genere la stessa importanza (clienti VIP, processo di un OS, ecc).

HEAP
Lo heap è una struttura dati in cui le chiavi sono organizzate come un “albero” binario (organizzazione degli
oggetti in ordine gerarchico o a seconda dei livelli; binario poiché ad ogni nodo ci sono poi due diversi rami
collegati). Sono rispettate alcune proprietà legate ai valori e alla struttura dell’albero:
• Proprietà sui valori (heap property);
• Proprietà sulla struttura (shape property).

Heap Property

E’ rigorosamente basata su un albero binario e, ci sono due gestioni dello heap:


• Max heap: se B è figlio di A allora la chiave di A è maggiore o uguale alla chiave di B;
• Min heap: se B è figlio di A allora la chiave di B sarà minore o uguale alla chiave di B;
Questo impone un ordinamento parziale.

Shape Property
La proprietà dice che lo heap è un albero completo ad ogni livello tranne (eventualmente) l’ultimo.

Giacomo Maccanti – g.maccanti@students.uninettunouniversity.net 34


ALGORITMI E PROGRAMMAZIONE AVANZATA - G.MACCANTI@STUDENTS.UNINETTUNOUNIVERSITY.NET

Come possiamo vedere, dalla figura, l’albero è effettivamente uno heap poiché rispecchia sia la heap
property che la shape property, infatti l’ultimo livello non è completo per il ramo di destra ma, comunque,
ai livelli superiori lo è.

Heap: a cosa serve?


• L’elemento con chiave massima è nella radice (Min heap sarà l’elemento più piccolo nel Max heap il
più grande);
• Ogni nodo ha un valore di chiave >= dei suoi discendenti (comodo quando inseriamo in una coda a
priorità);
• Se l’albero è completo eccetto l’ultimo livello, è ben bilanciato, quindi memorizza n elementi e ha
altezza ℎ = #$%& ' (ci da l’idea del costo dell’operazione);
Come abbiamo detto, uno heap è implementato tramite vettore; in particolare il vettore ci permette di
trovare, da un generico nodo i, i figli che saranno in posizione 2i (sinistro) e 2i+1 (destro).

Ricordiamo che l’albero serve solo per visualizzare le proprietà dello heap, ma fisicamente questa struttura
non esiste, nemmeno in memoria, poiché viene implementata tramite vettore.

PRIMITIVE DI GESTIONE DI UNA CODA A PRIORITA’


• Inizializza(q) – INIZIALIZZA LA CODA;
• CodaVuota(q) – CI DICE SE LA CODA E’ VUOTA;
• Maximum(q) – LEGGE IL VALORE CON MASSIMA PRIORITA’ DALLA CODA;
• ExtractMax(q) – ESTRAE IL VALORE CON MASSIMA PRIORITA’ DALLA CODA;
• Insert(q) – INSERISCE UN ELEMENTO IN CODA;
• Heapify – INVOCA LO HEAPIFY;
• BuildHeap – INVOCA IL BUILDHEAP.

q = generica coda;
x= elemento dello stesso tipo di quelli presenti nella coda.

L’implementazione di una coda può avvenire tramite vettore o tramite lista ma, nella pratica, si usa una
struttura dati detta heap che si presta ad una implementazione tramite vettori.
• Heapify: impone la heap property:
o Si applica ad un generico elemento i;
o Assume che i due sottoalberi siano, a loro volta, già degli heap;
• BuildHeap: dato un generico vettore A[1,…,n] lo converte in uno hea;

Heapify: codice c
Heapify(A,i) //A E’ IL VETTORE CHE HA LO HEAP E i IL NODO SU CUI APPLICHAIMO HEAPIFY: A[i]
{

Giacomo Maccanti – g.maccanti@students.uninettunouniversity.net 35


ALGORITMI E PROGRAMMAZIONE AVANZATA - G.MACCANTI@STUDENTS.UNINETTUNOUNIVERSITY.NET

l <- left(i) //RAPPRESENTA FIGLIO SINISTRO


r <- right(i) //RAPPRESENTA FIGLIO DESTRO
m <- max(A[i],A[l],A[r]) //CONFRONTA GLI INDICI DEL NODO, DEL FIGLIO DI SINISTRA E DEL
FIGLIO DI DESTRA E LO MEMORIZZANO IN M
largest <- index of m //LARGEST E’ L’INDICE PIU’ GRANDE, DEL VALORE M
if(largest != i) //SE i, IL NODO SU CUI DEVO APPLICARE LO HEAPIFY, DEVE DIVENTARE UNO
HEAP, DOVRO’ APPLICARE LA HEAPPROPERTY
{
SWAP(A[i],A[largest]); //SCAMBIO L’INDICE DEL VALORE PIU’ GRANDE CON QUELLO CHE GLI
COMPETE
Heapify(A, largest); //REINVOCO LA HEAPIFY SU QUESTO NODO
}
}

Heapify: esempio

Notiamo che il valore 4 in posizione 2, è più piccolo dei suoi


sottoalberi, sia destro che sinistro; non è uno heap quindi. Però,
allo stesso modo, i sottoalberi 14 e 7 sono più grande dei loro
sottoalberi 2, 8 e 1, quindi sono degli heap. Posso applicare la
heapify all’elemento 2

Lo heapify sceglie il più grande, ovviamente 14, dopodiché


scambialo con 4. Una volta che lo scambio è avvenuto, quindi gli
elementi sono stati invertiti.

Ora vediamo, però, che i due sotto alberti di 4, quindi 2 e 8, non


sono più uno heap; è necessario, quindi, applicare lo heapify al
nodo 4.

Chi è il più grande tra 4, 2 e 8? Ovviamente 8, quindi ecco che lo


heapify scambia gli elementi portando il valore con indice maggiore
in testa al nodo e riposiziona i sottoalberi più piccoli.

Giacomo Maccanti – g.maccanti@students.uninettunouniversity.net 36


ALGORITMI E PROGRAMMAZIONE AVANZATA - G.MACCANTI@STUDENTS.UNINETTUNOUNIVERSITY.NET

BuildHeap: codice c
Come abbiamo già detto, l’operazione BuildHeap permette di costruire uno heap partendo da un generico
vettore A[1,…,n]. Questo, avviene secondo le seguenti regole:
• Applica Heapify su tutti i nodi interni a partire dal basso;
• Utile nel contesto degli algoritmi di ordinamento.

BuildHeap(A) //A E’ UN GENERICO VETTORE


{
N<-lenght[A]
For(i<-[n/2] downto 1
Heapify(A,i)
}

CODE A PRIORITA’ E HEAP


Vediamo come le code a priorità possono godere della funzionalità apportate da uno heap attraverso le
due primitive principali:
• ExtractMax(q);
• Insert(q,x);

Estrazione del massimo in una coda a priorità: codice c


ExtractMax(A)
{
If CodaVuota(A)
{
error “heap underflow”
}
max = A[1] //ESTRAGGO IL MASSIMO, SAPPIAMO CHE IN UN MAXHEAP E’ SEMPRE IL PRIMO ELEMENTO
A[1] = A[n] //PORTO L’ULTIMO ELEMENTO DEL VETTORE IN POSIZIONE 1 (TANTO A[1] E’ SALVATO)
n<-n-1 //DECREMENTO IL VETTORE, DI FATTO CANCELLO L’ULTIMO ELEMENTO (SPOSTATO IN POS. 1)
Heapify(A,1) //APPLICO LA HEAPIFY SULL’ELEMENTO 1, SAPENDO CHE SI RIPERCUOTE SU TUTTI
return max
}

Inserimento in una coda a priorità: codice c


Lo insert, funziona come segue:
• Aggiunge un nuovo nodo come foglia – in fondo;
• Percorre un cammino dalla foglia verso la radice per trovare il punto appropriato in cui inserire
utilizzando la heap property.
Insert(A, Key)
{
n n+1 //INCREMENTO n, POICHE’ STIAMO AGGIUNGENDO UN ELEMENTO
i = n //i VIENE INIZIALIZZATO A n, POICHE’ ABBIAMO UN ELEMENT IN PIU’
while (i>1 and A[Parent(i)] < key){ //FINCHE’ i>1 (NON SIAMO ARRIVATI ALLA RADICE) E
FINCHE’ Parent(i) (PARENT E’ IL NOSTRO PREDECESSORE, IL NODO A CUI E’ ATTACCATO IL CERTICE) E’
PIU’ PICCOLO DELLA NOSTRA CHIAVE, DEVE NAVIGARE ALL’INTERNO DELL’ALBERO
A[i] = A[Parent(i)] //LI SCAMBIAMO: A[i] PRENDE L’INDICE DEL SUO PREDECESSORE
i = Parent(i)}
A[i] = key //QUANDO USCIAMO, IN A[i] POSSO COPIARE IL VALORE DELLA CHIAVE
}

Giacomo Maccanti – g.maccanti@students.uninettunouniversity.net 37


ALGORITMI E PROGRAMMAZIONE AVANZATA - G.MACCANTI@STUDENTS.UNINETTUNOUNIVERSITY.NET

Inserisco un nuovo elemento nel vettore, questo nuovo elemento


non è sedici, semplicemente è un nuovo elemento.

7 è il mio parent, li confronto, essendo che 16 è maggiore di 7,


sposto sette.

Dopo aver copiato sette, sposto il mio indice al livello superiore,


dove c’era il mio parent.

Rieseguo il confronto: tra 16 e 14, qual è l’elemento maggiore?


Ovviamente ancora 16

Copio 14 e mi sposto di una posizione; tra 16 e 18, qual è il


maggiore? 18, che è già al suo posto quindi esco dal ciclo e
posiziono 16.

Giacomo Maccanti – g.maccanti@students.uninettunouniversity.net 38


ALGORITMI E PROGRAMMAZIONE AVANZATA - G.MACCANTI@STUDENTS.UNINETTUNOUNIVERSITY.NET

ADT ALBERO
Abbiamo già capito come è formato un albero poiché lo abbiamo visto quando abbiamo parlato delle
ricorsioni e dello heap. Per ora, dobbiamo vederlo come una collezione di oggetti che ha le seguenti
proprietà:
• Molte informazioni sono strutturare in maniera gerarchie;
• Organizzate su più livello (organigrammi, alberi genealogici, ecc.).

L’elemento più caratterizzante di un albero, è il suo grado. Il grado di un albero ci dice quanti discendenti
può avere un dato nodo.

ADT albero: codice c


struct EA{
TipoElemento info;
PElemAlbero Figli[N];
};

typedef struct EA ElemAlbero;


typedef ElemAlbero *PElemAlbero;

ADT albero binario: codice c


struct EA{
TipoElemento info;
PElemAlbero Sinistro, Destro;
};

ADT albero: operazioni


La semantica dipende dalla relazione che regola la gerarchia!!! Ad esempio, come si inserisce in un albero
dipende dalla relazione tra le chiavi.

Un esempio di regole per la gerarchia: alberi binari “ordinati” o di ricerca


Per ogni nodo con chiave x
Nodi nel sottoalbero di sinistra hanno chiavi x
Nodi nel sottoalbero di destra hanno chiavi x

Giacomo Maccanti – g.maccanti@students.uninettunouniversity.net 39


ALGORITMI E PROGRAMMAZIONE AVANZATA - G.MACCANTI@STUDENTS.UNINETTUNOUNIVERSITY.NET

ALGORITMI E PROGRAMMAZIONE AVANTAZA – PARTE II – ALGORITMI


CONCETTO DI ALGORITMO
Un algoritmo è un procedimento che consente di ottenere un risultato atteso eseguendo, in un
determinato ordine, un insieme finito di passi semplici corrispondenti ad azioni scelte solitamente da un
insieme finito.

In riferimento ai programmi, un algoritmo è una sequenza di istruzioni che:


• Opera su dei dati;
• Riceve dei valori in ingresso;
• Genera dei valori in uscita;
• Termina dopo un numero finito di passi.

PERCHE’ SONO IMPORTANTI?


• Per motivi pratici: la maggior parte dei problemi pratici richiede algoritmi efficienti;
• Per motivi tecnologici: gli algoritmi possono essere implementati come programmi.
ESEMPI DI ALGORITMI:
1. Ordinare un insieme di oggetti;
2. Trovare un elemento in un insieme di oggetti;
3. Trovare il percorso più breve tra due destinazioni.

NON SOLO ALGORITMI


Quando parliamo di programmi, quindi, stiamo facendo riferimento ad algoritmi, in genarle, ma anche a
strutture dati complesse che ci permettono di risolvere un problema ben specifico.
Programmi = algoritmi + strutture dati

N.B. La scelta della struttura dati può influire sull’efficienza di un algoritmo; es: accedere ad un elemento.

PROGETTO DI UN ALGORITMO
Come facciamo ad “inventare” un algoritmo?
La struttura del problema da risolvere può essere usata per orientare il progetto dell’algoritmo.
Gli algoritmi sono tipicamente classificati secondo un paradigma metodologico, cioè una stratega
algoritmica generale.

PARADIGMI ALGORITMICI

Paradigmi generali
• Divide and conquer;
• Ricerca ed enumerazione.

Paradigmi per problemi di ottimizzazione


• Programmazione dinamica;
• Paradigma greedy.

Giacomo Maccanti – g.maccanti@students.uninettunouniversity.net 40


ALGORITMI E PROGRAMMAZIONE AVANZATA - G.MACCANTI@STUDENTS.UNINETTUNOUNIVERSITY.NET

DIVIDE AND CONQUER (DIVIDE-ET-IMPERA) – DIVIDI E RISOLVI


Il problema viene suddiviso in istanza di dimensione più piccola dello stesso problema, le quali vengono
risolte ricorsivamente.

RICERCA ED ENUMERAZIONE
La (o una) soluzione al problema è ottenuta tramite una ricerca all’interno di uno spazio delle soluzioni.

PROGRAMMAZIONE DINAMICA
E’ un paradigma molto potente ma non sempre applicabile; si utilizza in presenza di certe caratteristiche.
La soluzione ottima al problema è ottenuta dalla soluzione ottime ai vari SOTTOPROBLEMI.

PARADIGMA GREEDY (AVIDO)


La soluzione ottima al problema è ottenuta concatenando le soluzioni ottime ai SOTTOPROBLEMI.

N.B. Decisione locale!

Giacomo Maccanti – g.maccanti@students.uninettunouniversity.net 41


ALGORITMI E PROGRAMMAZIONE AVANZATA - G.MACCANTI@STUDENTS.UNINETTUNOUNIVERSITY.NET

ANALISI DEGLI ALGORITMI


Analisi di un algoritmo, significa prevedere le risorse richieste da un algoritmo:
• Tempo di esecuzione;
• Quantità di memoria;
• Altre risorse (spazio su disco, porte di comunicazione, risorse software, ecc.).

In questo corso, il tempo di esecuzione, è il parametro fondamentale.

Ci sono, inoltre, alcuni parametri che influenzano l’analisi:


• Dimensione dell’input;
• Valori di input;
• Altro (modello di esecuzione, ecc.).

Quello che faremo, in modo particolare, è studiare in maniera semi-formale (analisi asintotica) la
complessità degli algoritmi.

ANALISI DI COMPLESSITA’: DEFINIZIONI


COMPLESSITA’: costo in termini di:
• Tempo di esecuzione T(n) – Time of n;
• Memoria S(n) – Storage of n.
n = grandezza del nostro INPUT.
ES: in un programma di ordinamento n sarà il numeri di elementi dell’insieme.
REQUISITI
Chiaramente il nostro strumento di analisi deve avere alcuni requisiti:
• METRICA NEUTRALE: esso, infatti, deve essere totalmente indipendente dal tipo di elaboratore. Per
questo non possiamo dare risposte in termini di millisecondi o secondi nell’esecuzione di un
algoritmo, poiché, come è ovvio, questi avranno tempistiche dipendenti dal supporto che le
esegue.
• COMPLESSITA’ ASINTOTICA: è un requisito legato alla dimensione dei dati. Non ci interessa la
velocità di esecuzione su quantità di dati piccoli, ma ci interessa quanto e come si comporta per
valori grandi di n (n->∞ letto n che tende a infinito).

ASINTOTICO: Nel linguaggio scientifico, ciò che tende ad avvicinarsi sempre più a qualcosa senza mai
raggiungerla o coincidere con essa.

Idea di fondo: O, Ω , Θ rappresentano rispettivamente ≤ , ≥ , =.

Giacomo Maccanti – g.maccanti@students.uninettunouniversity.net 42


ALGORITMI E PROGRAMMAZIONE AVANZATA - G.MACCANTI@STUDENTS.UNINETTUNOUNIVERSITY.NET

IMPORTANZA DELLA COMPLESSITA’

IN QUESTA RIGA ABBIAMO LA GRANDEZZA


IN QUESTA COLONNA ABBIAMO DIVERSE
FUNZIONI CHE RAPPRESENTANO IL DEI NOSTRI DATI IN INPUT
POSSIBILE ANDAMENTO DELLA FUNZIONE
T(N)

Vediamo, in questa tabella, il tempo di esecuzione ottenuto in secondi T(n) per un certo generico algoritmo.
Adesso assumiamo che, l’elaboratore su cui stiamo lavorando, impieghi per un problema di dimensione 1,
quindi T(1), un micro secondo – T(1) = 1µS.

Semplicemente, questa tabella, ci dice come scala il tempo di esecuzione all’aumentare della quantità dei
dati in INPUT.
Notiamo infatti, come un algoritmo poco efficiente, non solo è molto lento, ma non è nemmeno in grado di
beneficiare dell’evoluzione tecnologica.

ANALISI DI COMPLESSITA’
La notazione asintotica è lo strumento che noi utilizziamo per analizzare in maniera semi-formale la
complessità degli algoritmi.

La notazione asintotica prevede tre notazioni standard:


1. O (letto O grande): fornisce un limite lasco, rispettivamente superiore;
2. Ω (letto omega grande) fornisce un limite lasco, rispettivamente inferiore;
3. Θ (letto teta grande): fornisce un limite stretto.

N.B. Ο,Ω,Θ identificano classi di funzioni, quindi è corretto scrivere la seguente notazione:
f(n) = O(g(n))

FUNZIONE LIMITATA SUPERIORIMENTE


Una funzione limitata superiormente è una funzione le cui immagini ammettono estremo
superiore finito. Quando abbiamo a che fare con una funzione limitata superiormente
possiamo tracciare una retta parallela all’asse x tale che il grafico delle funzione stia tutto
sotto di essa.

FUNZIONE LIMITATA INFERIORMENTE


Una funzione è limitata inferiormente se la sua immagine è limita inferiormente (cioè
ammette inf limitato).

Giacomo Maccanti – g.maccanti@students.uninettunouniversity.net 43


ALGORITMI E PROGRAMMAZIONE AVANZATA - G.MACCANTI@STUDENTS.UNINETTUNOUNIVERSITY.NET

FUNZIONE LIMITATA SUPERIORMENTE INFERIORMENTE


Una funzione limitata è una funzione limita sia superiormente che inferiormente. In
questo caso possiamo disegnare sopra e sotto la funzione due rett parallele all’asse x
tali da circoscrivere il grafico.

NOTAZIONE O (“O GRANDE”)


f(n) = O(g(n)) iff ∃ c>O e nO ≥ O tale che f(n) ≤ c°g(n) ∀n ≥ nO
Si dice che una funzione f(n) è O GRANDE di un’altra funzione g(n) se e solo se esiste una costante C
positiva e un valore n zero maggiore o uguale a zero tale che f(n) è minore a c per g(n) per qualunque n
maggiore o uguale a n zero.

La notazione O non fornisce un limite stretto.

L’obiettivo in generale, quindi, è quello di cercare il limite superiore più stretto possibile; altrimenti la
nostra analisi è troppo approssimata.

T(n) = O(g(n)) significa che il tempo di esecuzione, anche nel caso peggiore, è limitato superiormente da
g(n).

NOTAZIONE Ω (“OMEGA GRANDE”)


f(n) = Ω(g(n)) iff ∃ c>O e nO ≥ O tale che f(n) ≥ c°g(n) ∀n ≥ nO
Si dice che una funzione f(n) è OMEGA GRANDE di una funzione g(n) se e solo se esiste una costante C
positiva e n0 maggiore o uguale a 0 tale per cui f(n) è maggiore o uguale a C per g(n) per qualunque n
maggiore di n0.

Quello che vediamo è che la nostra f(n) per valori di n maggiori di n0 (quindi dimensioni grandi) è limitata
inferiormente da c*g(n).

T(n) = Ω(g(n)) significa che il tempo di esecuzione, anche nel caso migliore, è limitato inferiormente da g(n).

NOTAZIONE (“TETA GRANDE”)


f(n) = Θ(g(n)) iff ∃ c1,c2>O e n0 ≥ O tali che c1°g(n) ≤ f(n) ≤ c2°g(n) ∀n ≥ nO
Si diche che una funzione f(n) è TETA GRANDE di g(n) se e solo se esistono due costanti C1 e C2 maggiori di
zero e n0 maggiore o uguale di 0 tali che f(n) è compresa da c1 per g(n) e c2 per g(n) per ogni n maggiore o
uguale di n0.

Giacomo Maccanti – g.maccanti@students.uninettunouniversity.net 44


ALGORITMI E PROGRAMMAZIONE AVANZATA - G.MACCANTI@STUDENTS.UNINETTUNOUNIVERSITY.NET

Se f(n) = Θ(g(n)) allora f(n) = O(g(n)) e f(n) = Ω(g(n))

T(n) = Θ(g(n)) significa che nel caso peggiore è O(g(n)) e nel caso migliore è Ω(g(n)) (in pratica non vi è
distinzione fra tempo di esecuzione nel caso peggiore e migliore).
Teta grande esplica il concetto di uguaglianza asintotica.

Giacomo Maccanti – g.maccanti@students.uninettunouniversity.net 45


ALGORITMI E PROGRAMMAZIONE AVANZATA - G.MACCANTI@STUDENTS.UNINETTUNOUNIVERSITY.NET

PROPRIETA’ DELLA NOTAZIONE ASINTOTICA


Analogica con confronti numerici:
• f(n) = O(g(n)) ≈ a ≤ b – quando una funzione è O di un’altra funzione g(n) è come dire che f(n) è <= di
g(n);
• f(n) = Ω(g(n)) ≈ a ≥ b – quando una funzione è Ω di un’altra funzione g(n) è come dire che f(n) è >= di
g(n);
• f(n) = Θ(g(n)) ≈ a = b – quando una funzione è Θ di un’altra funzione g(n) è come dire che f(n) è = g(n).

Transitività
f(n) = Θ(g(n)) e g(n) = Θ(h(n)) à f(n) = Θ(h(n))
Abbiamo detto che Θ equivale ad una uguaglianza quindi, se sappiamo che f(n) = Θ(g(n)) e sappiamo che
g(n) = Θ(h(n)), essendo che rappresentano una uguaglianza, possiamo tranquillamente dire che f(n) =
Θ(h(N)).

La proprietà vale anche per la notazione O e Ω.

Simmetria
f(n) = Ο(g(n)) ßàg(n) = Ω(f(n))

Simmetria trasposta
f(n) = Ο(g(n)) à g(n) = Ω(f(n))

Riflessività
Tutte tre le notazioni godono della proprietà riflessiva:
f(n) = Θ(f(n))
f(n) = Ο(f(n))
f(n) = Ω(f(n))

Somma e massimo
In questo caso stiamo valutando il costo asintotico di una somma di funzioni.

F1(n) + ... fm(n) à Θ(max(f1(n),…,+fm(n)))

Esempio:
3'& + ' + 7 = Θ 3& = Θ(n& )

COMPLESSITA’ DELL’INSERTIONSORT
Ora vedremo un esempio di calcolo di complessità asintotica di un algoritmo; lo faremo con un esempio.
Es: algoritmo di ordinamento per inserimento (InsertionSort).

Giacomo Maccanti – g.maccanti@students.uninettunouniversity.net 46


ALGORITMI E PROGRAMMAZIONE AVANZATA - G.MACCANTI@STUDENTS.UNINETTUNOUNIVERSITY.NET

Vedete che abbiamo un vettore e, ad ogni iterazione, l’algoritmo provvede a spostare i vari elementi
posizionandoli nel giusto ordine. Ovviamente, ad ogni ciclo, sarà necessario un numero indefinito di
confronti e di scambi.
Ora traduciamo l’algoritmo in pseudocodice; pseudocodice significa che abbiamo, a grandi linee, lo
scheletro del programma ma che, ancora, non è stato tradotto nella sintassi definitiva. Questo basta, ad
ogni modo, per farci un’idea generale e proseguire con la nostra analisi asintotica.
PSEUDOCODICE:
1 for j 2 to n do //for(j=2; j<n; j++)
2 key A[j] //METTO L’ELEMENTO A[j] NELLA VARIABILE
3 //inserisci A[j] nella sequenza A[1...j-1]
4 i j – 1 //i = j-1
5 while i > 0 and A[i] > key do //while(i> 0 && A[i] > Key){
6 A[i+1] A[i] //A[i+1] = A[i]
7 i (i – 1) //i = (i-1)
8 A[i+1] key //A[i+1] = key
9 [j (j + 1)] //j = (j+1)

In questa tabella possiamo vedere il nostro pseudocodice, numerato da 1 a 9, il costo (è il tempo di


esecuzione impiegato dall’istruzione) e n. di volte che rappresenta il numero di iterazioni che vengono
eseguite:
Viene eseguita n-1 volte
La riga 5 è più complicata,
perché il corpo di un ciclo
poiché non è come il ciclo
viene eseguito sempre
for che è sistematica e
una volta di meno.
prevedibile; per poterne
Vediamo, infatti, che
calcolare il costo,
l’istruzione è dentro ad un
dobbiamo fare delle
ciclo di for (inizio a riga 1)
assunzioni sui valori.

Diciamo quindi che il Sempre perché siamo


tempo di j è equivalente al all’interno di un ciclo,
numero di volte che la iteriamo n-1 volte; in
riga 5 viene eseguita per questo caso j-1.
un certo j

COME POSSO VALUTARE LA COMPLESSITA’ DI UN ALGORITMO?


Sommando tutti i contributi, otteniamo che:
2 ' =1+4∗'+6∗ '−1 +8∗ 9: +?∗ (9: − 1)
;<&…> ;<&…>
Dove abbiamo che:
a=8@ // TUTTI I TERMINI ITERATI N VOLTE
Giacomo Maccanti – g.maccanti@students.uninettunouniversity.net 47
ALGORITMI E PROGRAMMAZIONE AVANZATA - G.MACCANTI@STUDENTS.UNINETTUNOUNIVERSITY.NET

b=(8& + 8A +8B + 8C ) //TUTTI I TERMINI ITERATI n-1 VOLTE


c=8D //SOMMATORIA DI Tj ITERATA n VOLTE
d=8E + 8F //SOMMATORIA DI Tj ITERATA n-1 VOLTE

ESSENDO CHE VOGLIAMO RAGGRUPPARE:


2 ' = 1−6+? + 4+6−? ∗'+ 8+? ∗ 9:
;<&…>

2 ' =G+H∗'+%∗ 9:
;<&…>

La complessità dipende da tj

Caso migliore: vettore già ordinato

Caso peggiore: vettore ordinato in ordine inverso

Complessità dell’Insertion Sort: tempo di esecuzione.


CASO OTTIMO: il caso ottimo è quello in cui la sequenza di partenza è già ordinata; in questo caso
l’algoritmo ha tempo di esecuzione Ꝋ(n).
CASO PEGGIORE: il caso peggiore è quello in cui la sequenza di partenza è in ordine inverso rispetto quella
di ordinamento; questo significa che bisognerà spostare tutti gli elementi al loro “posto”. In questo caso si
ha tempo di esecuzione Ꝋ(n^2).
CASO MEDIO: il caso medio è come quello peggiore, poiché dovrò eseguire n iterazioni per spostare gli
elementi nell’ordine esatto. In questo caso si ha tempo di esecuzione Ꝋ(n^2).

Giacomo Maccanti – g.maccanti@students.uninettunouniversity.net 48


ALGORITMI E PROGRAMMAZIONE AVANZATA - G.MACCANTI@STUDENTS.UNINETTUNOUNIVERSITY.NET

ANALISI DI PROGRAMMI RICORSIVI: RICORRENZE


Abbiamo visto, fino ad ora, come il calcolo per gli algoritmi abbia funzionato abbastanza bene per quanto
riguarda quelli iterativi, quindi tutti quegli algoritmi che eseguono una determinata o un numero
determinato di istruzioni un certo numero di volte; peccato che questo calcolo non funzioni per le
ricorrenze.
In particolare, quando affrontiamo il tema della complessità di un algoritmo ricorsivo, è bene ricordare che
tale difficoltà sta proprio nella ricorsione, ovvero un’equazione che definisce una sequenza in modo
ricorsivo.

N.B. l’analisi dei programmi ricorsivi richiede la soluzione delle ricorrenze.

RISOLVERE UNA RICORRENZA


Risolvere una ricorrenza, significa ottenere una soluzione in forma chiusa (non ricorsiva). Dobbiamo
trasformare la nostra ricorrenza in una formula che non sia più ricorsiva; in pratica trasformiamo il
programma da uno ALGORITMO RICORSIVO ad uno ALGORITMO ITERATTIVO.
SERIE E SOMMATORIE
Serie aritmetica
'(' + 1)
∑>J<@ K = 1 + 2 + ⋯ + ' =
2
Sommatoria per k che va da 1 a n dell’indice k, ovvero l’equivalente di 1+2+3+…+n; sappiamo che questa
somma equivale a n(n+1)/2
Serie geometrica
O >P@ − 1
∑>J<N O J &
= 1+O +O +⋯+O = >
O−1
(x!=1)
Sommatoria per k che va da 0 a n di tutte le potenze che hanno un certo valore x^k, ovvero 1 + x +
x^2+…+x^n; sappiamo che questa sommatoria produce come risultato x^n+1 -1/x-1. La sommatoria vale
solo per x > 1, se è minore, si verifica un caso speciale:
Caso speciale: x<1
1
∑Q J
J<N O =
1−O
SOLUZIONE DELLE RICORRENZE
Per risolvere le ricorrenze abbiamo a disposizione tre strumenti più o meno sistematici:
• Metodo della sostituzione;
• Metodo iterativo;
• Metodo principale o Master.
METODO DELLA SOSTITUZIONE
Si basa su un’ipotesi di soluzione, in particolare questo metodo richiede di ipotizzare una soluzione e
provarla usando l’induzione matematica.
Questo sistema è molto potente ma non sistematico, perché:
• L’efficacia dipende dall’ipotesi iniziale;
• Nessuna regola per la scelta dell’ipotesi, si va un po’ per esperienza.
METODO ITERATIVO
Può essere visto come uno strumento in cui si espande la ricorrenza; in particolare la si espande fino a
quando non si raggiunge il caso terminale T(1) con una espressione che dipende solo da n. In pratica
trasformiamo il programma da ricorsivo a iterativo, poiché ad ogni “passo” si elimina la ricorsività in favore
dell’iterazione.

Giacomo Maccanti – g.maccanti@students.uninettunouniversity.net 49


ALGORITMI E PROGRAMMAZIONE AVANZATA - G.MACCANTI@STUDENTS.UNINETTUNOUNIVERSITY.NET

Dobbiamo chiederci, dopo i espansioni, come sarà scritta la nostra equazione? Vediamo che dopo ogni
espansione abbiamo un termine costante “c” in più e un termine ricorsivo che si divide per due “T(n/4),
T(n/8), ecc.”. Quindi, alla fine, come espressione generica dopo i passi, avremo c*i+T(n/2^i).

La seconda domanda che dobbiamo porci è, quanto andiamo avanti? Andiamo avanti finché non
raggiungiamo T1, ovvero finché il termine n/2i diventa 1 à log & '

Sostituendo i nella formula:

2 ' = 8 log & ' + 2 1 = U(log & ')

METODO PRINCIPALE/MASTER
Utile perché applica un metodo matematico, sempre, ma non a tutte le ricorrenze.
Le ricorrenze a cui può essere applicato, sono quelle riconducibili al seguente tipo:

Dove a>=1, b>1 sono costanti. F(n) è una funzione asintoticamente positiva.

Abbiamo tre casi a seconda del confronto tra f(n) e '^ log W 4:
1. f(n)< '^ log W 4
2. f(n)= '^ log W 4
3. f(n)> '^ log W 4
Interpretazione asintotica:
1. f(n) = U('XYZ[ \]^ )
2. f(n) = Θ('XYZ[ \ )
3. f(n) = Ω('XYZ[ \]^ ) à T(n)= Θ('XYZ[ \ )

Per qualche ε>0.

Le relazioni tra f(n) e n^logba devono valere polinomialmente.

Giacomo Maccanti – g.maccanti@students.uninettunouniversity.net 50


ALGORITMI E PROGRAMMAZIONE AVANZATA - G.MACCANTI@STUDENTS.UNINETTUNOUNIVERSITY.NET

ALGORITMI DI ORDINAMENTO
PERCHE L’ORDINAMENTO?
L’ordinamento è un problema fondamentale ed il più studiato nell’informatica.
Una stima dice che il 25% degli elaboratori nel mondo, stiamo eseguendo programmi di ordinamento dei
dati.

ORDINAMENTO
Con linguaggio matematico è esprimibile come una relazione ℜ su un insieme S tale che, dati a,b,c, Є S.
PROPRIETA’ DELL’ORDINAMENTO
Simmetrica
aℜa
Transitiva
aℜb and bℜc <-> aℜc
Anti riflessiva
aℜb and bℜa <-> a=b
Es: la relazione <= sui numeri reali.

In pratica:
Data una sequenza di n valori A’=(a’1,…,a’n) tale che a’1<=a’2<=…<=a’n.

ASSUNZIONI
• Il tempo di accesso ad un elemento, deve avvenire in tempo costante O(1) – ovviamente
abbiamo un vettore e, sappiamo bene che, un vettore, può accedere direttamente ad un
elemento, tramite il suo indice, indifferentemente dal numero degli elementi;
• La dimensione del vettore è O(n) – ovvero la quantità di dati deve essere conosciuta a priori e,
una volta stanziato il vettore, non lo si può né ingrandire né rimpicciolire;
• Ordinamento senza duplicazione – significa che non possiamo ordinare i dati appoggiandoci su
un secondo vettore di comodo, ma l’ordinamento verrà eseguito, direttamente, sul vettore di
partenza.

CLASSI DI ALGORITMI DI ORDINAMENTO

N.B. gli algoritmi quadratici trovano applicazione solo a livello teorico; nella pratica non sono molto
utilizzati poiché non performanti. Degli algoritmi logaritmici, il quicksort è quello meglio implementabile e
funzionale.

Giacomo Maccanti – g.maccanti@students.uninettunouniversity.net 51


ALGORITMI E PROGRAMMAZIONE AVANZATA - G.MACCANTI@STUDENTS.UNINETTUNOUNIVERSITY.NET

HEAPSORT
E’ un algoritmo di ordinamento basato sullo ADT CODA A PRIORITA’ come struttura dati. Esso permette
un’efficiente estrazione del min/max da una sequenza.

Heap: costo delle primitive


Heapify: U(log & ')
BuildHeap: U(')

L’efficienza degli heap sta proprio nel costo di queste due primitive.

Heap e ordinamento
Uno heap, per come è strutturato, rappresenta un ordinamento parziale (solo su alcuni insiemi di valori) su
un insieme.
Noi possiamo riassettarlo, con costo lineare, per ottenere un algoritmo che predisponga, in partenza, una
sequenza ordinata attraverso la Heapify.

Heap: implementazione
Il nostro algoritmo è composto in due fasi:
1. Trasforma il vettore in heap (BuildHeap);
2. Costruiamo la nostra sequenza ordinata partendo dal massimo – avviene con una eliminazione
ripetuta del massimo con mantenimento della proprietà dello heap (iterazione Heapify).

Heapsort: pseudocodice
N.B. SIAMO IN UNO MaxHeap
HeapSort(A) //A E’ UN GENERICO VETTORE DI DIMENSIONE N – A[n]
{
BuildHeap(A,n); //COSTRUISCO LO HEAP PASSANDO IL MIO VETTORE E LA SUA GRANDEZZA
for i ← n downto 2 //CICLO DI FOR ESEGUITO n-1 VOLTE à for(i=n; i>=1; i-2)
swap(A[1], A[i]) //SCAMBIA IL PRIMO ELEMENTO DELLO Heap (IL PIU’ GRANDE) CON ELEMENTO iESIMO
//LO SCAMBIO PROVOCA UNA PERTURBAZIONE DELLO Heap, quindi A NON E’ PIU’ UNO
//Heap
n ← n-1 //DECREMENTO LA QUANTITA’ DI n; IN PRATICA GLI DICO DI CONCENTRARSI SOLO
SULLA //SULLA RESTANTE PARTE DEGLI ELEMENTI, POICHE’ L’ULTIMO ELEMENTO ORA E’ IL
//MASSIMO
Heapify(A,1) //INVOCO Heapify, QUINDI RIPRISTINO LE PROPRIETA’ DELLO Heap
}

Giacomo Maccanti – g.maccanti@students.uninettunouniversity.net 52


ALGORITMI E PROGRAMMAZIONE AVANZATA - G.MACCANTI@STUDENTS.UNINETTUNOUNIVERSITY.NET

Heapsort: esempio
Sequenza non ordinata di 8 elementi; ovviamente non è uno heap.

Applichiamo la BuildHeap e otteniamo una versione ordinata tipo Heap.

Lo Heap viene ottenuto in tempo lienare O(n).

COME PROCEDE, QUINDI, IL NOSTRO ALGORITMO, DOPO AVER COSTRUITO LO HEAP?

PRIMO PASSO

Scambia il primo elemento, quindi 12, con l’ultimo, quindi 2 (swap A[1], A[i]).

Dopodiché, elimino l’ultimo elemento (n ← n-1).


In questo modo, abbiamo sicuramente assegnato 12 all’ultimo elemento del vettore
per lasciarlo fisso lì, al suo posto e dimenticarcelo (per il momento). Vediamo che
abbiamo una nuova struttura ad albero che, ovviamente, non è più uno Heap.

Applico la Heapify e riottengo una riorganizzazione dei valori fino ad avere uno Heap.
N.B. vediamo che il numero due è stato riposizionato come ultimo elemento del
vettore. Ovviamene 12 lo vediamo ma non lo consideriamo.

SECONDO PASSO

Scambiamo nuovamente il primo elemento con l’ultimo (swap A[1], A[i]).


Riduciamo la grandezza del nostro vettore (n ← n-1); quindi anche dieci sarà
posizionato in penultima posizione e “dimenticato” lì.

La nostra struttura non era più uno heap, dopo l’applicazione di Heapify otteniamo
nuovamente uno heap.

TERZO PASSO

Scambio il primo elemento con l’ultimo (ricordo che ad ogni iterazione di


decrementa il numero di elementi massimo del vettore) (swap A[1], A[i]).
Riduciamo la grandezza del nostro vettore (n ← n-1); quindi anche 7 sarà
posizionato in terzultima posizione e “dimenticato” lì.

Giacomo Maccanti – g.maccanti@students.uninettunouniversity.net 53


ALGORITMI E PROGRAMMAZIONE AVANZATA - G.MACCANTI@STUDENTS.UNINETTUNOUNIVERSITY.NET

Applichiamo lo Heapify per riottenere uno heap.

APPLICANDO GLI N PASSI, GIUNGIAMO ALL’ULTIMO

Scambiamo sempre i due elementi del vettore (swap A[1], A[i]).


Togliamo 1 agli n elementi del vettore (n ← n-1).

All’ultima iterazione, avremo un solo elemento che, per forza, sarà uno heap e, a
questo punto, abbiamo terminato il nostro algoritmo.

Heapsort: analisi della complessità asintotica

Tutti i casi hanno costo O(nlogn), non abbiamo un caso migliore e uno peggiore, anche se avessimo un
vettore ordinato al contrario.

Giacomo Maccanti – g.maccanti@students.uninettunouniversity.net 54


ALGORITMI E PROGRAMMAZIONE AVANZATA - G.MACCANTI@STUDENTS.UNINETTUNOUNIVERSITY.NET

QUICKSORT
È un algoritmo basato sul paradigma DIVIDE AND CONQUER (DIVIDE-ET-IMPERA) – DIVIDI E RISOLVI così
chiamato perché ha, in generale, prestazioni migliori tra quelli basati sul confronto.
Il funzionamento concettuale del QUICKSORT, consiste di tre fasi:
1. Scegliamo un elemento (pivot – è il nostro elemento di riferimento);
2. Sulla base del nostro pivot, riorganizziamo il vettore – riorganizzare il vettore significa eseguire una
sorta di ordinamento parziale che vede la suddivisione del vettore in due partizioni:
a. Tutti gli elementi <= del pivot spostati prima del pivot (partizione di sinistra);
b. Tutti gli elementi >= del pivot spostati dopo il pivot (partizione di destra).
c. per la Transitiva sappiamo che gli elementi della partizione di sinistra sono <= di quella
destra e gli elementi della partizione di destra sono >= di quella di sinistra.
3. Ordiniamo ricorsivamente le due partizioni.

Quicksort: pseudocodice
QuickSort(A,p,r) //A E’ IL VETTORE, p E r SONO GLI ESTREMI SINISTRA E DESTRA DEL
VETTORE
{
if (p < r) { //SE L’INDICE SINISTRO DELLA PARTIZIONE E’ MINORE DELL’INDICE DESTRO
q = Partition(A,p,r) //q SARA’ L’INDICE DI MEZZERIA DELLE DUE PARTIZIONI
QuickSort(A,p,q) //ORDINO GLI ELEMENTI PRIMA DEL PIVOT (DA p A q)
QuickSort(A, q +1 ,r) //ORDINO GLI ELEMENTO DOPO DEL PIVOT (DA q+1 A r)
}
}
}
q divide il vettore in due sotto vettori, tali che gli elementi di A[p,q] <= elementi di A[p+1,r]

Giacomo Maccanti – g.maccanti@students.uninettunouniversity.net 55


ALGORITMI E PROGRAMMAZIONE AVANZATA - G.MACCANTI@STUDENTS.UNINETTUNOUNIVERSITY.NET

Quicksort: partition
È chiaro come appaia subito che l’intelligenza del Quicksort() sta tutta nella funzione Partition().
La funzione Partition() esegue i seguenti passi:
1. Scelta del pivot;
2. Generazione dei due sotto vettori.

Ha una complessità lineare pari a O(n).

Quicksort partition: pseudocodice


Partition(A,p,r) //PASSIAMO IL NOSTRO VETTORE A, PIU’ GLI ESTREMI
{
x = A[p]; //X, IL PIVOT, E’ IL PRIMO ELEMENTO DELLA PARTIZIONE
i = p – 1; j = r + 1; //SONO INDICI INIZIALIZZATI ALL’ESTERNO DEL VETTORE
while(true) //CICLO PRINCIPALE WHILE
do Il while continua fino a trovare un elemento >= del pivot; cioè
j = j – 1; quanto trovo un elemento <= di x da destra
while A[j] x;
do Il while continua fino a trovare un elemento <= del pivot; cioè
i = i + 1; quanto trovo un elemento >= di x da sinistra

while A[i] x
if (i < j){ Scambio i valori
swap (A[i],A[j]);
} else {
return j; //j RAPPRESETA IL PUNTO DI MEZZERIA DELLA NOSTRA PARTIZIONE; SARA’ q
}
Endwhile //IL CICLO CONTINUA FIN QUANDO NON SI SONO SCAVALCATI GLI INDICI
}

Quicksort: esempio
Scelgo un generico vettore A di 8 elementi:
A[53264137]

Partition (livello 0)
Il nostro pivot (x) come abbiamo detto, assumerà il valore del primo elemento del vettore (5 in questo
caso).
Inoltre vediamo l’estremo sinistro (p) sia valorizzato a indice 0 e l’estremo destro (r) a indice 7.
Per ultimo notiamo che i due i due indici i e j vengono posizionati all’esterno del vettore; i = -1.

Dopo le prime due iterazioni del nostro ciclo, vediamo che i e j hanno rispettivamente assunto i valori 0 e 6,
questo significa che siamo in grado di confrontare il numero 5 (in questo caso il pivot) per la partizione
sinistra e il numero 3 per la partizione di destra; ovviamente entrambi sono fuori posto, poiché 3 è più
piccolo e andrebbe a sinistra e 5 è maggiore di tre, quindi andrebbe a destra. Li scambio.

I è minore di j, quindi non ho finito di iterare; devo continuare:

Giacomo Maccanti – g.maccanti@students.uninettunouniversity.net 56


ALGORITMI E PROGRAMMAZIONE AVANZATA - G.MACCANTI@STUDENTS.UNINETTUNOUNIVERSITY.NET

Infine, arriviamo al punto in cui i è maggiore di j, quindi ho finito di iterare:

In questo momento abbiamo che il vettore A ha una partizione di sinistra che va da A[p,q] e una partizione
di destra che va da A[q+,r].

Partition (livello 1)

Con l’esempio vediamo come, il vettore, viene, di volta in volta, suddiviso in sotto vettori che sono
analizzati e ordinati. Tutto questo viene fatto senza toccare le alte partizioni quindi senza rispostare o
modificare i dati già precedentemente ordinati.

Giacomo Maccanti – g.maccanti@students.uninettunouniversity.net 57


ALGORITMI E PROGRAMMAZIONE AVANZATA - G.MACCANTI@STUDENTS.UNINETTUNOUNIVERSITY.NET

Quicksort: analisi
E’ importante, ovviamente, valutare la complessità dell’algoritmo di Quicksort e, di conseguenza, il suo
costo. Possiamo avere, in particolare, tre casi:
1. Caso migliore;
2. Caso medio;
3. Caso peggiore.

Analisi del Quicksort: caso migliore


Il caso migliore si ha quando l’algoritmo di Partition() ritorna due sotto partizioni esattamente bilanciate
(pari alla metà quindi un albero esattamente bilanciato).

Analisi del Quicksort: caso medio


Il caso medio, come nel caso migliore (O(nlogn)) ha una complessità che non dipende dallo sbilanciamento
delle partizioni ma solo dal fatto che esso sia proporzionale.

Analisi del Quicksort: caso peggiore


Il caso peggiore si verifica quando la partizione risulta massimamente sbilanciata; ovvero quando una
partizione è estremamente banale e una estremamente complicata.

Giacomo Maccanti – g.maccanti@students.uninettunouniversity.net 58


ALGORITMI E PROGRAMMAZIONE AVANZATA - G.MACCANTI@STUDENTS.UNINETTUNOUNIVERSITY.NET

ALGORITMI CON COMPLESSITA’ LINEARE


Come abbiamo visto, la complessità O(nlogn) è il massimo ottenibile per gli algoritmi di:
• Ordinamento basato su confronti;
• Ordinamento in situ (cioè senza avere un vettore di appoggio).
Soprattutto senza dover fare nessuna assunzione sul valore dei dati.

Diciamo quindi, come commento generale, che per gli algoritmi di ordinamento è relativamente facile
svolgere il loro compito con una complessità buona O(nlogn) e i vincoli sono abbastanza significativi.
Il problema è che su un insieme di dati molto grandi potrebbe comunque essere una limitazione; per
questo motivo ci poniamo l’obbiettivo di ottenere una complessità lineare O(n), ricordandoci che per
andare oltre il limite teorico è necessario rilassare una o più di queste assunzioni.

COUNTING SORT (ORDINAMENTO PER CONTEGGIO)


La prima assunzione che andiamo a “rilassare” è proprio quella di fare delle assunzioni sui valori da
ordinare (non lo faceva nessuno degli algoritmi fino ad ora visti).
Assumiamo che gli elementi appartengono ad un certo intervallo [1,…,K], per qualche k
Esempio: Se il nostro vettore contenesse valori di età e noi dovessimo ordinarlo per età, potremmo
assumere che un umano non può vivere oltre i 130 anni (esagerando) e che quindi il nostro range vada da
[1,…,130].
Perché, allora, è così importante la conoscenza dei valori? Perché la conoscenza dei valori permette di
calcolare la posizione di una chiave (senza confronti!).
Ciò ci permette di ottenere un costo che è pari al calcolo della posizione di ogni valore:
Complessità: O(k+n), ordina in tempo lineare se ho conoscenza di due parametri n (numero dei dati totale
che sto ordinando) e k (range).
Se k = O(n), cioè se il mio range è della stessa grandezza della dimensione dei dati, ordino in tempo lineare!

Counting sort: principio


Ritorniamo alla domanda di prima: perché è importante conoscere il range dei valori compresi tra un valore
minimo e un valore massimo?
Perché così posso calcolare la posizione in cui un generico elemento va posizionato. Questo viene reso
possibile con delle statistiche che ci permettono di determinare, per un elemento x, il numero di elementi
che sono <= x.

Counting sort: strutture dati


Vediamo, ora, su quali strutture si basa il nostro algoritmo:
• A: vettore da ordinare;
• Vettori ausiliari (rilassamento seconda assunzione – non più algoritmo in situ):
o B: memorizza il risultato (stiamo, di fatto, moltiplicando lo spazio poiché abbiamo un
secondo vettore di appoggio);
o C: memorizza il conteggio (n. di elementi <= x).

Counting sort: pseudocodice


L’algoritmo del counting sort funziona in3 fasi:
CountingSort (A,n) //A E’ IL NOSTRO VETTORE, n LA DIMENSIONE DEL VETTORE
for i=1 to k C[i] O //SAPPIAMO CHE ABBIAMO TANTI ELEMENTI QUANTI SONO GLI ELEMENTI
DA ORDINARE, QUINDI POSSO ANDARE DA 1 A k ANZICHE’ 1 a N, INIZIALIZZO L’iESIMO ELEMENTO A 0

/* fase I: calcola conteggi (statistiche)*/

for i=1 to n //PER OGNI ELEMENTO DEL VETTORE, DEVO FARE UN ISTOGRAMMA

Giacomo Maccanti – g.maccanti@students.uninettunouniversity.net 59


ALGORITMI E PROGRAMMAZIONE AVANZATA - G.MACCANTI@STUDENTS.UNINETTUNOUNIVERSITY.NET

C[A[i]] C[A[i]]+1; //IN PRATICA, PER OGNI ELEMENTO, MI SEGNO QUANTE VOLTE COMPARE;
CIOE’ IL NUMERO 1 COMPARE 2 VOLTE, IL NUMERO 5 COMPARE 1 VOLTA, ECC.

/* fase II: calcola conteggi cumulativi */

for i=2 to k
C[i] C[i] + C[i-1]; //SAPRO’ QUANTI ELEMENTI SONO <= DI UN CERTO ELEMENTO

/* fase III: posiziona A[i] nella posizione opportuna e aggiorna i conteggi */

for i=n downto 1


B[C[A[i]]] A[i]
C[A[i]] C[A[i]]-1

Counting sort: esempio

In fase 1 calcolo i conteggi (inteso come La prima considerazione che facciamo è il


le volte in cui appare un elemento del calcolo del range: vediamo che i valori del
vettore A). vettore vanno da 1 a 6, quindi k=6. Ci
servirà, quindi, un altro vettore grandezza
Attenzione che il calcolo viene fatto sulla
6 per calcolare le statistiche di questi
posizione dell’indice: indice posizione 1,
valori.
appare 2 volte, indice posizione 2, appare
0 volte (non ho 2), indice posizione 3, Mi dice quanti valori sono <= dell’indice.
appare tre volte, indice posizione 4, In questo esempio vediamo che sono in
appare tre volte, ecc. posizione 4, mi chiedo: quanti valori ho <=
di 4? Esattamente 7 (3,4,1,3,4,1,4).

Vediamo che il vettore B inizia a


posizionare gli elementi a partire
dall’ultimo elemento del vettore A. In
questo caso diciamo che B memorizza il
valore 4 in posizione 7, poiché A in pos. 8
ha quattro, quindi C in posizione 4 ha
sette.

Counting sort: analisi

T(n) S(n)
Fase I O(n) O(k)

Giacomo Maccanti – g.maccanti@students.uninettunouniversity.net 60


ALGORITMI E PROGRAMMAZIONE AVANZATA - G.MACCANTI@STUDENTS.UNINETTUNOUNIVERSITY.NET

Fase II O(k) O(n)


Fase III O(n) O(2n+k)
Totale O(n+k)

STABILITA’ DI UN ALGORITMO DI ORDINAMENTO


Un algoritmo di ordinamento si dice stabile se preserva l’ordine relativo degli elementi con identica chiave
nella sequenza originale.

CONCLUSIONI ALGORITMI DI ORDINAMENTO

Giacomo Maccanti – g.maccanti@students.uninettunouniversity.net 61


ALGORITMI E PROGRAMMAZIONE AVANZATA - G.MACCANTI@STUDENTS.UNINETTUNOUNIVERSITY.NET

INSIEMI DINAMICI E DIZIONARI


Quando parliamo di insiemi dinamici, stiamo palando di insiemi che variano nel tempo; ovvero aggiungiamo
oggetti, eliminiamo oggetti, modifichiamo gli oggetti aumentandoli o diminuendoli, ecc.
Ognuno di questi oggetti, inoltre, contiene i seguenti elementi:
• Una chiave – quella, per esempio, su cui applichiamo l’ordinamento;
• Dati “satellite” – sono ausiliari ma caratterizzano il dato nel suo insieme.

L’insieme da cui le chiavi sono prese, può essere ordinato; in questo caso alcune operazioni non sono
significative.

OPERAZIONI
Interrogazioni
Forniscono informazioni sull’insieme.

Operazioni di modifica
Modificano l’insieme.

PRIMITIVE DI GESTIONE DEGLI INSIEMI


Indicano le operazioni che si possono eseguire sui dizionari; esse, sono:
• Ricerca – Search();
• Inserimento – Insert();
• Cancellazione – Delete();
• Minimo – Minimum() *solo se S è ordinato;
• Massimo – Maximum() * solo se S è ordinato;
• Predecessore – Predecessor() *solo se S è ordinato;
• Successore – Successor() *solo se S è ordinato;
• Insieme vuoto – IsEmpty() *operazione ausiliaria;
• Inizializza – Clear() *operazione ausiliaria;
• Taglia – Size() *operazione ausiliaria;
• Elementi dell’insieme – List() *operazione ausiliaria.

S = generico insieme.

Ricerca in un BST: definizione


Ritorna l’elemento con chiave k in S o NULL se un elemento con chiave k non appartiene a S.

Inserimento in un BST: definizione


Aggiunge l’elemento x a S. Se ha successo ritorna vero, falso altrimenti.

Cancellazione in un BST: definizione


Cancella l’elemento x da S. Se ha successo ritorna vero, falso altrimenti.

Minimum in un BST: definizione


*Se S è ordinato.
Ritorna l’elemento di S con valore di chiave più piccolo o NULL se S è vuoto.

Maximum in un BST: definizione

Giacomo Maccanti – g.maccanti@students.uninettunouniversity.net 62


ALGORITMI E PROGRAMMAZIONE AVANZATA - G.MACCANTI@STUDENTS.UNINETTUNOUNIVERSITY.NET

*Se S è ordinato.
Ritorna l’elemento di S con valore di chiave più grande o NULL se S è vuoto.

Predecessor in un BST: definizione


*Se S è ordinato.
Ritorna l’elemento S con valore di chiave più grande <k o NULL se non esiste.

Successor in un BST: definizione


*Se S è ordinato.
Ritorna l’elemento S con valore di chiave più piccola >k o NULL se non esiste.

Isempty in un BST: definizione


*Operazione ausiliarie.
Ritorna vero se l’insieme è vuoto, falso altrimenti.

Clear in un BST: definizione


*Operazione ausiliarie.
Inizializza l’insieme all’insieme vuoto.

Size in un BST: definizione


*Operazione ausiliarie.
Ritorna la cardinalità dell’insieme.

List in un BST: definizione


*Operazione ausiliarie.
Elenca gli elementi dell’insieme.

Giacomo Maccanti – g.maccanti@students.uninettunouniversity.net 63


ALGORITMI E PROGRAMMAZIONE AVANZATA - G.MACCANTI@STUDENTS.UNINETTUNOUNIVERSITY.NET

DIZIONARI
Un insieme dinamico che fornisce solo le operazioni Search(), Insert(), Delete() è detto dizionario.
Quando l’insieme è totalmente ordinato, si parla di dizionario ordinato.

COME IMPLEMENTARE UN INSIEME DINAMICO?


• Vettori (ordinati e non);
• Liste (ordinate e non);
• Code a priorità;
• Code;
• Stack.

PRIMA DI DECIDERE, VEDIAMO QUAL E’ IL LORO COSTO ASINTOTICO (CI BASIAMO SOLO OP. BASILARI).

Giacomo Maccanti – g.maccanti@students.uninettunouniversity.net 64


ALGORITMI E PROGRAMMAZIONE AVANZATA - G.MACCANTI@STUDENTS.UNINETTUNOUNIVERSITY.NET

ALBERI BINARI DI RICERCA (BST)


Gli alberi binari di ricerca sono una particolare struttura dati che ci permette di implementare ed utilizzare i
dizionari.
Implementazione ad albero (binario) di un dizionario ordinato Permette complessità Ο(h) per Search(),
Insert() e Delete(); h = altezza dell’albero.

Nel caso migliore è possibile organizzare n chiavi in un albero di altezza h=log2n.

PROPRETA’ DI UN BST
Per ogni nodo con chiave x:
• I nodi nel sottoalbero di sinistra hanno chiavi <= di x;
• I nodi nel sottoalbero di destra hanno chiavi >= di x.

PRIMITIVE DI GESTIONE DI UN BST


Le operazioni di base di un albero di ricerca, sono sempre le stesse:
• Inserimento();
• Ricerca();
• Cancellazione();
• Visita().

Per ovvie ragioni, essendo che abbiamo già parlato degli alberi, sappiamo che le primitive sarebbero
intrinsecamente ricorsive ma, per ragioni di efficienza, la maggior parte è iterativa.

Ricerca di un BST
Dato un generico elemento da ricercare “x”:
• Se x è < della chiave della radice, cerca nel sottoalbero di sinistra;
• Se x è > della chiave della radice, cerca nel sottoalbero di destra;
• Se x è = alla chiave della radice, trovato.

In realtà, quando si parla di ricerca di un BST, dobbiamo sapere che quasi sempre ci si riferisce a
quella iterativa; sebbene ne esista una versione ricorsiva, non è facile risalirne al costo poiché
sarebbero necessarie delle assunzioni. Noi le vedremo entrambe, ma è richiesta quella iterativa.

Giacomo Maccanti – g.maccanti@students.uninettunouniversity.net 65


ALGORITMI E PROGRAMMAZIONE AVANZATA - G.MACCANTI@STUDENTS.UNINETTUNOUNIVERSITY.NET

Ricerca di un BST (ricorsiva): pseudocodice


Search(T,k) //T E’ IL NOSTRO ALBERO, k E’ LA NOSTRA CHIAVE DA RICERCARE
{
if(T == NULL) //SE HO UN ALBERO VUOTO OPPURE HO SCORSO L’ALBERO INTERO
return O //TORNO 0 (NON TROVATO) ED ESCO
if(k < key)) //SE k E’ MINORE DELLA CHIAVE DEL VERTICE DELL’ALBERO IN CUI SONO
Search (left[T], k) //CERCA NEL SOTTOALBERO DI SINISTRA
if(k > key(T)) //SE k E’ MAGGIORE DELLA CHIAVE DEL VERTICE DELL’ALBERO IN CUI SONO
Search(right[T], k)//CERCA NEL SOTTOALBERO DI DESTRA
if(k= key(T)) //SE k E’ UGUALE ALLA CHIAVE DELL’ALBERO
return 1 //TORNO 1 (TROVATO) ED ESCO
}

Ricerca di un BST (iterativa): pseudocodice


Notiamo come la ricorsione sia stata rimpiazzata dal nostro ciclo while:
Search(T, k){ //T E’ IL NOSTRO ALBERO, k E’ A NOSTRA CHIAVE DA RICERCARE
while (T !=NULL) //FINCHE’ L’ALBERO NON E’ VUOTO (O VUOTO SUBITO O SCORSO TUTTO)
if (k = key(T)) //SE k E’ UGUALE ALLA CHIAVE DELL’ALBERO
return 1 //TORNO 1 (TROVATO) ED ESCO
else if (k < key(T)) //SE k E’ MINORE DELLA CHIAVE DEL VERTICE DELL’ALBERO IN CUI SONO
T left[T] //FACCIO AVANZARE IL PUNTATORE DEL SOTTOALBERO DI SINISTRA
else //SE k E’ MAGGIORE DELLA CHIAVE DEL VERTICE DELLA’LBERO IN CUI
SONO
T right[T] //FACCIO AVANZARE IL PUNTATORE DEL SOTTOALBERO DI DESTRA
Endwhile
return 0 //SE SONO ARRIVATO A QUESTO PUNTO, NON SONO ENTRATO NEL WHILE
PERCHE’ L’ALBERO ERA VUOTO, OPPURE L’HO SCORSO TUTTO E NON HO TROVATO NESSUN ELEMENTO = A k;
TORNO 0 (NON TROVATO) ED ESCO
}

Ricerca di un BST (iterativa): costo


La ricerca implica, nel caso peggiore, attraversare l’albero dalla radice ad una foglia:
T(n)=O(h).

Ricerca di un BST: esempio


9 è la nostra k (elemento
da ricercare) mentre 6,8
e 12 sono i nostri Key.

Giacomo Maccanti – g.maccanti@students.uninettunouniversity.net 66


ALGORITMI E PROGRAMMAZIONE AVANZATA - G.MACCANTI@STUDENTS.UNINETTUNOUNIVERSITY.NET

Visita di un BST
Le visite (il modo in cui elenchiamo gli elementi appartenenti ad un insieme dinamico implementato con un
BST) sono classificate in base all’ordine dei visita dei nodi:
• Inorder (infisso);
• Preorder (prefisso);
• Postorder (postfisso).
Tutte queste funzioni hanno complessità Θ(n) poiché, parlando di una visita, sappiamo dover “toccare” tutti
gli elementi.

Visita inorder (ordine infisso) di un BST: pseudocodice


INORDER(x){
if x != NULL then{
INORDER(left[x])
print key[x]
INODERDER(right[x]) }}

Visita preorder (prefisso) di un BST: pseudocodice


PREORDER(x){
if x != NULL then{
print key[x]
PREORDER(left[x])
PREORDER (right[x]) }}

Visita postorder (ordine postfisso) di un BST: pseudocodice


POSTORDER(x){
if x != NULL then{
POSTORDER(left[x])
POSTORDER (right[x])
print key[x] }}

Visita di un BST: esempio

Inserimento in un BST: definizione


Chiamiamo u il nodo che vogliamo inserire sapendo che u viene sempre inserito come foglia (per non
ristrutturare il mio albero ogni volta); per fare questo sono necessarie due operazioni:
• Ricerca del nodo v che diventerà genitore di u;
• Inserimento di u come figlio di v;

Inserimento in un BST: pseudocodice


Tree-Insert (T, u) //T E’ IL NOSTRO ALBERO E U L’ELEMENTO DA INSERIRE
{
y NULL; x root(T) //x VIENE INIZIALIZZATA ALLA RADICE DELL’ALBERO, y A NULL
// cerca la posizione
while x != NULL do //CICLO FINCHE’ x NON SARA’ NULL
y x //y PRENDE IL VALORE DI x

Giacomo Maccanti – g.maccanti@students.uninettunouniversity.net 67


ALGORITMI E PROGRAMMAZIONE AVANZATA - G.MACCANTI@STUDENTS.UNINETTUNOUNIVERSITY.NET

if (key[u] < key[x]) then //SE LA CHIAVE DEL NODA DA INSERIRE E’ MINORE DI x
x left[x] //x SEGUE IL PUNTATORE A SINISTRA
else //ALTRIMENTI
x right[x] //x SEGUE IL PUNTATORE A DESTRA
endwhile //ESCO DAL CICLO QUANDO x E’ = A NULL
v y //v DIVENTERA’ IL GENITORE DI U
// GESTIONE DEI CASI PARTICOLARI
if (v = NULL) then //v E’ NULL SE L’ALBERO E’ VUOTO - CASO PARTICOLARE I
root[T] u //SE VUOTO AVRA’ COME VERTICE IL NUOVO ELEMENTO
else //ALBERO NON VUOTO – CASO PARTICOLARE II
if (key[u] < key[v]) //SE u E’ MINORE INSERISCO COME FIGLIO SINISTRO
left [v] u
else //SE u E’ MAGGIORE INSERISCO COME FIGLIO DESTRO
right[v] u
endif
}

Inserimento in un BST: esempio


Siamo nel caso particolare I, dove ho un albero vuoto (T = NULL) e devo
inserire un nuovo elemento (u = 5).
Avrò che la radice sarà uguale al nuovo elemento: root[T]=u

Siamo nel caso particolare II, dove ho un albero già esistente (T != NULL)
e devo inserire un nuovo elemento (u=5).
Secondo lo pseudocodice avremo che x viene inizializzato alla radice e y
a NULL: x = root(T); y = NULL
Cominciamo a scendere muovendo x e y finché non è uguale a NULL

y prende il vecchio valore di x (in questo caso la radice): y ß x mentre


x mi indica il punto in cui mi trovo.
Dopodiché comincio a confrontare u con x per capire in quale
sottoalbero andare, prima andrò a sinistra (u=5 < x=6), poi avrò che u >
x (u=5 > x=2) e andò a destra

Arrivo fino al punto in cui u è maggiore di x (u=5 > x=4) e dovrei andare
a destra, mi accorgo che spostando x questo assume valore NULL
(posizione libera per l’inserimento) ed esco dal ciclo di while.
v è uguale a y (4): v y

Entriamo nella seconda fase dove ci dice che dobbiamo inserire u

Giacomo Maccanti – g.maccanti@students.uninettunouniversity.net 68


ALGORITMI E PROGRAMMAZIONE AVANZATA - G.MACCANTI@STUDENTS.UNINETTUNOUNIVERSITY.NET

come figlio di v, sul disegno vediamo l’ovvietà, ma su codice dobbiamo chiederci ancora se u < x o u > x: if
(key[u] < key[v])
Entreremo nel ramo else, poiché u > x e inserirò l’elemento: right[v] u

Inserimento in un BST: analisi di complessità


Devo percorrere l’albero dalla radice ad una foglia:
T(n)=O(h).

N.B. h è l’altezza dell’albero.

Cancellazione in un BST: definizione


L’operazione di cancellazione è abbastanza articolata, poiché la funzione Delete() prevede tre casi
particolari, in cui sto cercando di eleminare un nodo x da un albero T:
1. x non ha figli (è una foglia);
2. x ha un solo figlio;
3. x ha due figli.

Cancellazione in un BST: caso 1


Se x non ha figli significa che sto eliminando una foglia, quindi eliminandola non avrò impatti significativi; la
si ricerca e la si elimina direttamente.

Cancellazione in un BST: caso 2


Se x ha un solo foglio, si connette il genitore del nodo eliminato al suo unico figlio; questa operazione è
anche detta by-pass di un nodo.

Cancellazione in un BST: caso 3


Se x ha de figli, si cerca il successore del nodo da eliminare, cancelliamo il successore (un altro nodo, non
proprio quello che voglio cancellare) e poi copiamo la chiave del successore (eliminato) nel nodo da
eliminare.
L’identificazione di successore o predecessore di un nodo richiedono il calcolo del massimo e minimo di un
BST. Questo avviene attraverso le funzioni Tree-Min e Tree-Max che non richiedono confronti poiché sono
sempre definite.

Tree-Min in un BST: pseudocodice


Tree-Min(T){
while (left[T] NULL)
do
T left[T]
return T
}

Tree-Max in un BST: pseudocodice


Tree-Max(T){
while (right[T] NULL) do
T right[T]
return T
}
Successore in un BST: definizione
Se x ha un figlio destro, il successore è la chiave più piccola del sotto albero destro, altrimenti attraversa
l’albero finché non trova un nodo x che sia figlio sinistro del suo genitore.

Giacomo Maccanti – g.maccanti@students.uninettunouniversity.net 69


ALGORITMI E PROGRAMMAZIONE AVANZATA - G.MACCANTI@STUDENTS.UNINETTUNOUNIVERSITY.NET

Successore in un BST: pseudocodice


Tree-Successor(x){
if (right[x] NULL) then //S HA UN FIGLIO DESTRO, IL SUCC. E’ LA CHIAVE PIU’ PICCOLA DEL
SOTTO ALBERO DESTRO
return Tree-Min(right[x])
y p[x]
while (y NULL and x = right[y]) do //ATTRAVERSO L’ALBERO FINCHE’ NON TROVO UN NODO x
FIGLIO SINISTRO DEL SUO GENITORE
x y
y p[y]

return y
}

Successore in un BST: esempio


Chi è il successore di 6?
Vediamo subito che 6 ha un sottoalbero destro, quindi sicuramente avrà un successore e sarà il valore più
piccolo del sottoalbero destro cioè il minimo del sottoalbero destro (il minimo si calcola inseguendo tutti i
puntatori del sottoalbero di sinistra ma non essendoci sottoalberi di sinistra, ci fermiamo subito) e
decretiamo 8 come il più piccolo e successore di 6.

Giacomo Maccanti – g.maccanti@students.uninettunouniversity.net 70


ALGORITMI E PROGRAMMAZIONE AVANZATA - G.MACCANTI@STUDENTS.UNINETTUNOUNIVERSITY.NET

Cancellazione in un BST: pseudocodice


Tree-Delete (T,z){ //T E’ IL NOSTRO ALBERO, Z L’ELEMENTO DA ELIMINARE
if (left[z] = NULL or right[z] = NULL) then //CASO 2, HA UN SOLO FIGLIO
y z
else
y Successor(z)
if (left[y] != NULL) then
x left[y]
else
x right[y]
if (x != NULL) then
p[x] p[y]
if p[y] = NULL then
// delete root
root[T] x
else
if (y=left[p[y]]) then
left[p[y]] x
else
right[p[y]] x
if (y != z) then
key[z] key[y]
return y
}

Cancellazione in un BST: analisi di complessità


Nel caso peggiore si attraversa l’albero dalla radice ad una foglia:
T(n)=O(h).

N.B. h è l’altezza dell’albero.

ADT BST in c: codice c


typedef struct btnode *TNodeptr;
typedef struct btnode{
int key;
TNodeptr left, right;
} TNode;

Ricerca in un BST: codice c


int Search(int key, TNodeptr head){
while(head != NULL){
if (head->key == key)
return(1);
else
if (head->key < key)
head = head->right;
else
head = head->left;
}
return(O);
}

Giacomo Maccanti – g.maccanti@students.uninettunouniversity.net 71


ALGORITMI E PROGRAMMAZIONE AVANZATA - G.MACCANTI@STUDENTS.UNINETTUNOUNIVERSITY.NET

TABELLE HASH
Le tabelle hash sono, in pratica, una implementazione di insieme dinamici non ordinati che permettono dei
tempi di accesso per inserimenti, ricerca e cancellazione molto veloci.

TABELLE AD ACCESSO DIRETTO


Stiamo parlando sempre di dizionari e di come implementarli e il punto chiave è che se noi analizziamo
bene come sono organizzati e quali sono le operazioni che devono fare con le risorse a nostra disposizione
possiamo accorgerci che se non è richiesto un ordine particolare nell’insieme che vogliamo rappresentare è
possibile implementare le operazioni base in tempo costante O(1).
Questo è possibile, parlando di tabelle ad accesso diretto, generalizzando il concetto di vettore cosa che, a
tutti gli effetti, sono.
Sia dato un vettore T in cui manteniamo un elemento per ogni possibile valore di chiave; questo significa
che usiamo il valore della chiave come indice.
Chiave k memorizzata in posizione T[k].

Tabelle ad accesso diretto: notazione


Per affrontare questo argomento, però, è necessario, prima di tutto, introdurre alcuni concetti chiave,
proposti di seguito:
UNIVERSO: u = {1,…,m} rappresenta l’insieme di tutte le chiavi.
m = |u| dimensione della nostra tabella.
K = {1,…,n} rappresenta il sottoinsieme di chiavi che sono effettivamente memorizzate, poiché è ovvio che
non useremo mai tutte le chiavi dell’intero universo.
n = |k| rappresenta la cardinalità dell’insieme k quindi il numero di chiavi effettivamente utilizzate.

Tabelle ad accesso diretto: esempio

Giacomo Maccanti – g.maccanti@students.uninettunouniversity.net 72


ALGORITMI E PROGRAMMAZIONE AVANZATA - G.MACCANTI@STUDENTS.UNINETTUNOUNIVERSITY.NET

Tabelle ad accesso diretto: problemi


Essendo che stiamo trattando di un vettore, sappiamo che, accedendo alle singole posizioni tramite indice,
abbiamo il vantaggio di lavorare in tempo costante O(1) e questo varrà, ovviamente, per la primitiva di
ricerca ma anche per quella di inserimento e cancellazione.
Sembra, a questo punto, che noi abbiamo trovato un algoritmo che risponde benissimo ai nostri criteri di
affidabilità è costo, ma non è così, infatti abbiamo due filoni di problemi molto importanti che dobbiamo
tenere in considerazione:

• Se la cardinalità è molto grande, quindi |U|, è impossibile riuscire ad allocare una tabella m=|U|;
• Se n << m (se il numero di elementi del sottoinsieme è molto più piccolo rispetto l’universo), avrò
che l’utilizzazione della tabella è basso (fattore di carico n/m) – per capire meglio, dobbiamo
pensare al fatto che avrò allocato uno spazio molto molto grande, per poi memorizzare,
effettivamente, un numero di dati molto basso.

ESEMPIO:
Facciamo finta di doverci occupare del sistema di immatricolazione della nostra università; sappiamo che
quasi tutte utilizzano, per le matricole, un numero di cifre pari a 6, quindi avremo un universo che va da 0 a
999999, di conseguenza la nostra tabella sarà pari a:
T[0,…,999999].
Supponiamo, poi, che il numero di studenti iscritti sia pari a 100:

Chiavi = studenti iscritti ad un corso (n=100).


Capiamo da soli che abbiamo allocato un numero altissimo di elementi per sfruttarne, alla fin fine, una
quantità irrisoria; avremo così un fattore di carico molto basso:
Fattore di carico = 10^-4.
Ovviamente ho un tempo di accesso costante, ma avrò anche un costo di memoria inaccettabilmente alto.
Da qui deduciamo che la tabella ad accesso diretto è più una possibilità teorica che una possibilità pratica.

TABELLA HASH
Abbiamo capito che le tabelle ad accesso diretto sono soltanto ipotizzabili teoricamente e non facilmente
applicabili nella pratica; la loro possibilità di applicazione è resa possibile dalle tabelle HASH.
La tabella HASH, infatti, è sempre una tabella basata su un vettore, ma, al contrario di quelle ad accesso
diretto, si basa sull’osservazione che soltanto una parte delle chiavi viene effettivamente utilizzato.
La tabella HASH, quindi memorizza in una tabella T di dimensione m, dove m, però, è molto più piccola del
nostro universo. T: m << |U|.
Come facciamo, però, a mappare un insieme di cardinalità di U su un insieme più piccolo di dimensione m?
Attraverso una funzione che definisce un indice h(k) (k è la mia chiave, h è la funzione) a partire da un
valore k, questa funzione è detta funzione di HASH.

Funzione di HASH = h:Uà{0,…,m-1} permette di trasformare valori dell’universo in un indice, relativamente


piccolo (più piccolo possibile poiché vogliamo utilizzare il quantitativo di spazio più piccolo possibile).

Giacomo Maccanti – g.maccanti@students.uninettunouniversity.net 73


ALGORITMI E PROGRAMMAZIONE AVANZATA - G.MACCANTI@STUDENTS.UNINETTUNOUNIVERSITY.NET

ESEMPIO:

Come vediamo dal nostro esempio, una tabella HASH ci permette di risparmiare molto spazio ma, tuttavia,
è possibile che due chiavi (ad esempio 3 e 19) collidano nella stessa posizione.

FUNZIONI DI HASH
Vediamo le caratteristiche che identificano le funzioni di HASH:

• Le funzioni di HASH non possono essere iniettive;


• Quando due chiavi finiscono nella stessa posizione, si parla di collisione – una collisione avviene
quando una chiave K1 e una chiave k2 (con valore diverso) sono tali per cui h(k1) = h(k2) (il valore di
HASH di K1 è uguale al valore di HASH di K2, quindi finiranno nella stessa posizione della tabella).
• Le collisioni non possono essere eliminate;
• Le collisioni possono essere ridotte usando appropriate funzioni di hash;
• Le funzioni devono essere obbligatoriamente gestite.

Funzioni di HASH: requisiti


• La funzione di HASH deve essere semplice, quindi h(K) deve essere calcolabile in O(1);
• La funzione di HASH deve essere uniforme, quindi h(K) deve distribuire le chiavi in modo uniforme.
Il criterio di uniformità semplice recita che: se le chiavi sono scelte da U secondo una distribuzione P e
consideriamo K come variabile causale, allora il concetto si descrive come:

Å K = 1/É
Ñ:Ü Ñ <;

J=0,1,…,m-1
Il problema è che tipicamente non è controllabile perché P è sconosciuto; In pratica si usano delle
approssimazioni.

GESTIONE DELLE COLLISIONI


Ricordiamo che per la gestione delle collisioni abbiamo:

• Dimensione della tabella = m;


• Numero di chiavi = n;
• Fattore di carico α = n/m.

Giacomo Maccanti – g.maccanti@students.uninettunouniversity.net 74


ALGORITMI E PROGRAMMAZIONE AVANZATA - G.MACCANTI@STUDENTS.UNINETTUNOUNIVERSITY.NET

I meccanismi per la gestione delle collisioni, sono essenzialmente due:

• Concatenazione;
• Indirizzamento aperto.

Gestione delle collisioni: concatenazione


(n >= m, α >= 1)
Elementi che collidono memorizzati in liste di collisione T[i] = chiavi con h(K)=i;
Ogni elemento della tabella contiene un puntatore a una lista di collisione.
Gli elementi che collidono sono memorizzati nella stessa lista, in generale: α >= 1.

Abbiamo una tabella di 10 elementi (da 0 a 9) e il nostro universo di n


elementi. La funzione di HASH è h(K) = k mod 10.

Abbiamo che 2 in mod 10 è 2, il resto è 2, quindi andiamo a


memorizzarlo in posizione 2.
Abbiamo che l’elemento 46 in mod 10 ha resto6, quindi andremo a
memorizzarlo in posizione 6.
Tutto funziona bene, finché non si verifica, come nel caso di 32, che la
sua funzione di HASH restituisca come resto della divisione, 2; questo
colliderà in posizione 2.

L’inserimento, di 32, come vediamo, avviene in testa, poiché inserisco


in coda avrebbe richiesto la scansione dell’intera tabella. Ad ogni
modo, non ho nessun motivo, al momento, per mantenere la lista
ordinata.

Gestione delle collisioni: indirizzamento aperto


(n <= m, α <= 1)
Tutti gli elementi sono memorizzati nella tabella; se una cella è occupata viene ricalcolata una nuova
posizione, in generale: α <= 1.

Stiamo dicendo che si procede per tentativi: ho una chiave, ne calcolo il valore di HASH, se la posizione
corrispondente di una tabella è libera, la inserisco, se la posizione è occupata, ricalcolo un altro valore.
Giacomo Maccanti – g.maccanti@students.uninettunouniversity.net 75
ALGORITMI E PROGRAMMAZIONE AVANZATA - G.MACCANTI@STUDENTS.UNINETTUNOUNIVERSITY.NET

Questo è apparentemente banale poiché si potrebbe supporre che il ricalcolo potrebbe avvenire su base
“casuale”; dobbiamo però fare attenzione al fatto che questa funzione viene utilizzata anche per cercare un
valore, quindi la sequenza di tentativi che viene prodotta deve essere ripetibile e riconducibile.

Di seguito alcune strategie per l’indirizzamento aperto che è possibile utilizzare:

• LINEAR PROBING;
• Quadratic probing;
• Double hashing.

La funzione di HASH dipende dalla posizione cercata: h = h(K,i)

h : U x {0,1,…,m-1} {0,1,…,m-1}

k = chiave
i = numero di tentativo (try)

A questo punto avremo che la funzione h() genererà una sequenza di probing (tentativi): permutazione
degli indici <0,1,2,…,m-1>

Sequenza di probing: <h(k,0), h(k,1),…,h(k,m-1)>

PRIMITIVE DI GESTIONE DELLE TABELLE HASH


Vediamo quali sono le operazioni che possiamo invocare sulle tabelle HASH:
• HashInsert;
• HashSearch;
• HashDelete

La HashInsert e la HashSearch seguono la sequenza di probing fino a che non si trova una posizione libera.
La HashDelete (cancellazione di un elemento) comporta la “rottura” della sequenza di probing, quindi
possiamo affermare che l’indirizzamento aperto non è adatto se è richiesta la cancellazione degli elementi”.

LINEAR PROBING
E’ forse il più facile tra tutti i meccanismi di probing; serve per valutare diverse posizioni generando una
certa sequenza di probing.

Questo è basato su un principio molto semplice:


h(k , i) = (h'(k) + i) mod m
Se trovo la posizione h(k) occupata, verifico se è libero quella immediatamente successiva.
h’(k) = funzione di HASH primaria.

Giacomo Maccanti – g.maccanti@students.uninettunouniversity.net 76


ALGORITMI E PROGRAMMAZIONE AVANZATA - G.MACCANTI@STUDENTS.UNINETTUNOUNIVERSITY.NET

PARADIGMI ALGORITMICI – PARTE I – PROGRAMMAZIONE DINAMICA


PROGETTO DI UN ALGORITMO
Per creare un buon algoritmo, dobbiamo studiare bene la struttura del problema, poiché quest’ultima può
essere usata per guidare il progetto dell’algoritmo.
Ovviamente, per risolvere un problema, cerchiamo un approccio che preveda algoritmi classificati secondo
un paradigma metodologico.

Paradigmi generali
• Divide and conquer;
• Ricerca ed enumerazione.

Paradigmi per problemi di ottimizzazione


• Programmazione dinamica;
• Paradigma greedy.

QUALE STRATEGIA SCEGLIERE?
A volte, proprio perché molto facile ed intuitivo, la soluzione divide et impera è quella più scelta; bisogna
stare attenti, però, poiché molto spesso questa scelta non è quella più efficiente in quanto ignora la
struttura del problema.

PROGRAMMAZIONE DINAMICA
La programmazione dinamica nasce proprio dal fatto che l’algoritmo divide-et-impera tende a generare un
numero esponenziale di sotto problemi quando, invece, il numero di sotto problemi è in genere molto
minore.
La programmazione dinamica evita di risolvere lo stesso sotto problema più volte attraverso la
memorizzazione delle soluzioni già compiute e che sono riutilizzabili.
Attenzione che questo tipo di algoritmo non sostituisce il divide-et-impera ma, semplicemente è un
paradigma che può essere implementato per problemi di ottimizzazione, quando si verificano:

• Più soluzioni;
• Soluzioni con “punteggio”;
• Cerca la soluzione con “punteggio” più alta.

Divide-et-impera e programmazione dinamica


Vediamo, nel dettaglio, le differenze che ci sono tra il paradigma divide-et-impera e la programmazione
dinamica:
Divide-et-impera

• Sottoproblemi indipendenti;
• Risolve ogni sottoproblema ricorsivamente in modo top-down.

Programmazione dinamica

• Osserva che i sottoproblemi non sono necessariamente indipendenti;


• Risolve ogni sottoproblema e memorizza la soluzione in una tabella (botto-up).

Giacomo Maccanti – g.maccanti@students.uninettunouniversity.net 77


ALGORITMI E PROGRAMMAZIONE AVANZATA - G.MACCANTI@STUDENTS.UNINETTUNOUNIVERSITY.NET

Programmazione dinamica, quando e come?


Dato un problema di ottimizzazione, quando possiamo applicare la programmazione dinamica?

• Quando ho sottoproblemi ripetuti, poiché solo se i sottoproblemi si ripetono c’è differenza


rispetto all’approccio diretto;
• Se la struttura ottima gode della sottostruttura ottima, ovvero la soluzione ottima al
problema consiste di soluzioni ottime ai sottoproblemi.
Come possiamo progettare un algoritmo?

• Caratterizzazione della struttura di una soluzione ottima (come è definita una soluzione
ottima? Gode della proprietà della sottostruttura ottima);
• Definizione ricorsiva del valore di una soluzione ottima (la soluzione ottima in funzione ai
sottoproblemi);
• Calcolo bottom-up del valore della soluzione ottima (memorizza soluzioni in una tabella a
partire da quelle ai problemi elementari);
• Costruzione di una soluzione ottima (solo se ci serve anche la soluzione e non solo il valore).

IL CAMBIO DI MONETE
DATI:

• Sia dato un intero n > 0 (pensiamo di andare ad un banco di cambio monete in una certa valuta
portando una certa quantità di euro).
• In quella valuta esistono un insieme di banconote D e queste hanno valori {d1,…,dm} (immaginate i
vari tagli da 5€, 10€, ecc.);
• Noi vogliamo calcolare il minimo numero di banconote in cui la quantità può essere cambiata.

Il cambio di monete: algoritmo più intuitivo


Scegli la banconota d, con valore più grande il cui valore non eccede n.
Nßn-d
Prosegui finché n=0.

Il cambio di monete: soluzione intuitiva non ottima


D = {12,8,1} e noi ci presentiamo con un assegno di n = 31
Scegli 12 n = 19 – AL PRIMO GIRO CI DA 12, POICHE’ CI STA IN 31 E MI RIMANE 19 DA DARE
Scegli 12 n = 7 – AL SECONDO GIRO CI DA ANCORA 12 PERCHE’ CI STA IN 19 E MI RIMANE 7
Scegli 1 n = 6 – NON POSSO DARTI 12, NEMMENO 8 PERCHE’ NON CI STANNO; NON HO ALTERNATIVE CHE DARTI 1
Scegli 1 n = 5
Scegli 1 n = 4
Scegli 1 n = 3
Scegli 1 n = 2
Scegli 1 n = 1
Scegli 1 n = 0

IN TOTALE 9 BANCONOTE
OVVIAMENTE QUESTA NON E’ UNA SOLUZIONE OTTIMA

Giacomo Maccanti – g.maccanti@students.uninettunouniversity.net 78


ALGORITMI E PROGRAMMAZIONE AVANZATA - G.MACCANTI@STUDENTS.UNINETTUNOUNIVERSITY.NET

Il cambio di monete: soluzione non intuitiva ma ottima


La soluzione ottima è quella che ci permette di avere 6 banconote: 12+8+8+1+1+1.
Vediamo che se io, la seconda volta, non ti avessi dato subito in modo avido (greedy) la banconota da 12,
avrei potuto, valutando le conseguenze, capire che avrei avuto una soluzione migliore.

Il cambio di monete: applicazione della programmazione dinamica


ALGORITMO: FASE 1
Caratterizzazione della soluzione ottima; in questa fase vogliamo vedere, se possibile, caratterizzare la
nostra soluzione ottima. Questo passa attraverso la dimostrazione che il problema da risolvere gode della
proprietà della sottostruttura ottima.

Supponiamo che io abbia un certo n > 0 (quantitativo da cambiare) e che, come riportato in figura, le
banconote (5 banconote) siano esattamente il cambio migliore che mi può essere fornito:

Quindi abbiamo per b e per n-b due soluzioni ottime; possiamo risolvere la nostra ricorrenza dal basso.

ALGORITMO: FASE 2
Scriviamo la ricorrenza attraverso la definizione ricorsiva della soluzione ottima.

C(p) = minimo numero di banconote per cambiare un valore p.

Leggiamo C(p):
• caso terminale – per p = 0, C(p) vale 0. Ovvio che se dobbiamo totalizzare una quantità pari a 0,
qual è il numero minimo di monete per totalizzare 0? Ovviamente 0;
• Per p != 0 – scegliamo la più piccola moneta tra tutte le monete che possono variare la mia
quantità.

ALGORITMO: FASE 3
Change (d[],n){ //d E’ IL VETTORE DELLE MONETE, N LA QUANTITA’ DA
CAMBIARE
C(0) 0 //IL VETTORE c VIENE IMPOSTATO A 0 CHE E’ IL VALORE
TERMINALE
for p 1 to n //PER p CHE VA DA 1 A n – QUINDI n ITERAZIONI
min //INIZIALIZZIAMO AD UN VALORE MOLTO GRANDE
for all d[i] <= p //PER TUTTI I VALORI DI MONETE CHE STANNO IN p
if 1 + C(p−d[i]) < min then //SE LA SOLUZIONE DEL MIO PROBLEMA E’ MINORE RISPETTO
AL MINIMO ATTUALE
min 1 + C(p − d[i]) //ALLORA AGGIORNA IL MINIMO

Giacomo Maccanti – g.maccanti@students.uninettunouniversity.net 79


ALGORITMI E PROGRAMMAZIONE AVANZATA - G.MACCANTI@STUDENTS.UNINETTUNOUNIVERSITY.NET

coin i
C(p) min
}

Il cambio di monete: esempio di applicazione alla programmazione dinamica


Abbiamo le nostre monete D e la nostra quantità da cambiare n:
D = {12,8,1}, n = 31

C vale zero:
C(0)=0

Parte il ciclo for che va da p = 1 a 31; attualmente abbiamo solo la soluzione per C che vale 0, quindi, qual è
l’unica monete che può cambiare 1? Ovviamente 1 perciò d[1]=1:
p=1 d[1]=1

C(1) è il minimo di tuti i valori possibili (ne ho solo uno possibile al momento) e applico quindi la ricorrenza;
scelto la moneta che ha costo 1 più la soluzione ottima del sottoproblema residuo:
C(1)=min(1+C(1-1)) = 1

Da 1 a 7 non cambierà molto, poiché 1 è l’unica moneta che, al momento, può fare la differenza:
p=7 d[1]=1
C(7) =min(1+C(7-1)) = 7

Le cose cambiano valutando C(8), perché applichiamo effettivamente la programmazione dinamica, che
valuta le opzioni e ci dà le varie possibilità:
p=8 d[1]=1
C(8) =min(1+C(8-1)) = 8

d[2]=8
C(8) =min(1+C(8-8)) = 1

Giacomo Maccanti – g.maccanti@students.uninettunouniversity.net 80


ALGORITMI E PROGRAMMAZIONE AVANZATA - G.MACCANTI@STUDENTS.UNINETTUNOUNIVERSITY.NET

PARADIGMI ALGORITMICI – PARTE II – ALGORITMI GREEDY


IL PARADIGMA GREEDY
Abbiamo visto, per ora, che la programmazione dinamica è intrinsecamente esaustiva per la risoluzione
ottima (cambia monete) ma dobbiamo anche ammettere che, molto spesso, un approccio di tipo greedy è
bastevole per ottenere, ugualmente, una soluzione ottima.
Una strategia greedy (avido o gretto) prevede un algoritmo che, ad ogni passo per la costruzione della
soluzione ottima, prende la decisione che appare localmente ottima senza valutare tutte le possibili
conseguenze e tutte le possibili scelte.
La strategia greedy è applicabile a problemi di ottimizzazione la cui soluzione consiste di una serie di
decisioni. Gli algoritmi greedy scelgono l’ottimo locale senza valutare l’impatto sull’ottimo globale.
Stiamo dicendo, quindi, che un algoritmo greedy è molto poco complesso e molto intuitivo; la brutta notizia
è che pochi problemi sono risolvibili in modo ottimo con questo paradigma ma, in casi in cui ci siano grossi
problemi da risolvere, si utilizzano le euristiche greedy.

ALGORITMI GREEDY: STRUTTURA GENERALE


La risoluzione generale di un algoritmo greedy prevede che la soluzione sia un insieme di oggetti con un
costo associato.
La soluzione complessiva viene ottenuta in modo incrementale scegliendo ad ogni passo l’oggetto con il
costo minore compatibile con la soluzione attuale.
Possiamo costruire due schemi (template):

Algoritmi greedy: struttura generale – schema 1


I costi (a1,a2,…,an) non sono modificati dalle scelte. Un oggetto ha costo statico, quindi sempre quel valore.
Greedy1 (a1, a2,…an)
S {}
//ordina ai in ordine crescente
for (ogni ai in ordine) do
if (ai può essere aggiunto a S) then
S S ai return S

Algoritmi greedy: struttura generale – schema 2


I costi (a1,a2,…,an) sono modificati dalle scelte. Un oggetto ha costo dinamico, quindi può essere
modificato dalle scelte fatte.

Greedy2 (a1, a2,…an)


S {}
while (ci sono elementi) do
//scegli ai con costo più basso
if (ai può essere aggiunto a S) then
S S ai
//aggiorna i costi ai
return S

Giacomo Maccanti – g.maccanti@students.uninettunouniversity.net 81


ALGORITMI E PROGRAMMAZIONE AVANZATA - G.MACCANTI@STUDENTS.UNINETTUNOUNIVERSITY.NET

ESEMPI DI ALGORITMI GREEDY

Il problema dello zaino


Dati n oggetti, ciascuno con:
• Peso Wi – peso;
• Valore Ci – costo.
Il nostro scopo è quello di caricare uno zaino in modo da massimizzare il valore totale senza eccedere una
capacità massima di W.
Immaginiamoci un ladro che debba rubare quante più cose possibili senza sfondare lo zaino in cui le
introduce, massimizzando il valore di ciò che sta rubando.
Assumiamo che sia possibile prendere frazioni di ogni oggetto (ad esempio pensiamo che il nostro ladro stia
rubando delle polveri) detto anche zaino continuo o frazionario.
ESEMPIO
Abbiamo un semplice caso con 3 oggetti, ognuno con un valore ed un peso ed il loro valore unitario (costo
per unità di peso). Il peso massimo che può portare lo zaino (la nostra capacità) è di 55Kg.
Abbiamo anche sviluppato un ranking:
1. Quello che costa di più in assoluto (valore unitario alto);
2. Quello che ha valore di più in assoluto (valore alto);
3. Quello che ha peso maggiore in assoluto (peso).

Applichiamo il paradigma greedy in base a diversi esempi:

Giacomo Maccanti – g.maccanti@students.uninettunouniversity.net 82


ALGORITMI E PROGRAMMAZIONE AVANZATA - G.MACCANTI@STUDENTS.UNINETTUNOUNIVERSITY.NET

Abbiamo l’applicazione diretta dello schema 1, poiché:

• Ordiniamo gli oggetti per valore unitario;


• Sceglie in ordine fino a saturare le capacità dello zaino.

OSSERVAZIONE
La versione del problema in cui un oggetto si prende interamente non è risolvibile in modo ottimo con una
scelta greedy è richiesta la risoluzione della programmazione dinamica.

Selezione di attività
E’ un problema che fa parte di scheduling, quindi assegnazione di attività nel tempo.
Il problema prevede n attività S={1,2,…,n} in competizione tra loro per l’utilizzo di una certa risorsa.

Noi dobbiamo determinare il massimo sottoinsieme di attività per l’utilizzo della risorsa senza conflitti.
In pratica stiamo dicendo: quante attività possiamo assegnare a questa macchina senza che si verifichino
dei conflitti, cioè non è possibile che due attività simultaneamente utilizzino questa macchina?

Ad ogni attività i Є S sono associati:


• Si = tempo di inizio;
• Fi = tempo di fine.
Una attività inizia prima di finire, quindi Si<=Fi.

Due attività i e j sono compatibili se gli intervalli [si,fi] e [sj,fj] sono disgiunti, ovvero fj ≤ si (attività j finisce
prima che inizi l’attività i) oppure fi ≤ sj (attività i finisce prima che inizi l’attività j).

Selezione di attività: pseudocodice


Activity-Selection(S,F){
A {1}
j 1
for i 2 to n do
if S[i] F[j] then
A = A {i}
j = i
}

Giacomo Maccanti – g.maccanti@students.uninettunouniversity.net 83


ALGORITMI E PROGRAMMAZIONE AVANZATA - G.MACCANTI@STUDENTS.UNINETTUNOUNIVERSITY.NET

Selezione di attività: esempio

Attività 1 finisce a 5, attività 2 finisce a 4, ecc.

Pensiamo, ora, di ordinarle per ordine di fine, vediamo


che la prima a finire è 2 che diventa l’attuale j (F[j] = 4
poiché finisce a 4) .

Preleverò di nuovo in tempo di fine, quindi vedo che 1


non va bene poiché finisce a 5, due è già occupato, tre va
bene perché inizia e finisce dopo due, così come 6.

L’algoritmo fornisce la risposta dicendoci che il massimo di attività compatibili è tre e sono:2, 3 e 6.

STRATEGIA GREEDY E OTTIMALITA’


Che caratteristiche deve avere un problema per essere risolto in modo ottimo con un algoritmo greedy?

I problemi risolubili in modo ottimo da una strategia greedy devono godere di due proprietà fondamentali:

• Sottostruttura ottima;
• Proprietà della scelta greedy.

Sottostruttura ottima
Già la conosciamo, una sottostruttura ottima prevede che tutti i sottoproblemi siano risolti in modo
ottimale con un approccio bottom-up.

Proprietà della scelta greedy


La proprietà dice che l’ottimo globale è ottenuto da una sequenza di scelte localmente ottime. Cioè
qualunque sia la scelta locale si passa a risolvere il sottoproblema risultante. Ovvero, una volta risolto in
maniera ottimale il problema locale, non valutiamo tutte le ulteriori alternative, ma passiamo direttamente
al problema successivo.

• La scelta di un algoritmo greedy è un algoritmo che può dipendere dalle scelte fatte in precedenza;
prendiamo come esempio quello fatto sulla Selezione di attività dove notiamo che, una volta scelto
il due o il tre, quella scelta influenzerà le scelte future;
• Non può dipendere da tutte le soluzioni al sottoproblema;
• Dopo aver preso una decisione questa non viene più riconsiderata; pensiamo sempre al problema
sella selezione di attività, una volta scelto il due, questo non verrà più considerato.

Giacomo Maccanti – g.maccanti@students.uninettunouniversity.net 84


ALGORITMI E PROGRAMMAZIONE AVANZATA - G.MACCANTI@STUDENTS.UNINETTUNOUNIVERSITY.NET

PARADIGMI ALGORITMICI – PARTE III – BACKTRACKING


Paradigmi basati sulla ricerca
Fino ad ora abbiamo potuto vedere come i paradigmi algoritmici si basino su una certa struttura ottima, ma
se per caso un problema di grandi dimensioni, non godesse di questa proprietà?
Dobbiamo, infatti, pensare che in molti problemi l’unica opzione è quella di verificare tutte le possibilità.
Quando diciamo “tutte le possibilità” ci stiamo riferendo allo spazio delle soluzioni. Stiamo quindi dicendo
che in assenza di alternative bisogna esplorare il nostro spazio delle soluzioni.
Abbiamo due possibilità:
• Ricerca esplicita;
• Ricerca implicita.

Ricerca esplicita (brute force)


La ricerca esplicita utilizza un approccio di brute force (forza bruta) che prevede di enumerare
esaustivamente tutti i canditati, quindi tutte le soluzioni fino a trovare quella necessaria; ovviamente il
costo è proporzionale al numero di soluzioni candidate.

ESEMPIO
Se io dovessi calcolare tutti i divisori di un numero n?

Soluzione
Enumera tutti gli interi da 1 a n e verifica che dividano esattamente n.

Ricerca implicita
Analizziamo con più cura lo spazio delle soluzioni e “potiamo” (pruning) quelle non necessarie. Implica
usare proprietà dello spazio delle soluzioni.

ESEMPIO
Calcolare tutti i divisori di un numero n.

SOLUZIONE
Enumera tutti gli interi da 1 a n (saltando i multipli di quelli che hai già verificato) e verifica che dividano
esattamente n.

SPAZIO DELLE SOLUZIONI


Il processo di soluzione viene rappresentato da un albero di decisione.
L’albero rappresenta diverse soluzioni e non la soluzione del problema.

Abbiamo due tipologie di nodo all’interno del nostro albero:


• Nodi interni à decisioni;
• Foglie à soluzioni candidate.

ESEMPIO
Vogliamo enumerare tutte le parole di 3 bit con 2 o più “1” (000,001,010,…,111).
Al momento, non essendoci una soluzione immediata, dobbiamo enumerare tutte le possibilità.
Essendoci 3 bit e solo due valori, abbiamo 2^3 = 8 possibilità = spazio di ricerca.

Organizziamo il nostro problema come albero di decisione:

Giacomo Maccanti – g.maccanti@students.uninettunouniversity.net 85


ALGORITMI E PROGRAMMAZIONE AVANZATA - G.MACCANTI@STUDENTS.UNINETTUNOUNIVERSITY.NET

La radice rappresenta i tre bit (tre trattini).

Dalla radice passiamo al primo nodo, a cui assegniamo il bit


più a sinistra, cioè 0. L’arco rappresenta l’assegnazione.

Arriviamo alla foglia, dove otteniamo un valore specificato


di tre bit.

Se torniamo indietro (backtracking), assegniamo al figlio


destro, l’altro valore assumibile, cioè 1.

Cosa osserviamo, a livello operativo, di questo albero? Che ad ogni livello abbiamo sempre due decisioni,
una decisione che associa sempre l’iesimo valore a 0 (sinistra) e a 1 (destra). Notiamo che ogni livello del
nostro albero specifica un bit successivo; primo livello bit 1, secondo livello bit 2 terzo livello bit 3. L’altezza
dell’albero è uguale per ogni ramo ed è pari a 3.

Ovviamente, per quanto riguarda la costruzione di un programma per risolvere questo problema, ci pare
anche troppo complicato un albero decisione; basterebbe, infatti, tre cicli for per scorrere tutte le varianti.
Pensiamo, però, a n elementi; cosa facciamo? Inseriamo n cicli for nel nostro algoritmo? Se avessimo 1000
elementi? 1000 cicli for?

ALGORITMO DI BACKTRACKING
Il backtracking è la parola inglese per “tornare indietro”. Pensate ad una serie di strade, diramate, le
percorriamo tutte per raggiungere la nostra meta, la nostra soluzione, ma arriviamo ad una strada a fondo
chiuso, dobbiamo allora tornare indietro (backtracking). Backtracking è il nome che diamo ad una variante
dell’approccio “brute force” in cui viene sistematicamente esplorato lo spazio delle soluzioni.
Sistematicamente significa visitare in preordine l’albero delle decisioni.
Pensiamo, prima di tutto, alla soluzione finale, ovvero ad un vettore di n oggetti composto da elementi a(1),
a(2),…,a(n).

Ogni a(i) viene scelto da un insieme ordinato s.


Lo schema generale costruisce questa soluzione chiamata V, vuota; quindi inizialmente avremo v= (). Ad
ogni passo (nel nostro albero attuale corrisponde ad un livello) lo estende con un nuovo valore v =
(a(1),a(2),…,a(k-1)) à al kesimo passo noi aggiungeremo un elemento v = (a(1),a(2),…,a(k-1),a(k)).
Ad un certo punto, ricorrendo, ci troveremo in un altro nodo e, questa soluzione parziale, dobbiamo vedere
se è accettabile o meno; è già un passo avanti rispetto al brute force dove invece continua anche se non ha
più senso continuare. Se la soluzione parziale è accettabile continuo (quindi scelto un altro a(k)+1,
altrimenti, se non va bene, cancella la scelta corrente a(k) e ne prova un'altra. Se le possibili scelta di a(k)
sono esaurite si fa backtrack e si effettua la prossima scelta per a(k-1).

Giacomo Maccanti – g.maccanti@students.uninettunouniversity.net 86


ALGORITMI E PROGRAMMAZIONE AVANZATA - G.MACCANTI@STUDENTS.UNINETTUNOUNIVERSITY.NET

Algoritmo di backtracking: pseudocodice


i = generico candidato;
k = livello di ricorsione;
m = n. di candidati ad ogni livello (i=1,…,m);
n = dimensione della soluzione (n candidati);
v = soluzioni.
Backtrack (k){ //k E’ IL PRIMO LIVELLO DELL’ALBERO
if (k>n) return //SE k > n TERMINIAMO SUBITO IL CICLO
i 0 //INIZIALIZZO I DATI – i SONO LE DECISIONI CHE VANNO DA 1 A m
do
i i+1 //LA PRIMA VOLTA CHE ENTRIAMO, FACCIAMO LA PRIMA SCELTA
if (accettabile) //SE LA SCELTA CI PUO’ ANDARE BENE RISPETTO AL PROBLEMA
a(k) i-esimo candidato
aggiungi a(k) a v //AGGIUNGO LA SOLUZIONE ALL’INSIEME DELLE SOLUZIONI
Backtrack(k+1) //PASSO AL NUOVO LIVELLO
if (successo) //SE ALLA FINE ABBIAMO AVUTO SUCCESSO
elimina a(k) da v //ELIMINO a(k) DALLA SOLUZIONE
while (insuccesso and i m)//RIMANDO NEL WHILE FINCHE’ NON TROVO UNA SOLUZIONE E i != m
}

Algoritmo di backtracking: calcolo di tutte le soluzioni


Con minime modifiche lo schema può gestire il caso di:

• Tutte le soluzioni;
• Una soluzione ottima.
E’ sufficiente eliminare le condizione di successo dal ciclo.
Algoritmo di backtracking calcolo di tutte le soluzioni: pseudocodice
Backtrack (k)
for (i = 1 …m)
scegli il k-esimo candidato
if (accettabile)
memorizzalo
if (k < n)
Backtrack (k + 1)
else
stampa la soluzione
cancella dalla soluzione
endfor

Algoritmo di backtracking: calcolo di una soluzione ottima


E’ sufficiente memorizzare la migliore tra tutte. Definiamo l’ottimalità in termini di una funzione a valori
positivi f().
Algoritmo di backtracking calcolo di una soluzione ottima: pseudocodice
Backtrack (k)
for (i = 1 …m)
scegli il k-esimo candidato
if (accettabile)
memorizzalo
if (k < n)
Backtrack (k + 1)
else

Giacomo Maccanti – g.maccanti@students.uninettunouniversity.net 87


ALGORITMI E PROGRAMMAZIONE AVANZATA - G.MACCANTI@STUDENTS.UNINETTUNOUNIVERSITY.NET

if (f(solution) > f (ottimo)


ottimo soluzione
cancella dalla soluzione
endfor

Giacomo Maccanti – g.maccanti@students.uninettunouniversity.net 88


ALGORITMI E PROGRAMMAZIONE AVANZATA - G.MACCANTI@STUDENTS.UNINETTUNOUNIVERSITY.NET

I GRAFI – PARTE I
I GRAFI
In molte applicazioni non sono rilevanti gli oggetti di un insieme, ma anche le relazioni tra di esse.
Pensate ad una mappa, dove gli oggetti potrebbero essere i punti di riferimento di questa mappa (casa,
lavoro, supermercati, aeroporti, ecc..) chiaramente ci interessa la relazione che ci sono tra questi oggetti,
tra queste località come l’esistenza di connettività: esiste una strada, un percorso, ecc. Possiamo fare
numerosi esempi di questo tipo. Pensate a internet; ci sono innumerevoli server sparsi in tutto il mondo e
noi abbiamo bisogno di avere determinate metriche, come la banda, tra un punto e l’altro.

DEFINIZIONI E PROPRIETA’
Un grafo G può essere visto come una coppia (V,E) dove:
• V è l’insieme dei vertici – disegnati con i cerchi;
• E è l’insieme degli archi – disegnati con le rette.

V = {A,B,C,D,E,F}

E = {(A,B),(A,D),(B,C),(C,D),(C,E),(D,E)
Vediamo che gli archi sono indicati con i vertici che mettono
in relazione; non tutti gli oggetti sono in relazioni tramite gli
archi: il vertice F non è in relazione con nessun altro.
Un arco è una coppia di vertici (v,w) per cui v є V e w Є V;
stiamo appunto dicendo che un arco viene identificato con
la notazione tra parentesi e tramite i vertici che esso connettono separati da virgola.

Definizioni e proprietà: grafi orientati


I grafi orientati, sono grafi che hanno archi con una particolare direzione.
I grafi G(V,E) in cui l’insieme di archi E è una relazione binaria tra vertici.

Relazione binaria significa che è una relazione collega un singolo vertice ad un singolo altro vertice.

V = {A,B,C,D,E,F}
E = {(A,B),(A,D),(B,C,),(D,C),(E,C),(D,E),(D,A),(F,F)}
I grafi orientati possono rappresentare un certo tipo di
informazione e relazione tra oggetti, intesa come senso di
dipendenza: A dipende da B o viceversa.
Gli archi (A,D) e (D,A) sono archi distinti.

Giacomo Maccanti – g.maccanti@students.uninettunouniversity.net 89


ALGORITMI E PROGRAMMAZIONE AVANZATA - G.MACCANTI@STUDENTS.UNINETTUNOUNIVERSITY.NET

Definizioni e proprietà: grafi non orientati


I grafi non orientati, sono grafi che non hanno archi con una particolare direzione.
I grafi G(V,E) in cui l’insieme di archi E è un insieme non ordinato; questo significa che la relazione tra gli
archi è una relazione non definita nella direzione. Ad esempio, l’arco (A,D) e (D,A) sono la stessa cosa
poiché niente mi specifica la direzione (potrebbe essere un arco bidirezionale).

V = {A,B,C,D,E,F}
E = {(A,B),(A,D),(B,C,),(C,D),(C,E),(D,E)}
Gli archi (A,D) e (D,A) sono lo stesso arco.

Vediamo, ora, le relazioni tra archi e vertici.

Definizioni e proprietà: incidenza


Se e =(w,v) Є E (si legge: se prendiamo un arco e(w,v) appartenente all’insieme degli archi) si dice che:

• Grafo orientato: è incidente dal vertice w al vertice v;


• Grafo non orientato: è incidente sui vertici w e v.

Definizioni e proprietà: adiacenza


Un vertice w è adiacente ad un vertice v se (v,w) Є E.

Notazione: vàw – significa che v è adiacente a w


BàA – B adiacente ad A;
CàB – C adiacente a B;

B /->D – B non è adiacente a D.

In un grafo non orientato la relazione di adiacenza è simmetrica:


AàB implica BàA.

Definizioni e proprietà: incidenza e adiacenza


• Adiacenza: relazione tra due vertici - “v e w sono adiacenti”;
• Incidenza: relazione tra un vertice ed un arco - “il vertice v e l’arco e sono incidenti”.

Giacomo Maccanti – g.maccanti@students.uninettunouniversity.net 90


ALGORITMI E PROGRAMMAZIONE AVANZATA - G.MACCANTI@STUDENTS.UNINETTUNOUNIVERSITY.NET

Definizioni e proprietà: grado di un vertice


• Grafo non orientato: il grado di un vertice è il numero di archi ad esso incidenti.

• Grafo non orientato: distinguo tra:


o Grado entrante – n° di archi entranti;
o Grado uscente – n° di archi uscenti.

Definizioni e proprietà: grafi pesati


Grafo in cui un’etichetta (peso) è associata ad ogni arco.
Peso definita da funzione: w: EàR
R: insieme numerico (interi o reali).

Se due vertici non sono uniti da un arco, si assume peso infinito.

Definizioni e proprietà: sottografo


Un sottografo di un grafo G = (V,E) è un grafo H = (V*,E*) tale che V* (sottoinsieme) E* e.
Stiamo dicendo che un sottografo non è altro che una porzione di un grafo ottenuta prendendo un
sottoinsieme dei vertici e un sottoinsieme degli archi.
N.B. essendo che deve essere un grafo significativo (non deve avere archi scollegati) deve valere la
seguente relazione:
H è un grafo, quindi deve valere E* V* x V*.

Giacomo Maccanti – g.maccanti@students.uninettunouniversity.net 91


ALGORITMI E PROGRAMMAZIONE AVANZATA - G.MACCANTI@STUDENTS.UNINETTUNOUNIVERSITY.NET

ESEMPIO DI SOTTOGRAFO H:

Sottoinsieme vertici originali

Sottoinsieme archi originali

Il sottografo ha senso poiché abbiamo conservato le


relazioni e i vertici del grafo originale.

Definizioni e proprietà: sottografo indotto


Dato un sottoinsieme V* V dei vertici di G = (V, E) Il sottografo di G indotto da V* è il grafo H = (V*, E*)
tale che: E* = {(w,v) E | w,v V*}
Stiamo dicendo che prendiamo solo archi che appartengono e uniscono quei vertici selezionati.

ESEMPIO DI SOTTOGRAFO INDOTTO:

Vediamo che abbiamo un sottografo


(V*) composto dai vertici A,C,D,
automaticamente vengono indotti
(selezionati) tutti gli archi che
collegano questi vertici.

Definizioni e proprietà: percorso o cammino


Un percorso in un grafo G = (V, E) è una sequenza <w1,w2,…,wn> di vertici tale che (wi, wi+1) ∈ E per 1 ≤ i ≤
n–1.

Un cammino non è altro che un insieme di vertici e relativi archi che gli uniscono.

La lunghezza di un cammino <w1,w2,…,wn> è n–1 (numero di archi che compongono quel cammino).

In arancione abbiamo selezionato il nostro cammino


<A,B,C,E>, quindi saranno contenuti i vertici A,B,C,E, così
come gli archi A,B – B,C, - C,E.

Distinguiamo il cammino tra:

Giacomo Maccanti – g.maccanti@students.uninettunouniversity.net 92


ALGORITMI E PROGRAMMAZIONE AVANZATA - G.MACCANTI@STUDENTS.UNINETTUNOUNIVERSITY.NET

• Cammino semplice – se tutti i vertici sono distinti;


• Cammino ciclo – se il primo e l’ultimo vertice coincidono.

<A,B,C,E>: semplice;
<A,B,C,E,D,C>: non semplice perché passo da c due volte;
<A,B,C,D,A>: ciclo, poiché ritorno al vertice di partenza.

Definizioni e proprietà: ciclo


Un grafo che non contiene cicli, si dice aciclico. Un esempio è il DAG (Directed Acyclic Graph) , ovvero un
grafo orientato e aciclico.

Definizione e proprietà: raggiungibilità


Se esiste un percorso p tra i vertici v e w, si dice che w è raggiungibile da v attraverso p.
P
Notazione: VàW – si legge che w è raggiungo da v attraverso p.

Un grafo non orientato G è connesso se esiste un percorso tra ogni coppia di vertici (tutte le possibili coppie
di vertici sono connesse tramite un cammino).

Non connesso poiché i cinque vertici (A,B,C,D,E) sono


tra loro connessi, ma non sono connessi a F.

Connesso poiché i sei vertici (A,B,C,D,E,F) sono tra


loro connessi.

Un grafo G è fortemente connesso se esiste un


percorso tra ogni coppia di vertici.

Giacomo Maccanti – g.maccanti@students.uninettunouniversity.net 93


ALGORITMI E PROGRAMMAZIONE AVANZATA - G.MACCANTI@STUDENTS.UNINETTUNOUNIVERSITY.NET

Un grafo completo è un grafo con un arco per ogni coppia di vertici.


Un grafo completo, con n vertici, possiede:

5 vertici à 10 archi

Definizione e proprietà: alberi


Un albero libero è un grafo non orientato, connesso e aciclico. Se un vertice è designato come vertice
“iniziale” (radice) l’albero si dice radicato.

Giacomo Maccanti – g.maccanti@students.uninettunouniversity.net 94


ALGORITMI E PROGRAMMAZIONE AVANZATA - G.MACCANTI@STUDENTS.UNINETTUNOUNIVERSITY.NET

RAPPRESENTAZIONE DI GRAFI
Per quanto riguarda i nostri grafi dobbiamo raffigurare i vertici e la loro connettività, noi abbiamo bisogno
di due rappresentazioni canoniche:
• Matrice di adiacenza;
• Lista di adiacenza.

Rappresentazione di grafi: matrice di adiacenza


La matrice di adiacenza è il modo più intuitivo per rappresentare un grafo. Costruiamo una matrice in cui, al
suo interno, memorizziamo l’informazione di adiacenza

Avremo 1, se esiste l’arco (v,w) Є E, 0


altrimenti.

Vedete che l’arco che collega il vertice


A ad A, non esiste; quindi 0.

L’arco che collega il vertice A a B,


esiste; quindi 1.

L’arco che collega il vertice A a C, non


esiste; quindi 0.

(informazione tra due vertici).

Rappresentazione di grafi: lista di adiacenza


La lista di adiacenza è molto meno intuitiva, ma ci consente di risparmiare molto più spazio.
Essa può essere vista come un vettore di |V| (cardinalità di V, tante liste quanti sono i vertici) liste, ciascuna
contenente la lista di adiacenza di un vertice.

Abbiamo un vettore in cui c’è un elemento che


rappresenta un puntatore di una lista, uno per
ogni vertice (detta lista), e a sua volta si
ricostruisce l’adiacenza tra tutti i vertici.

Attenzione che in un grafo orientato, scriveremo


soltanto gli archi tra i vertici in cui questi
puntano.

Rappresentazione di grafi: efficienza


• Matrice di adiacenza: conveniente per grafi densi (molti archi);
• Lista di adiacenza: conveniente per grafi sparsi (pochi archi).

ADT GRAFO
Vediamo come implementar il grafo come tipo di dato astratto.

Giacomo Maccanti – g.maccanti@students.uninettunouniversity.net 95


ALGORITMI E PROGRAMMAZIONE AVANZATA - G.MACCANTI@STUDENTS.UNINETTUNOUNIVERSITY.NET

Intanto, come prima cosa, dobbiamo definire il tipo di implementazione da utilizzare e poi le eventuali
strutture dati tipiche.

Implementazione di un grafo tramite lista di adiacenza


Essendo che in generale i grafi non sono molto densi, decidiamo di utilizzare come implementazione per il
nostro grafo, la Rappresentazione di grafi: lista di adiacenza.
La lista di adiacenza viene implementata come lista bidimensionale, ovvero una prima lista lineare che
contiene i vertici e una seconda lista che rappresenta, per quel vertice, una lista dei vertici ad esso adiacenti
(in pratica rappresenta le informazioni dell’arco corrispondente).

Implementazione di un grafo tramite lista di adiacenza: definizione del tipo


Abbiamo due diverse struct per archi e vertici
VERTICI
typedef struct graphnode *NodePtr; //NodePtr IDENTIFICA UN PUNTATORE ALLA STRUCT graphnode
typedef struct graphnode { //DICHIARAZIONE DELLA STRUCT graphnote (VERTICI)
char *Name; //IDENTIFICA IL VERTICE: A,B,C,ecc.
// altri eventuali campi
NodePtr Next; //PUNTATORE ALLA LISTA DEI VERTICI
EdgePtr AdjList; //PUNTATORE CHE RAPPRESENTA L’INFORMAZIONE ASSOCIATA ALL’ARCO
} Node //IDENTIFICA UN ELEMENTO DEL graphnode

ARCHI
typedef struct edge *EdgePtr; //EdgePtr IDENTIFICA UN PUNTATORE ALLA STRUCT edge
typedef struct edge { //DICHIARAZIONE DELLA STRUCT edge (ARCHI)
int Weight; //PESO DELL’ARCO – NON ESISTE SE IL GRAFO NON E’ PESATO
// altri eventuali campi
NodePtr NextNode; //PUNTATORE CHE INDICA QUESTO ARCO QUALE VERTICI CONNETTE
EdgePtr NextEdge; //PUNTATORE CHE COLLEGA ALL’ARCO SUCCESSIVO
} Edge;

struct node

Rappresenta la lista dei vertici; tanti elementi in


questa lista quanti i vertici del grafo.

Nelle liste verticali ci sono le informazioni sulle liste


di adiacenza.

Nextedge connetterà a successivi archi.

Ricordiamo che questo è un arco, ci dice da quale


vertice è connesso a quale connetterà.

struct edge

Giacomo Maccanti – g.maccanti@students.uninettunouniversity.net 96


ALGORITMI E PROGRAMMAZIONE AVANZATA - G.MACCANTI@STUDENTS.UNINETTUNOUNIVERSITY.NET

ESEMPIO DI GRAFO ORIENTATO

Vertice

Lista adiacenza

Vedete come il vertice A, attraverso il suo


Adjlist, mi connetta alla lista di adiacenza,
quindi al mio arco.

Il mio arco (lista di adiacenza) a sua volta mi


dice qual è il vertice che collega attraverso il
puntatore Nextnode.

PRIMITIVE DI GESTIONE DEI GRAFI


Le operazioni di base di un grafo, sono:
• RicercaNodo() – ricerca un vertice;
• InserisciEdge() – inserisce un arco;
• CancellaNodo() – cancella vertice;
• CancellaEdge() – cancella arco.

Non è richiesta nessuna speciale struttura dati per le liste di adiacenza. Tutte le primitive richiedono ricerca,
inserimento, cancellazione da/in una lista bidimensionale.

VISITE DI UN GRAFO
Per visita di un grafo intendiamo l’attraversamento di tutti i suoi vertici.
Le applicazioni sono molto importanti, pensiamo ad esempio alla necessità di capire se un certo vertice
raggiunge un dato vertice, oppure verificare l’esistenza di cicli, o, ancora, verificare la connettività.

Visite di un grafo: tipi di visita


Partendo da un nodo (sorgente) abbiamo due opzioni:

• Visita in ampiezza: scopre tutti i vertici a distanza n (n° di archi) dalla sorgente prima di scoprire
quelli a distanza n+1;
• Visita in profondità: scopre tutti i vertici adiacenti al vertice scoperto per ultimo.

Giacomo Maccanti – g.maccanti@students.uninettunouniversity.net 97


ALGORITMI E PROGRAMMAZIONE AVANZATA - G.MACCANTI@STUDENTS.UNINETTUNOUNIVERSITY.NET

Visite di un grafo: visita in ampiezza


Vediamo che abbiamo il nostro grafo orientato:

Abbiamo detto che si parte scoprendo i vertici vicini alla sorgente e poi quelli a distanza n+1 (quindi quelli
adiacenti a quelli adiacenti):

Scopro i vertici adiacenti alla sorgente, quindi B e D:

Dopodiché scopro quelli adiacenti a B e D, quindi per B sarà C e per D sarà ancora C ed E; per ultimo
vediamo che C può visitare nuovamente E, già visitato da D, e F, non ancora visitato:

In ordine, in vertici vengono visitati: A,B,D,C,E,F.

Giacomo Maccanti – g.maccanti@students.uninettunouniversity.net 98


ALGORITMI E PROGRAMMAZIONE AVANZATA - G.MACCANTI@STUDENTS.UNINETTUNOUNIVERSITY.NET

Visite di un grafo: visita in profondità


Vediamo che abbiamo il nostro grafo orientato:

Partendo da A, visitiamo B:

Da B, possiamo visitare solo C, dopodiché da C possiamo andare o in D o in E; abbiamo detto che si procede
in ordine alfabetico, quindi andiamo verso E:

Con E non posso andare da altre parti, quindi torno a C, che va a F, che non può continuare quindi torno a
C, che non può continuare, quindi torno a B, che non ha altre vie, quindi torno ad A, che può andare verso
D. Ci fermiamo poiché abbiamo già visitato sia C che E:

In ordine, in vertici vengono visitati: A,B,C,E,F,D.

Giacomo Maccanti – g.maccanti@students.uninettunouniversity.net 99


ALGORITMI E PROGRAMMAZIONE AVANZATA - G.MACCANTI@STUDENTS.UNINETTUNOUNIVERSITY.NET

Visite di un grafo: algoritmi di visita


Gli algoritmi di visita possono essere descritti da un algoritmo “generico”.
Questo algoritmo “generico”, però, ha bisogno di classificare i vertici:
• Bianco: sono vertici non ancora visitati o scoperti;
• Grigio: sono vertici visitati ma i cui vertici adiacenti non sono stati tutti scoperti;
• Nero: sono vertici visitato con tutti i vertici adiacenti visitati.

Visite di un grafo - algoritmi di visita generico - inizializzazione: pseudocodice


Prima di iniziare la nostra visita, abbiamo bisogno di una inizializzazione (Pre-processing) per poter
valorizzare tutti i nodi al corretto attributo.
Initialize (G) //PASSO ALLA FUNZIONE IL MIO GRAFO G
foreach u V do //PER OGNI VERTICE u APPARTENENTE A V, FACCIO
color [u] white //COLORO OGNI VERTICE DI BIANCO (VERTICE MAI VISITATO)

Costo: O(V)

Visite di un grafo - algoritmi di visita generico: pseudocodice


Visit (G,s) // G E’ IL NOSTRO GRAFO, s IL SORGENTE
color[s] grey
while there grey vertices do
u a grey vertex
if a white vertex v adj to then
color [v] grey
else
color [u] black

Ο(V+E) se contiamo i vertici adiacenti una sola volta.

Può essere importante ricorda il vertice da cui sono arrivato nello scoprire un vertice. Associamo un
attributo P[u] (predecessore) ad ogni vertice; inizialmente a NULL.

Visit (G,s) // G E’ IL NOSTRO GRAFO, s IL SORGENTE


color[s] grey
while there grey vertices do
u a grey vertex
if a white vertex v adj to then
color [v] grey
P[v] u
else
color [u] black

Dopo la Visit(G,s), solo i vertici neri eccetto s hanno predecessore non nullo, con le seguenti proprietà:
• Vp – tutti i vertici neri raggiungibili da s;
• Gp – albero o foresta di alberi.

Giacomo Maccanti – g.maccanti@students.uninettunouniversity.net 10


0
ALGORITMI E PROGRAMMAZIONE AVANZATA - G.MACCANTI@STUDENTS.UNINETTUNOUNIVERSITY.NET

Per la gestione dei vertici grigi, usiamo una struttura dati D i cui elementi sono ordinati:
• create_empty – crea una struttura vuotoa;
• first(D) – preleva il primo elemento (no modifiche);
• add(D,x) – aggiunge x a D;
• remove_first(D) – elimina il primo elemento da D;
• not_Empty(D) – ritorna TRUE se D non è vuoto, FALSE altrimenti.

Perché i vertici grigi? Perché quelli bianchi non sono ancora stati toccati, quelli neri sono ormai inutili
poiché visitati, quelli grigi, invece, devono finire di essere visitati.

Visit (G, s) //PASSO IL GRAFO G E LA SORGENTE s


D create_empty //CREDO D, VUOTO
color [s] gray //LA SORGENTE DIVENTA GRIGIA, POICHE’ SI PARTE CON LE VISITE
add (D, s) //AGGIUNGO LA SORGENTE ALLA STRUTTURA DATI
while not_empty (D) do //FINCHE’ NON HO PRELEVATO TUTTI I VERTICI VISITABILI
u first (D) //LA STRUTTURA DATI CHE NASCE VUOTA AVRA’ SOLO LA SORGENTE
if un vertice bianco v adiacente a u then
color [v] gray //LO COLORO DI GRIGIO
P[v] u //MI MEMORIZZO IL PREDECESSORE
add (D,v) //AGGIUNGO v ALLA LISTA
else //ALTRIMENTI SE NON ESISTONO VERTICI BIANCI
color [u] black //LO COLORO DI NERO
remove_first (D) //LO RIMUOVO DALLA CODA

Visite di un grafo: visita in ampiezza (BFS)


La visita in ampiezza, detto anche Breadh-First Search scopre tutti i vertici a distanza n (n° di archi) dalla
sorgente prima di scoprire quelli a distanza n+1.
La visita in ampiezza è un’implementazione della visita generica in cui la struttura dati D, usata per
memorizzare i vertici grigi, è una coda.

La visita in ampiezza calcola la “distanza” (n° di archi) di ogni vertice dalla sorgente. Questo genera un
albero di visita (insieme aciclico di vertici) che ha:
• S come radice;
• Include tutti i vertici raggiungibili dalla sorgente;
• Ogni cammino dalla sorgente ad un vertice è minimo (nel n° di archi) – in pratica stiamo costruendo
il cammino più breve.

Visite di un grafo (BFS) in ampiezza: pseudocodice


BFS (G,s) //G E’ IL GRAFO, s LA SORGENTE
Q make_empty //Q E’ UNA CODA CHE SAR’ INIZIALIZZATA ALL’INSIEME
VUOTO
color [s] grey //COLORO L’ELEMENTO
d[s] O //LA SORGENTE, INIZIALMETE, AVRA’ DISTANZA 0 DA SE
STESSA
enqueue (Q, s) //METTO L’ELEMENTO IN CODA
while not_empty (Q) do //FINCHE’ LA CODA NON E’ VUOTA
u head (Q) //u PRENDE L’ELEMENTO IN TESTA ALLA CODA
for ogni v adiacente a u do //PER OGNI VERTICE ADIACENTE ALLA NOSTRA CODA
if color [v] = white then//SE IL VERTICE E’ BIANCO, QUINDI NON ANCORA SCOPERTO
color [v] grey //COLORALO DI GRIGIO
P[v] u //MI SEGNO IL PREDECESSORE
d[v] d[u] + 1 //MI SEGNO LA DISTANZA
enqueue (Q, v) //AGGIUNGO IL VERTICE V ALLA CODA DEI VERTICI GRIGI Q

Giacomo Maccanti – g.maccanti@students.uninettunouniversity.net 10


1
ALGORITMI E PROGRAMMAZIONE AVANZATA - G.MACCANTI@STUDENTS.UNINETTUNOUNIVERSITY.NET

dequeue (Q) //TERMINADO IL FOR, NON HO PIU’ VERTICI ADIACENTI, LO


CANCELLO DALLA CODA
color [u] black //LO FACCIO DIVENTARE NERO

IL COSTO E’: O(V+E)

ESEMPIO

Dato un grafo di 8 nodi (A,B,C,D,E,F,G,H)

Abbiamo la nostra coda Q.

In fase di inizializzazione svuotiamo la coda e poi


aggiungiamo artificialmente A nella testa ed inizio
con il mio ciclo while che dice che per ogni vertice
adiacente alla testa della lista (ne abbiamo due, B
e D che metto nella coda)

Terminati i vertici adiacenti, A “muore”


diventando di colore nero (qui usiamo il verde per
problemi con lo sfondo) e verrà eliminata dalla
coda.

Eliminando A, B diventerà in automatico la nuova


testa e metterò nella coda i vertici adiacenti,
quindi C,D,F.

Giacomo Maccanti – g.maccanti@students.uninettunouniversity.net 10


2
ALGORITMI E PROGRAMMAZIONE AVANZATA - G.MACCANTI@STUDENTS.UNINETTUNOUNIVERSITY.NET

Vedete che, mano a mano che scremiamo i


vertici, andiamo sempre più a farli diventare tuti
neri.

Infine abbiamo ottenuto un albero di visita, dove


ogni livello corrisponde alla distanza dalla
sorgente. Livello 0, livello 1 (B,D), livello 2
(C,F,H), ecc.

Visite di un grafo (BFS) in ampiezza: proprietà


Detta δ(s,v) la minima distanza (n° di archi) tra s e v, dopo BFS(G,s) si ha:
d[ν] = δ(s,ν)∀ ν∈V

Visite di un grafo: visita in profondità (DFS)


La visita in profondità, detta anche Depth-first search (DFS), scopre tutti i vertici adiacenti al vertice
scoperto per ultimo.
Il grafo dei predecessori può risultare in una foresta di alberi.

Visite di un grafo (DFS) in profondità: pseudocodice


DFS (G,s) //ABBIAMO IL NOSTRO GRAFO G E LA SORGENTE s
S make_empty_stack //S (STACK) VIENE INIZIALIZZATO A VUOTO
color [s] grey //COLOROLA SORGENDE DI GRIGIO
push (S, s) //AGGIUNGIAMO LA PRIMITIVA ALLO STACK
while not_empty (S) do //FINCHE’ LO STACK NON E’ VUOTO
u top (S) //u E’ IL TOP DELLO STACK
for ogni v adiacente a u do //PER OGNI VERTICE v ADIACENTE A u
if color [v] = white then //SE E’ BIANCO (NON ANCORA VISITATO)
color [v] grey //LO COLORIAMO DI GRIGIO
P[v] u //MI SALVO IL SUO PREDECESSORE
push (S,v) //LO AGGIUNGO ALLO STACK
pop (S) //LO TOLGO DALLO STACK
color [u] black //LO COLORO DI NERO

ESEMPIO

Abbiamo il nostro stack rappresentato a pila.

Man mano che aggiungo i vertici (diventano i top)


passo al vertice adiacente scegliendo per ordine
alfabetico.
Giacomo Maccanti – g.maccanti@students.uninettunouniversity.net 10
3
ALGORITMI E PROGRAMMAZIONE AVANZATA - G.MACCANTI@STUDENTS.UNINETTUNOUNIVERSITY.NET

Quando abbiamo terminato i successori, torniamo


indietro stando ben attenti a colorare di nero (verde
in questo caso) i vertici che non hanno ulteriori visite.

H non ha ulteriori nodi visitabili, poiché B è già stato


visitato. Viene colorato di nero e tolto dallo stack.

Stessa cosa varrà per G, non per F, che può ancora


visitare K, quindi visiterà K, che a quel punto non ha
più adiacenze e diventerà nero.

Progressivamente li eliminiamo tutti.

Giacomo Maccanti – g.maccanti@students.uninettunouniversity.net 10


4
ALGORITMI E PROGRAMMAZIONE AVANZATA - G.MACCANTI@STUDENTS.UNINETTUNOUNIVERSITY.NET

Possiamo dire, in generale, che la visita in profondità somiglia molto alla visita infissa di un albero.
La visita in profondità (DFS) viene implementata in modo ricorsivo.

Giacomo Maccanti – g.maccanti@students.uninettunouniversity.net 10


5
ALGORITMI E PROGRAMMAZIONE AVANZATA - G.MACCANTI@STUDENTS.UNINETTUNOUNIVERSITY.NET

ETICHETTE TEMPORALI
La visita in profondità può essere arricchita attraverso l’informazione di tempo; ovviamente un tempo
virtuale.
In particolare noi ci memorizziamo due attributi:

• d[v]: tempo in cui v viene scoperto (grigio) – incrementato ogni volta che un vertice viene scoperto;
• f[v]: tempo in cui v è processato (nero) – incrementato ogni volta che un vertice viene chiuso.

Ogni vertice viene etichettato con una coppia d[v]/f[v].


ESEMPIO

ALBERI DI COPERTURA MINIMI (MINIMUM SPANNING TREE – MST)


Dato un grafo non orientato connesso e pesato G(V,E), bisogna determinare un sotto grafo aciclico T⊆ E
che connette tutti i vertici in modo che il costo totale sia minimo.
w(t)=Σ(u,v)∈T w(u,v)
Essendo T aciclico e connesso è per definizione un albero.

Alberi di copertura minima: applicazione


Vediamo un problema riferito alla posatura di cavi in una città che può essere risolta attraverso gli alberi di
copertura minima.
I cavi possono essere sotterrati solo lungo certe posizioni.
Costo del percorso = difficoltà della posatura (es. tipo di terreno – se ho roccia non posso posare).
Si vuole minimizzare il costo e raggiungere ogni casa.

RISOLUZIONE INEFFICIENTE MA OVVIA


Noi abbiamo la nostra centrale e gli n centri abitati, come facciamo a connetterli tutti (le case) con la
centrale, minimizzando il costo?

Giacomo Maccanti – g.maccanti@students.uninettunouniversity.net 10


6
ALGORITMI E PROGRAMMAZIONE AVANZATA - G.MACCANTI@STUDENTS.UNINETTUNOUNIVERSITY.NET

Ovviamente, non connettendo ogni singola casa alla centrale; sebbene il problema sia risolto non abbiamo
minimizzato per niente il nostro costo.

RISOLUZIONE EFFICIENTE MA MENO OVVIA


Abbiamo lo stesso risultato, nel senso che le singole case sono connesse alla centrale, ma il costo della
stesura è di molto ridotto.

Proviamo a tradurlo in termini di grafo:

• Modello: grafo;
• Vertici: case;
• Archi: possibili connessioni;
• Pesi: costi di posatura.
La soluzione è un MST.

Giacomo Maccanti – g.maccanti@students.uninettunouniversity.net 10


7
ALGORITMI E PROGRAMMAZIONE AVANZATA - G.MACCANTI@STUDENTS.UNINETTUNOUNIVERSITY.NET

Alberi di copertura minima: algoritmo generico


Il problema del MST ha una struttura tale per cui un algoritmo greedy fornisce la soluzione ottima.
Algoritmo sono varianti di un algoritmo generico.
L’algoritmo generico che andremo a vedere, consiste nel seguente concetto: nel crescere un sottoinsieme A
di archi che è sempre un sottoinsieme di un MST. Stiamo dicendo che nel nostro algoritmo partiamo da un
insieme di archi, ai quali aggiungeremo altri archi mantenendo come invariante la proprietà appena
accennata.
L’arco che noi aggiungiamo è detto arco sicuro: ad ogni passo un arco (u,v) è aggiunto ad A in modo da non
violare la proprietà A ⊆ T.
(a,d) non sicuro poiché se aggiunto creerebbe un ciclo;
(c,d) non sicuro poiché se aggiunto non sarebbe sottoinsieme dell’MST;

(b,c) sicuro.

Alberi di copertura minima: pseudocodice


Generic-MST(G,w) //VIENE PASSATO IL GRAFO G E IL PESO w DEGLI ARCHI
A 0 //L’INSIEME A VIENE INIZIALIZZATO A 0
while A non è un MST do
cerca un arco sicuro (u,v)
A A {(u,v)} //AGGIUNGE L’ARCO AD A CHE E’ UN MST
return A

Alberi di copertura minima – arco sicuro: definizioni

• Un taglio (S,V-S) di un grafo non orientato G=(V,E) è una partizione di V – stiamo dicendo che
l’insieme dei vertici è diviso in due parti, una chiamata S, l’altra chiamata V-S;
• Un arco attraversa il taglio se uno dei suoi vertici appartiene alla partizione S e l’altro alla partizione
V-S;
• Un taglio rispetta un insieme A di archi se nessun arco in A attraversa il taglio;
• Un arco è leggero se il suo peso è il minimo tra tutti quelli che attraversano il taglio.
Il concetto è quello di valutare il peso degli archi che attraversano questo taglio.
ESEMPIO
Definiamo un taglio (la linea verde a punti . . . ),
questo taglio è il risultato della partizione dei
vertici S che sono quelli che stanno graficamente
sopra il taglio S{a,b,d,e}; e di V-S che sono i
restanti, quindi quelli che sono sotto il taglio V-
S{c,f,g,h,i}.

Giacomo Maccanti – g.maccanti@students.uninettunouniversity.net 10


8
ALGORITMI E PROGRAMMAZIONE AVANZATA - G.MACCANTI@STUDENTS.UNINETTUNOUNIVERSITY.NET

Se assumiamo che questi archi azzurri (insieme


aciclico) rappresentino l’insieme A, è evidente che
il taglio rispetta gli archi stessi poiché non ne
attraversa nemmeno uno.
Di tutti gli archi che attraversano questo taglio (6),
quello con peso più piccolo è l’arco leggero (c,d di
peso 7).

Alberi di copertura minima: individuazione degli archi sicuri


Dato un insieme di archi A ⊆ MST ed un taglio che lo rispetta, un arco sicuro è un arco leggero che
attraversa il taglio.
INTERPRETAZIONE

OSSERVAZIONI

• Dato che A ⊆ MST ̈ A è aciclico lui stesso;


• Ad ogni passo (passo dell’algoritmo, quindi dopo ogni aggiunta di un arco sicuro) il grafo Ga=(V,A)
è una foresta (insieme di tanti alberi che devono essere aciclici);
• Ogni componente connessa di Ga è un albero;
• Dato che l’insieme A ∪ {(u,v)} deve essere aciclico, ogni arco sicuro per A connette due componenti
di Ga.

Alberi di copertura minima: analisi dell’algoritmo.


Inizialmente l’insieme A è vuoto ← 0.
La foresta Ga consiste di |V| componenti (alberi).
Il ciclo che aggiunge archi sicuri viene ripetuto |V-1| volte.
Ovviamente il nostro MST avrà un massimo di archi pari a |V|-1.
Ad ogni iterazione l’aggiunta di un arco sicuro riduce di 1 il numero di componenti connesse (inizialmente i
vertici non saranno connessi e ad ogni iterazione lo saranno). L’algoritmo termina quando esiste un singolo
componente (1 solo albero).

Giacomo Maccanti – g.maccanti@students.uninettunouniversity.net 10


9
ALGORITMI E PROGRAMMAZIONE AVANZATA - G.MACCANTI@STUDENTS.UNINETTUNOUNIVERSITY.NET

ESEMPIO
Inizialmente il mio insieme A ← è vuoto; quindi non ha
elementi ne vincoli iniziali.
Abbiamo 4 componenti connesse (a,b,c,d).
Ora devo aggiungere l’arco sicuro, cioè quell’arco con peso
inferiore e che connette due componenti connesse; quindi a,b.
Lo aggiunto.

Vedete che ora abbiamo il nostro insieme A, valorizzato con i


vertici a,b.
Quindi ora abbiamo solo 3 componenti connesse: a,b e poi c e
d singolarmente.

Dopodiché aggiungo l’arco con peso 2 che connette la


componente b e d. Adesso abbiamo due componenti
connesse a,b,d e c singolarmente.
Notiamo che possiamo collegare c tramite l’arco b,d o tramite
l’arco d,c; noi scegliamo l’arco sicuro (quello leggero) e
connettiamo b,d.

Ed ecco che abbiamo l’insieme A di |V|-1 archi e una sola


componente connessa.

Giacomo Maccanti – g.maccanti@students.uninettunouniversity.net 11


0
ALGORITMI E PROGRAMMAZIONE AVANZATA - G.MACCANTI@STUDENTS.UNINETTUNOUNIVERSITY.NET

ALGORITMO DI KRUSKAL
L’algoritmo di Kruskal è basato sulla costruzione del MST tramite la connessione delle varia componenti
connesse.
E’ un algoritmo greedy perché ad ogni passo aggiungiamo alla foresta, l’arco di peso minore.

Algoritmo di Kruskal: pseudocodice


MST-Kruskal (G,w) //G E’ IL NOSTRO GRAFO, w IL PESO
T ← ∅ //LAVORIAMO CON L’INSIEME T CHE INIZIALMENTE E’ VUOTO
Ordina gli archi
for ogni arco (u,v) //PER OGNI ARCO
if (u e v non sono connessi in T) then
T ← T ∪ {(u,v)} //AGGIUNGO L’ARCO (u,v,) ALL’INSIEME T
return T

Algoritmo di Kruskal: analisi


Dipende dalla struttura dati usata per memorizzare la foresta. Usando strutture dati tradizionali, abbiamo:
Ordinamento: O(E lg E);
Ciclo for: O(E);
Verifica di connessione: O(V).
TOTALE: O(E lg E) + O(EV)=O(V*E).

PERCORSI MINIMI IN UN GRAFO


Percorsi minimi in un grafo: definizioni e proprietà
Dato un grafo orientato G=(V,E), con una funzione peso w: E → V ed un percorso p=(V0,…,Vk) il costo w(p)
del percorso è la somma dei pesi corrispondenti:

Il percorso minimo tra due vertici u e v:

Delta(u,v) rappresenta la lunghezza del percorso minimo tra


due vertici.

E diciamo che questa è uguale al minimo tra tutti i pesi del


percorso p che uniscono u,v se esiste un percorso tra u,v.

Se questo percorso non esiste, lo definiamo i background come


minimo (infinito).

Quello che noi vogliamo studiare è: dato un grafo orientato G=(V,E), con una funzione di peso w: E → R un
percorso minimo tra due vertici u e v è un cammino per cui: w(p) = δ(u,v).

Giacomo Maccanti – g.maccanti@students.uninettunouniversity.net 11


1
ALGORITMI E PROGRAMMAZIONE AVANZATA - G.MACCANTI@STUDENTS.UNINETTUNOUNIVERSITY.NET

Percorsi minimi in un grafo: applicazioni pratiche


Una delle applicazioni più naturali che si possono fare è, certamente, quella di una mappa stradale. Noi
possiamo simulare un navigatore stradale che deve cercare la distanza minima tra due località.
Nella nostra mappa possiamo vedere un grafo, dove i vertici sono le località, le connessioni sono le nostre
strade e i pesi sono le distanze autostradali.

Adesso astraiamo la mappa e teniamo solo il nostro grafo:

Questa astrazione del problema, ci suggerisce due varianti possibili per risolvere il problema:
Sorgente singola: tra un vertice (sorgente) e tutti gli altri vertici;
Sorgente multipla: tra tutte le coppie di vertici.

SORGENTE SINGOLA

Ad esempio voglio sapere la distanza tra Torino (sorgente) e Bologna vediamo nella figura qual è la distanza
minima. Però, se prendo Torino come sorgente, posso anche calcolare la distanza minima tra tutte le altre
destinazioni.

Giacomo Maccanti – g.maccanti@students.uninettunouniversity.net 11


2
ALGORITMI E PROGRAMMAZIONE AVANZATA - G.MACCANTI@STUDENTS.UNINETTUNOUNIVERSITY.NET

Percorsi minimi in un grafo: archi con peso negativo


Il percorso minimo può non essere definito in presenta di archi con peso negativo.
Sicuramente la distanza minima non è definita in presenza di uno o più cicli a peso negativo (il peso totale è
negativo).

Percorsi minimi in un grafo: algoritmi


Un algoritmo lo abbiamo già visto, tra cui il BFS (costruisce la distanza minima dalla sorgente, ma in termini
di archi non in termine di peso).
Pesi non-negativi: algoritmo di Dijkstra;
Pesi negativi: algoritmo di Bellman-Ford.
Il risultato dell’esecuzione di un algoritmo di percorsi minimi, produce un albero dei percorsi minimi.
Un albero dei percorsi minimi con radice S è un sotto grafo orientato dove G’(V’,E’) of G con V’⊆ V e E’ ⊆
E.
G’: albero con radice in S;
V’: insieme dei vertici raggiungibili da s;
Per ogni V ⊆ V’ l’unico percorso semplice da S a V è minimo.

Percorsi minimi in un grafo: rilassamento


Il rilassamento è l’operazione alla base di questi algoritmi. Tutti gli algoritmi per percorsi minimi hanno due
caratteristiche:
• sotto struttura ottima – già vista;
• rilassamento.
Cosa si intente per sotto struttura ottima applicata ai cammini minimi? Si intendo che un cammino minimo è
composto da sotto percorsi a loro volta minimi. Un percorso minimo è il percorso ottimo da una sorgente alla
destinazione, questo può essere a sua volta scomposto in sotto percorsi a loro volta minimi.

SOTTO STRUTTURA OTTIMA

Questo appare facilmente intuibile, poiché se noi immaginiamo un percorso minimo s ad un vertice v, come
segue:
p’
s-------------->u----->v
Immaginate il percorso minimo tra s e v, passando per u, possiamo quindi dire, intuitivamente che il percorso
s → u è a sua volta un percorso minimo ottimo chiamato p’.
Il peso minimo del cammino minimo da s a v è: δ ( s , v ) = δ ( s , u ) + w ( u , v )

Giacomo Maccanti – g.maccanti@students.uninettunouniversity.net 11


3
ALGORITMI E PROGRAMMAZIONE AVANZATA - G.MACCANTI@STUDENTS.UNINETTUNOUNIVERSITY.NET

RILASSAMENTO
L’operazione di rilassamento ha come obiettivo quella di voler valutare e trovare un percorso minimo tra s e
v attraverso u.
Alla base di questo, abbiamo la definizione della distanza minima d[v] che rappresenta il limite superiore per
la distanza minima da s a v (inizialmente ∞). P[v] è il predecessore di v nel percorso minimo attuale da s a v
(inizialmente NULL).
Il rilassamento tenta di decrementare la stima d[v] del percorso minimo v. Se avvenuta aggiorna anche il
predecessore di v.
Questo è alla base di tutti gli algoritmi.

Percorsi minimi in un grafo – rilassamento: pseudocodice


Relax (u,v,w) //APPLICHIAMO IL RILASSAMENTO ALL’ARCO u, v DI PESO w
if d[v] > d[u] + w(u,v) then //SE LA STIMA SUL VERTIVCE V d[v] E’ MAGGIORE DELLA
STIMA TRA LA SORGENTE E IL VERTICE U + IL PESO DELL’ARCO CHE UNISCE u,v
d[v] = d[u] + w(u,v) //ALLORA MI CONVIENE USARE LA STIMA FATTA SUL PERCORSO CHE
VA DA u + L’ARCO CHE UNISCE u,v
p[v] = u //ALLORA IL PREDECESSORE E’ u

Percorsi minimi in un grafo – rilassamento: esempio

La stima attuale di V è 9; ovviamente il rilassamento avrà successo poiché


passare da u per arrivare a v è un percorso più corto di quello attuale che
ha costo 9.

Quindi, dato che 9 è più grande di 5 + 2, rilassiamo.


Aggiorniamo d[v] a 5 + 2.

Se invece 5 fosse la stima di u e 6 la stima di v, non ci conviene passare da


u, poiché 5 + 2 è più grande di sei e noi non ci passiamo.

ALGORITMO DI DIJKSTRA
Mantiene un insieme S di vertici, le cui distanze minime sono state calcolate; cioè questo algoritmo nel
momento in cui ad un vertice viene assegnato un valore d[u], questo non verrà più modificato.
Per cui ci sono vertici per cui la stima della distanza coincide con la distanza minima: d[u] = δ(s,u).
Questo algoritmo sceglie in modo greedy il vertice u in V-S (s sono quelli assegnati, quindi V – S sono quelli
non ancora assegnati) con la stima più bassa – aggiunge u a s.

Utilizza una coda a priorità Q per memorizzare i vertici e con d[] come chiave.

Algoritmo di Dijkstra: pseudocodice


Dijkstra (G,s) //G E’ IL GRAFO, s LA SORGENTE

Giacomo Maccanti – g.maccanti@students.uninettunouniversity.net 11


4
ALGORITMI E PROGRAMMAZIONE AVANZATA - G.MACCANTI@STUDENTS.UNINETTUNOUNIVERSITY.NET

Initialize (G,s) // d=∞, p=NULL //INIZIALIZZIAMO TUTTI I VERTICI


d[s]=0 //LA DISTANZA DALLA SORGENTE VIENE MESSA A 0
S= ∅ //L’INSIEME s DEI VERTICI DEFINITIVI VIENE MESSO A 0
Q = V(G) //NELLA CODA METTIAMO DENTRO TUTTI I VERTICI DEL GRAFO
while (Q ≠ ∅ ) //FINCHE’ NON ABBIAMO PROCESSATO TUTTI I VERTICI
u = Delete_Min (Q) //u ESTRAE ED ELIMINA IL VALORE MINIMO CHE STA NELLA
TESTA
S = S ∪ {u} //AGGIUNGO L’ELEMENTO APPENA ESTRATTO A s
foreach v adiacente a u //TUTTI GLI ELEMENTI VENGONO RILASSATI
Relax (u,v,w)

Giacomo Maccanti – g.maccanti@students.uninettunouniversity.net 11


5
ALGORITMI E PROGRAMMAZIONE AVANZATA - G.MACCANTI@STUDENTS.UNINETTUNOUNIVERSITY.NET

Algoritmo di Dijkstra: esempio


Abbiamo il nostro grafo orientato aciclico.

Prendiamo 1 come sorgente e vogliamo calcolare la


distanza che c’è tra la sorgente e tutti gli altri vertici.
L’inizializzazione ci dice che S è l’insieme vuoto e che la
nostra coda Q verrà valorizzata con tutti i vertici, ognuno
con la sua priorità. La sorgente 1 avrà 0 mentre gli altri
vertici hanno priorità (distanza) infinito poiché ancora non
la si conosce. L’algoritmo dice di prendere l’elemento con
priorità minima, vedete che nella coda gli elementi sono già
tutti ordinati, quindi prenderemo 1.

Nell’insieme S viene inserito 1, quindi significa che quello non


verrà più modificato; a questo punto facciamo il rilassamento di
tutti gli archi uscenti da uno, che sono 2,3 e 4. Rilassare
significa: possiamo modificare la stima di 2,3 e 4? Sicuramente
sì perché inizialmente hanno una stima pari a infinito, quindi
essendo la stima di 1 pari a 0 possiamo rilassare i valori e dare a
2 il 10, a 3 l’1 e a 4 il 5.

Vedete che questa è una coda a priorità, quindi ovviamente


ogni volta ci troviamo in testa l’elemento con priorità minore. In
questo caso il vertice 3 con priorità 1 (peso dell’arco). 5 e 6 non
sono ancora stati aggiornati quindi avranno distanza infinita.

Prelevo 3 e lo aggiungo all’insieme S.


Ora posso rilassare tutti gli archi uscenti da 3, quindi 2,5 e 4.
Vediamo quindi il risultato: 3 ha una stima pari a 1, 2 ha una
stima pari a 9, poiché tra 3 e 2 ho distanza 8, aggiungiamo 1 di
distanza dalla sorgente, ottengo 9 (viene rilassato positivamente
poiché inizialmente 2 aveva peso 10 e ora ha 9). Stessa cosa per
4 che aveva una stima paria 5, ma adesso otterrà 4, poiché da 3
a 4 abbiamo 3, più 1 dalla sorgente 4. Stessa cosa per la stima di
5.

Giacomo Maccanti – g.maccanti@students.uninettunouniversity.net 11


6
ALGORITMI E PROGRAMMAZIONE AVANZATA - G.MACCANTI@STUDENTS.UNINETTUNOUNIVERSITY.NET

Vediamo che, di volta in volta, a forza di aggiungere gli elementi


all’insieme S e a rilassarli, otteniamo un insieme completo di
elementi con la loro distanza minima.
Notate come una volta che quel vertice viene aggiunto all’insieme
S, questo non viene più toccato né considerato in nessun modo
(greedy).

Abbiamo, inoltre, il vettore p con i nostri predecessori.

Ad esempio, se voglio raggiungere 6, devo passare da 5, da 3 e 1.

Giacomo Maccanti – g.maccanti@students.uninettunouniversity.net 11


7
ALGORITMI E PROGRAMMAZIONE AVANZATA - G.MACCANTI@STUDENTS.UNINETTUNOUNIVERSITY.NET

ALGORITMO DI BELLMAN-FORD
L’idea nasce dall’iterare |V|-1 volte e, ad ogni passo, applicare il rilassamento ad ogni arco del grafo. Questo
avviene perché se noi usiamo l’algoritmo di Dijkstra ad un grafo con dei pesi negati, questo, semplicemente
può tornare dei risultati sbagliati; proprio perché greedy. Ogni volta che estraiamo un vertice, questo avrà
una distanza minima che non cambierà più (questo vale solo se tutti gli archi sono positivi).

Algoritmo di Bellman-Ford: pseudocodice


Bellman_Ford (G,s) //G E’ IL MIO GRAFO, s LA MIA SORGENTE
Initialize(G,s,d) // d=∞, p=NULL //INIZIALIZZO LA SORGENTE A 0 (Initialize) E LE
DISTANZE
for i = 1 to |V|-1 do //ITERIAMO PER V-1 VOLTE
foreach arco (u,v) in E do //PER OGNI ARCO DEL MIO GRAFO
Relax (u,v,w) //RILASSO I VERTICI
foreach arco (u,v) in E do //UNA VOLTA RILASSATI TUTTI, RICICLO PER TUTTI GLI ARCHI
if d[v] > d[u] + w(u,v) then //VERIFICO CHE LA DISTANZA MINIMA SIA CORRETTA
return FALSE //SE HO UN ARCO CON PESO NEGATIVO, NON POSSO FARE STIME
return TRUE //TUTTI GLI ARCHI HANNO PESO POSITIVO; STIME CORRETTE

Giacomo Maccanti – g.maccanti@students.uninettunouniversity.net 11


8
ALGORITMI E PROGRAMMAZIONE AVANZATA - G.MACCANTI@STUDENTS.UNINETTUNOUNIVERSITY.NET

TEORIA DELLA COMPLESSITÀ


PROBLEMI INTRATTABILI
Problemi trattabili e intrattabili
La maggior parte degli algoritmi analizzati hanno complessità polinomiale in n: T(n)=O(n^k) per qualche
costante n; questi sono detti problemi trattabili.

Problemi intrattabili
Che cosa intendiamo, allora, per problemi intrattabili? Intendiamo algoritmi il cui tempo di esecuzione è
maggiore di n^k. Più grandi di n^k non sarà ovviamente una funzione polinomiale ma sarà una funzione
diversa.
Perché quelli polinomiali sono quelli che preferiamo? Perché, matematicamente parlando, hanno un pregio
nei loro esponenti che sono, generalmente, piccoli. Se noi prendessimo un algoritmo di O(n^100) potrebbe
sicuramente considerarsi intrattabile, ma è improbabile che si verifichi.
L’altro aspetto del perché ci “piacciono” gli algoritmi polinomiale, sta nel fatto che esistono particolari
proprietà di chiusura.

Proprietà di chiusura degli algoritmi polinomiali


Rispetto alla somma
f(n) + g(n) → la somma di un polinomio è comunque un polinomio
Rispetto alla moltiplicazione
f(n)·g(n) → il prodotto di un polinomio è comunque un polinomio
Rispetto alla composizione
f(g(n)) → una funzione polinomiale di un polinomio è comunque un polinomio
questo si traduce come chiusura dei polinomi attraverso la somma, la moltiplicazione il prodotto e la
funzione di un polinomio; tutto questo ci aiuta nella risoluzione e creazione degli algoritmi.

Tutti gli algoritmi sono polinomiali?


Purtroppo no! Non tutti gli algoritmi hanno risoluzione polinomiale; alcuni problemi richiedono tempo
super-polinomiale. Alcuni sono addirittura non risolubili.
Esiste, infatti, una ampia classe di problemi rilevanti per i quali la risposta è sconosciuta (e che
probabilmente è no). Sono definiti problemi NP completi.

PERCHE’ È RILEVANTE?

Perché la conoscenza dei problemi intrattabili è importante per il progetto di un algoritmo. Se intrattabile, è
inutile cercare un algoritmo efficiente esatto, meglio puntare ad una efficace approssimazione.

SE IL MIO PROBLEMA È INTRATTABILE?

Le soluzioni sono di tipo:


Brute-Force: possiamo farlo perché anche se è di tipo super polinomiale, magari il problema da risolvere è
piccolo;
Algoritmi approssimati: troviamo una soluzione ragionevole;
Euristiche: algoritmi efficaci nella maggior parte dei casi, non hanno pretesa di ottimalità.

Giacomo Maccanti – g.maccanti@students.uninettunouniversity.net 11


9
ALGORITMI E PROGRAMMAZIONE AVANZATA - G.MACCANTI@STUDENTS.UNINETTUNOUNIVERSITY.NET

PROBLEMI DI DECISIONE
Tutta la teoria della NP-completezza si applica ai problemi di decisione, ovvero a problemi con un risultato
booleano (vero, falso).
Apparentemente sembra una restrizione, ma i problemi di ottimizzazione possono essere facilmente
ricondotti a problemi di decisione.

Ottimizzazione VS Decisione
Rivediamo il problema dei percorsi minimi.
Dato un grafo G=(V,E). Due vertici u,v ∈ V, quale è il percorso minimo in G tra u e v?
Come possiamo tradurlo in un problema di decisione? Possiamo provarlo a tradurlo in una problema
risolvibile con una funzione booleana:
Dato un grafo G=(V,E). Due vertici u,v ∈ V, e un intero k>=0. Esiste un percorso in G tra u e v di lunghezza al
più k?

Se possiamo risolvere il problema di ottimizzazione rapidamente, lo stesso vale per il problema di decisione
corrispondente; questo vale anche nel caso in cui non si possa risolvere. Se possiamo provare che un
problema di decisione è intrattabile, lo stesso si applica al problema di ottimizzazione.

NON DETERMINISMO
Deterministici sono, in generale, i sistemi che noi abitualmente utilizziamo, come i calcolatori. Il
determinismo potrebbe essere sintetizzato come l’univocità di una scelta in un processo. Un programma è
deterministico se, a fronte di certi INPUT, fornisce sempre lo stesso OUTPUT.

DETERMINISMO VS NON DETERMINISMO


Implementare il sistema deterministico può essere complesso ma fattibile.
Implementare il sistema non deterministico è infattibile.
Perché parliamo di non determinismo? Perché ci aiuta a capire e caratterizzare i problemi intrattabili,
attraverso il concetto di algoritmo non deterministico.

Gli algoritmi non deterministici sono algoritmi in cui il passo successivo è scelto in modo non deterministico,
ovvero per specificarli si introduce una funzione speciale e due istruzioni eseguite in O(1).
Choice (x 1 ,...,x n )
Ritorna arbitrariamente uno degli elementi (x 1 ,...,x n ).
failure()
terminazione senza successo.
success()
terminazione con successo.

Determinismo VS non determinismo: istruzione choiche()


Interpretabile come una istruzione “magica” che sceglie il valore “giusto”. Rappresenta n potenziali
assegnazioni di algoritmo deterministico.
N.B. la choiche non è una scelta casuale e non è implementabile nei calcolatori reali; servirebbero
calcolatori quantistici.

Determinismo VS non determinismo: struttura di un algoritmo non deterministico


Un algoritmo non deterministico è composto da due fasi:
Scelta (esecuzione di una o più choice());

Giacomo Maccanti – g.maccanti@students.uninettunouniversity.net 12


0
ALGORITMI E PROGRAMMAZIONE AVANZATA - G.MACCANTI@STUDENTS.UNINETTUNOUNIVERSITY.NET

verifica.
Molti problemi rilevanti hanno una soluzione in tempo polinomiale non deterministico, ma non esiste un
algoritmo deterministico polinomiale.

NP COMPLETEZZA E ALGORITMO APPROSSIMATI


RIDUCIBILITA’
Il concetto della riducibilità è essenziale per poter classificare e categorizzare i problemi della classe NP.
I problemi NP non hanno tutti lo stesso livello di difficoltà. Alcuni sono più difficili di altri.

Il concetto di riducibilità, prevede l’applicazione a due problemi che chiamiamo A, B.


Dati A e B, diciamo che A è riducibile a B (A ∝ B) se:
1. Esiste una funzione f (un algoritmo deterministico polinomiale) che trasforma istanze di A
in istanze di B;
2. La risposta ad A per l’input x è SI se e solo se la risposta a B per l’input f(x) è SI.

Supponiamo che la nostra scatola verde sia un algoritmo per


risolvere B; è un algoritmo di decisione, quindi produrrà risposte
SI/NO a seconda degli INPUT che noi forniamo.
Ora pensiamo alla scatola gialla come l’algoritmo A riconducibile a
B. Sarà riducibile se, con una funzione f (deterministica o
polinomiale) possiamo generare delle istanze f(x) che
diventeranno istanze per l’algoritmo B e, tutte le volte che x
applicato al problema A, fornisce vero, lo stesso accadrà
all’algoritmo B su f(x).

ESEMPIO

A: date n variabili Booleane con valori x1,…,xn, esiste almeno una variabile con valore VERO?
B: dati n interi i1,…,in, è max{i1,…,in}>0?

ALGORITMO PER B
foreach j =1…n
if (ij > 0)
return true
return false

A: possiamo trasformare un’istanza di A in un’istanza di B?


f(x)
foreach j =1…n
if (xj == true)
ij = 1
else
ij = 0

PROBLEMI NP- completi E NP-hard


Problemi NP-hard
Un problema A è NP-HARD se per ogni altro problema A’ ∈ NP, A’∝ A. Questa definizione ci sta dicendo che
i problemi Np-HARD sono almeno difficili come i problemi NP, o che i problemi NP sono riconducibili ai
problemi NP-HARD.
Giacomo Maccanti – g.maccanti@students.uninettunouniversity.net 12
1
ALGORITMI E PROGRAMMAZIONE AVANZATA - G.MACCANTI@STUDENTS.UNINETTUNOUNIVERSITY.NET

Problemi NP-completi
Un problema A è NP-completo (NPC) se:

1. E’ un problema NP;
2. Per ogni altro problema A’ in NP, A’∝ A.

DEFINIZIONE INFORMALE

Se un qualunque problema nella classe NPC fosse risolubile in tempo polinomiale, allora tutti i problemi in
NP possono essere risolti in tempo polinomiale.

P: i problemi “facili”.

NP: problemi facili da verificare ma probabilmente difficili da


risolvere.

NPC: i problemi più difficili tra gli NP.

NP-hard: problemi difficili almeno come quelli in NP.

DIMOSTRAZIONE DI NP-completezza
Accediamo ora l’aspetto ingegneristico della questione, ovvero, dato un problema, capire se è NP-
completo; come si fa?
1. Si mostra che il problema è in NP (verificabile in tempo polinomiale);
2. Dimostriamo che è NP-hard, cioè un problema noto come NPC può essere ridotto a questo problema.
Serve un insieme di problemi NPC noti.

TEOREMA DI COOK E PROBLEMA SAT


Cook dimostrò che il problema della soddisfacibilità booleana (SAT) è NP-completo.
Nella sua forma più completa il problema SAT consiste nello trovare un’assegnazione di valori (vero o falso)
per cui la formula a1 op a2 op … an è vera (op = operatore booleano) .

1° ESEMPIO DEL PROBLEMA SAT


Data una funzione booleana: X1 X2 (X3 X’1 X’2)

È SODDISFACIBILE?
SI: poiché se assegniamo (x1=F, x2=F, x3=T) e risolviamo la funzione, uscirà come risultato V(ero).

2°ESEMPIO DEL PROBLEMA SAT


Data una funzione booleana: X1 X2 X1 X’2 X’1 X2 X’1 X’2

È SODDISFACIBILE? NO!
Ricordatevi che il problema non è risolvere il problema ma trovare il caso a noi necessario.

ALGORITMO NON DETERMINISTICO


NDSat (E,n) {
/* scelta */

Giacomo Maccanti – g.maccanti@students.uninettunouniversity.net 12


2
ALGORITMI E PROGRAMMAZIONE AVANZATA - G.MACCANTI@STUDENTS.UNINETTUNOUNIVERSITY.NET

for i = 1 to n do
xi Choice (true, false )
/* verifica *
if E(x1, x2, … ,xn) = T then
success
else
failure
}

ALTRI PROBLEMI NP
Usando la riducibilità e partendo da SAT, è stato dimostrato che altri numerosi problemi classici sono NPC.

PROBLEMI NP-completi CLASSICI


Molti problemi pratici sono (purtroppo) NP-completi…
Varianti e regole dei problemi NP-completi: http://en.wikipedia.org/wiki/List_of_NP-complete_problems
Alcuni problemi:
1. Problema della copertura dei vertici;
2. Problema del commesso viaggiatore (TSP);
3. Problema della massima clique;
4. Problema dello zaino.

Problemi NP-completi classici: problema della massima clique


Dato un grafo non orientato, determinare se contiene un sotto grafo connesso (clique) di dimensione k.

Giacomo Maccanti – g.maccanti@students.uninettunouniversity.net 12


3
ALGORITMI E PROGRAMMAZIONE AVANZATA - G.MACCANTI@STUDENTS.UNINETTUNOUNIVERSITY.NET

Problemi NP-completi classici: problema della copertura dei vertici


Dato un grafo non orientato G=(V,E), ed un intero k, determinare se esista un insieme di k vertici C V per
cui ogni arco (u,v) E, o u C o v C.

Problemi NP-completi classici: problema del commesso viaggiatore


Dato un grafo connesso e pesato G(V,E,w), esiste un ciclo che visita ogni vertice più una volta e ha costo
pari a d?

ALGORITMI PSEUDOPOLINOMIALI
Definizione: quando il tempo di esecuzione è polinomiale nel valore numerico dell’input (che però è
esponenziale nella lunghezza degli input).

ALGORITMI APPROSSIMATI
Cosa fare se il mio problema è intrattabile?
1. Approssimazioni;
2. Euristiche.

Algoritmi approssimati: approssimazioni


Gli algoritmi approssimati sono adatti per problemi di ottimizzazione garantiscono che una soluzione sia
vicina entro un x% rispetto all’ottimo.

Algoritmi approssimati: euristiche


Le euristiche sono algoritmi che si comportano bene in casi specifici (ma tipici). In generale, nessuna
garanzia di qualità. Spesso usate euristiche greedy.

Giacomo Maccanti – g.maccanti@students.uninettunouniversity.net 12


4
ALGORITMI E PROGRAMMAZIONE AVANZATA - G.MACCANTI@STUDENTS.UNINETTUNOUNIVERSITY.NET

RINGRAZIAMENTI

Questo testo è stato reso possibile grazie alla documentazione fornita dall’Università Telematica
Internazionale UniNettuno, che desidero ringraziare per il supporto e la dedizione che adotta nei confronti
dei propri studenti.

Desidero, inoltre, ringraziare tutti i miei colleghi di studio che hanno, insieme, dato consigli, mosso critiche
(costruttive) e, soprattutto, hanno trovato utilità in questa guida. Se, anche tu, senti questa necessità, sappi
che accoglierò ogni commento a braccia aperte.

Giacomo Maccanti – g.maccanti@students.uninettunouniversity.net 12


5

Potrebbero piacerti anche