




























































































Studia grazie alle numerose risorse presenti su Docsity
Guadagna punti aiutando altri studenti oppure acquistali con un piano Premium
Prepara i tuoi esami
Studia grazie alle numerose risorse presenti su Docsity
Prepara i tuoi esami con i documenti condivisi da studenti come te su Docsity
Trova i documenti specifici per gli esami della tua università
Preparati con lezioni e prove svolte basate sui programmi universitari!
Rispondi a reali domande d’esame e scopri la tua preparazione
Riassumi i tuoi documenti, fagli domande, convertili in quiz e mappe concettuali
Studia con prove svolte, tesine e consigli utili
Togliti ogni dubbio leggendo le risposte alle domande fatte da altri studenti come te
Esplora i documenti più scaricati per gli argomenti di studio più popolari
Ottieni i punti per scaricare
Guadagna punti aiutando altri studenti oppure acquistali con un piano Premium
Gli appunti seguono fedelmente tutto il programma svolto a lezione e comprendono le slide con eventuali chiarimenti del professore. Sono presenti annotazioni negli pseudocodici che aiutano nella loro comprensione.
Tipologia: Appunti
1 / 251
Questa pagina non è visibile nell’anteprima
Non perderti parti importanti!





























































































Come detto, i passi che costituiscono un algoritmo devono essere eseguibili da un qualche agente di
calcolo in grado di eseguire ogni passo in un tempo finito
calcolo
Un modello di calcolo è un modello teorico di una macchina di calcolo.
Usare un modello di calcolo piuttosto che un altro ha impatto:
Nell’ambito dello studio degli algoritmi si fa di solito riferimento alla Random Access Machine
(RAM), ossia un modello astratto e semplificato dei calcolatori reali “equivalente” alla macchina di
Turing (con essa è possibile cioè calcolare tutto ciò che è possibile calcolare con la macchina di Turing)
In essa:
o due di essi sono registri speciali: accumulatore (memorizza il risultato di calcoli) e
Instruction Counter IC (memorizza l’indirizzo della prossima istruzione)
o non ci poniamo problemi sulla dimensione dei numeri che utilizziamo ( assunzione
forte! )
memorizzare un intero grande a piacere
Un programma per RAM consiste in una sequenza di istruzioni di vario tipo:
La RAM è un modello astratto e semplificato dei calcolatori reali (le istruzioni della RAM assomigliano
alle tipiche istruzioni di un linguaggio assembler), tuttavia ci sono delle differenze :
della macchina stessa, non richiede un caricamento separato in memoria per essere eseguito)
Per valutare l’efficienza di un programma per la RAM è necessario definire un modello di costo, ossia
definire quando costa in termine di “tempo” ogni istruzione della RAM.
Esistono due tipologie di modelli:
dimensione degli operandi
operandi
a. il costo è proporzionale al logaritmo del valore degli operandi, cioè al numero di bit
necessari a rappresentarli
Il modello a costi uniformi è più semplice in quanto la valutazione dell’efficienza di un algoritmo si
riduce al conteggio delle operazioni previste dall’algoritmo, MA è più irrealistico (sommare due
numeri da 8 bit o due numeri da 109 bit ha lo stesso costo)
delle assunzioni
o assumendo che gli operandi abbiano una dimensione massima prefissata , il modello
di costo uniforme può essere ragionevolmente utilizzato
Noi faremo riferimento alla RAM con le seguenti assunzioni :
istruzioni di controllo
o In particolare per un input di dimensione 𝑛 si assume che i valori su cui si opera siano
rappresentabili con c ⋅ log 𝑛 bit con 𝑐 ≥ 1
Queste assunzioni saranno ragionevoli per certi tipi di algoritmi (studiati in questo corso), ma in altri
casi potrebbero essere assunzioni eccessivamente semplificative (come ad esempio nell’algoritmo RSA
per la crittografia)
Per descrivere i nostri algoritmi faremo uso di uno pseudocodice, ossia un “linguaggio” che:
cicli, ecc.)
vantaggi:
può essere tradotto in qualsiasi linguaggio)
Convenzioni:
Non ci sono quindi delimitatori di inizio e fine blocco
necessitano di dichiarazione
o in caso di equivoco si ricorre ai commenti
o Sono possibili assegnazioni multiple: i = j = k
o I blocchi nei due rami dell’istruzione vengono mostrati
tramite l’indentazione: l’else viene indentato allo stesso
livello del corrispondente if
▪ Non viene intrapresa nessuna azione per gestire l’errore
Gli algoritmi vengono realizzati sotto forma di programmi
linguaggio di programmazione
Il concetto di algoritmo e inscindibile da quello di dato.
rappresentano l’istanza del problema, produce dati in uscita che sono la risposta al problema
per l’istanza data
È fondamentale dunque che i dati siano ben organizzati e strutturati in modo che il calcolatore li possa
elaborare efficientemente
cui l'algoritmo stesso deve operare
Una struttura dati può essere vista come un "contenitore" in cui è possibile memorizzare dati e che ci
offre delle operazioni per inserire, modificare, cancellare e recuperare i dati memorizzati
efficienti
Le strutture dati possono essere classificate in due modi
Prima classificazione:
(esempi: array, record)
d’esecuzione e può essere diversa da esecuzione a esecuzione (esempi: liste, code, pile, alberi,
grafi)
Seconda classificazione:
A. Strutture dati astratte : definiscono la tipologia di dati contenuti nella struttura stessa e
l’insieme di operazioni che la struttura dati mette a disposizione per manipolarli
a. non specificano come le operazioni vengono realizzate
B. Strutture dati concrete : realizzazioni concrete delle strutture dati astratte
c. realizzano il comportamento descritto dalle strutture dati astratte tramite costrutti
“concreti” (array, record e puntatori, ecc.)
esempio:
Struttura dati astratta: collezione di n numeri con le seguenti operazioni:
Struttura dati (concreta):
Al fine di stabilire le proprietà degli algoritmi, come la loro correttezza o la loro complessità si farà uso
di vari strumenti matematici. Di seguito analizzeremo le principali.
0
1
− 1
1
𝑎
𝑚
𝑛
𝑚𝑛
𝑚
𝑛
𝑛
𝑚
𝑚
𝑛
𝑚+𝑛
Notazione:
2
𝑒
𝑘
𝑘
Proprietà:
log 𝑏
𝑎
𝑐
𝑎𝑏 = log
𝑐
𝑎 + log
𝑐
𝑏
𝑛
= 𝑛 log
𝑏
𝑏
1
𝑎
= − log
𝑏
𝑏
log
𝑐
𝑎
log
𝑐
𝑏
log
𝑏
𝑐
log
𝑏
𝑎
Dato un numero reale x, si chiama floor di x (o parte intera inferiore di x), denotato con ⌊x⌋, l’intero più
grande che è minore o uguale a x.
Dato un numero reale x, si chiama ceiling di x(o parte intera superiore di x),denotato con ⌈x⌉, l’intero
più piccolo che è maggiore o uguale a x.
Per un qualsiasi numero reale x si ha che
Per un qualsiasi numero intero n si ha che
Per x, y ∈ R, n ∈ Z e m ∈ N+ si ha che
L’induzione può essere usata per dimostrare proprietà sui tipi di dato ricorsivi. Ad esempio:
In un’espressione il numero di parentesi aperte è uguale al numero di parentesi chiuse.
Dimostrazione.
aperte né parentesi chiuse, la proprietà è vera.
o Ipotesi induttiva: 𝛼 𝑒 𝛽 sono due espressioni in cui il numero di parentesi aperte è
uguale al numero di parentesi chiuse.
o Si dimostra che una combinazione di 𝛼 𝑒 𝛽 hanno un numero di parentesi aperte uguale
al numero di parentesi chiuse.
(𝛼 + 𝛽), (𝛼 − 𝛽), (𝛼 × 𝛽) e (𝛼/𝛽) hanno un numero di parentesi aperte uguale al
numero di parentesi chiuse.
Al fine di dimostrare la correttezza di algoritmi in cui compaiono cicli si fa uso dell’ invariante di ciclo ,
ovvero una proprietà che risulta essere vera prima di ogni iterazione del ciclo.
Dato che l’invariante di ciclo si mantiene vera ad ogni iterazione, risulterà vera anche quando il ciclo
termina. Scegliendo una proprietà opportuna si può dimostrare la correttezza dell’algoritmo.
Per dimostrare la validità di un’invariante di ciclo si usa l’induzione:
quella iterazione, cioè all’inizio della prossima iterazione.
Esempio 1: dimostrazione correttezza algoritmo
Dimostrare la validità del seguente algoritmo: ricerca del minimo in un array
Invariante di ciclo : all’inizio dell’iterazione i-esima la variabile 𝑚𝑖𝑛 contiene il minimo del sottoarray
Dimostrazione:
sottoarray 𝐴[ 1.. 1 ].
o Ipotesi induttiva: si assume che l’invariante sia vera per un certo i (inizio dell’iterazione
i-esima).
o Si dimostra che alla fine dell’iterazione i-esima (e inizio dell’iterazione (i + 1) - esima)
l’invariante di ciclo rimane vera
Durante l’iterazione i-esima si ha uno tra due casi:
▪ Caso 1. 𝐴
< 𝑚𝑖𝑛, allora 𝑚𝑖𝑛 viene posto a 𝐴
. E quindi l’invariante rimane
ancora vera dato che 𝑚𝑖𝑛 è il minimo di 𝐴
e 𝐴
è minore di 𝑚𝑖𝑛, il
che implica che 𝐴[𝑖] è il minimo del sottoarray 𝐴[ 1.. 𝑖]
▪ Caso 2. 𝐴[𝑖] ≥ 𝑚𝑖𝑛, allora 𝑚𝑖𝑛 non viene modificato. E quindi l’invariante
riamane vera dato che 𝑚𝑖𝑛 è il minimo di 𝐴[ 1.. 𝑖 − 1 ] e 𝐴[𝑖] è maggiore o
uguale a min, ovvero min è il minimo del sottoarray 𝐴[ 1... 𝑖]
Correttezza algoritmo: data l’invariante di ciclo risulta essere vero l’algoritmo.
Dimostrazione:
Quando il ciclo termina l’invariante è vera e 𝑖 = 𝐴. 𝑙𝑒𝑛𝑔𝑡ℎ + 1.
In base all’invariante di ciclo 𝑚𝑖𝑛 = 𝑚𝑖𝑛(𝐴[ 1.. 𝑖 − 1 ]) ovvero 𝑚𝑖𝑛 = 𝑚𝑖𝑛(𝐴[ 1.. 𝐴. 𝑙𝑒𝑛𝑔𝑡ℎ]), cioé è
pari al minimo dell’intero array.
infine elaborati ed interpretati i risultati ottenuti
In definitiva vorremmo un procedimento per valutare la complessità di un metodo che:
codice
L’ analisi di complessità degli algoritmi è un procedimento per stimare le risorse utilizzate da un
algoritmo per la sua esecuzione. Tale metodo ci permette di valutare se l’algoritmo è più o meno
efficiente
Occorre precisare che, poiché un algoritmo è qualcosa di astratto non ha molto senso parlare di utilizzo
di risorse di calcolo da parte di un algoritmo. Ciò che dobbiamo/vogliamo valutare è l’utilizzo di risorse
da parte di un programma che realizzi l’algoritmo.
delle risorse utilizzate già a livello di algoritmo
una certa approssimazione nella valutazione fatta
Il tempo utilizzato dall’algoritmo durante la sua esecuzione di solito cresce al crescere della
dimensione dell’input. É pertanto necessario descrivere la sua complessità tramite una funzione, detta
funzione di costo , che indica il tempo impiegato per eseguire l’algoritmo in funzione della dimensione
dell’input.
notazione asintotica
Precisiamo meglio la dimensione dell’input e la funzione di costo
a. Dipende dal tipo di problema (ad esempio numero 𝑛 di elementi da ordinare)
b. In alcuni casi è espressa tramite due (o più) parametri (ad esempio dimensione 𝑚 𝑥 𝑛
delle matrici)
dell’input. La indichiamo con 𝑇(𝑛) (o 𝑇(𝑛, 𝑚)), essendo 𝑛 la dimensione dell’input
Il tempo impiegato per un’esecuzione dipende da:
a. costo delle operazioni elementari
b. configurazione dell’input
Un algoritmo è una sequenza di passi, ciascuno corrispondente ad un’operazione elementare
eseguibile nel modello di calcolo che stiamo adottando (nel nostro caso un’operazione elementare del
modello RAM). Dunque, se stabiliamo un costo per ciascuna di tali istruzioni, il costo dell’algoritmo è
dato dalla somma dei costi delle istruzioni che compongono la sequenza di passi.
Esempio:
Affinché si possa assegnare un valore numerico a 𝑇(𝑛) bisogna assegnare un costo alle operazioni 𝑐 𝑖
Abbiamo visto che nel modello RAM esistono due modelli di costo per le operazioni elementari
dei suoi operandi e quindi proporzionale al logaritmo del valore degli operandi
valore degli operandi
Abbiamo detto che, sebbene più irrealistico, il modello di costo uniforme può essere ragionevolmente
utilizzato per i nostri scopi
Con l’assunzione del modello a costi costanti tutti i coefficienti 𝑐
𝑖
sono costanti e potremmo dunque
pensare di definire il valore di ciascun parametro in base all’operazione corrispondente.
complicherebbe l’analisi
abbiano lo stesso costo e sia pari ad 1
o Sebbene questa sia una approssimazione forte, risulta accettabile in pratica poiché
comunque il costo delle operazioni elementari non dipende dalla dimensione dell’input
e tale semplificazione non cambia il tipo di funzione di costo (funzione lineare,
quadratica, logaritmica, ecc.) ma solo i coefficienti
La funzione di costo diviene quindi una funzione che, per ogni valore di input, ci dice quante operazioni
elementari vengono eseguite
Supponiamo ora che all’interno del nostro algoritmo venga invocato un altro algoritmo (si ha cioè
un’invocazione di una funzione/metodo). Come valutiamo il costo dell’invocazione?
dimensione pari a quella utilizzata nell’invocazione
o Nella realtà c’è un costo dovuto all’invocazione e al passaggio dei parametri, ma nel
modello RAM tale costo viene ignorato
Assumendo la probabilità che l’elemento sia presente pari a 𝑝, possiamo calcolare la complessità di
caso medio come:
Dunque
Anche con le semplificazioni introdotte, abbiamo visto che il calcolo preciso della funzione di costo
risulta essere piuttosto macchinoso.
accontenta di ricavare l’ andamento asintotico della funzione di costo, cioè la velocità con cui
la funzione “va a infinito”.
o La velocità di crescita di un algoritmo ci da informazioni sulla velocità con la quale
degradano le sue prestazioni
Perché ci interessa il comportamento all’infinito della funzione? Principalmente per 3 motivi:
fatto trascurabile
efficienti può essere sostanziale
prestazioni
Le notazioni asintotiche sono un formalismo matematico che permette di esprimere la velocità di
crescita di una funzione di costo, e quindi di effettuare confronti fra diverse funzioni
cui codominio è l’insieme ℝ dei reali
Vengono usate tre tipi di notazione:
1. Notazione O
Data una funzione 𝑔(𝑛), si denota con 𝑂(𝑔(𝑛)) l’insieme di funzioni
0
0
Se 𝑓(𝑛) ∈ 𝑂(𝑔(𝑛)) allora si dice che:
Esempio: 3 n
2
− 8n ∈ O(n
2
Dimostrazione: Si devono trovare 𝑐 ed 𝑛
0
tali che ∀𝑛 ≥ 𝑛
0
, 0 ≤ 𝑓(𝑛) ≤ 𝑐𝑔(𝑛), ovvero 0 ≤
2
2
. Ma 0 ≤ 3 𝑛
2
− 8 𝑛 è soddisfatta per ogni 𝑛 ≥ 3. Ci si
concentra ora su 3 𝑛
2
2
, che può essere riscritta come ( 3 − 𝑐)𝑛
2
8 𝑛 ≤ 0 e se si sceglie 𝑐 = 4 si ha −𝑛
2
− 8 𝑛 ≤ 0 che è vera per ogni 𝑛 ≥ 0.
Data una funzione 𝑔(𝑛), si denota con Ω(𝑔(𝑛)) l’insieme di funzioni
0
0
Se 𝑓(𝑛) ∈ Ω(𝑔(𝑛)) allora si dice che:
Data una funzione 𝑔(𝑛), si denota con 𝛩(𝑔(𝑛)) l’insieme di funzioni
1
2
0
1
2
0
Se 𝑓(𝑛) ∈ 𝛩(𝑔(𝑛)) allora si dice che:
Per le notazioni asintotiche valgono le seguenti proprietà:
o 𝑓(𝑛) ∈ 𝑂(𝑓(𝑛))
o 𝑓(𝑛) ∈ Ω(𝑓(𝑛))
o 𝑓(𝑛) ∈ 𝛩(𝑓(𝑛))
o se 𝑓(𝑛) ∈ 𝑂(𝑔(𝑛)) e 𝑔(𝑛) ∈ 𝑂(ℎ(𝑛)) allora 𝑓(𝑛) ∈ 𝑂(ℎ(𝑛))
o se 𝑓(𝑛) ∈ Ω(𝑔(𝑛)) e 𝑔(𝑛) ∈ Ω(ℎ(𝑛)) allora 𝑓(𝑛) ∈ Ω(ℎ(𝑛))
o se 𝑓(𝑛) ∈ Θ(𝑔(𝑛)) e 𝑔(𝑛) ∈ Θ(ℎ(𝑛)) allora 𝑓(𝑛) ∈ Θ(ℎ(𝑛))