L'informatica è definita come la scienza degli algoritmi che descrivono e trasformano l'informazione.
Un algoritmo è una sequenza di comandi elementari e univoci che terminano in un tempo finito e operano su strutture dati.
I comandi elementari non possono essere ulteriormente scomposti e sono univoci.
Un algoritmo ben specificato non richiede che l'esecutore pensi, ma solo che esegua i passi precisamente.
Se un calcolatore produce un output errato, è colpa del progettista dell'algoritmo, non del calcolatore.
Le strutture dati sono necessarie per gestire i dati utilizzati dall'algoritmo, organizzandoli e memorizzandoli per semplificare l'accesso e la modifica.
Efficienza di un algoritmo
Non esiste una struttura dati adatta a ogni problema; è necessario conoscere i vantaggi e gli svantaggi delle principali strutture dati.
La scelta della struttura dati è fondamentale quanto la progettazione dell'algoritmo.
L'efficienza di un algoritmo si misura in termini di tempo di esecuzione e quantità di memoria richiesta.
I calcolatori sono veloci ma non infinitamente.
La memoria è economica ma non gratuita né illimitata.
Il costo computazionale degli algoritmi è valutato in termini di numero di operazioni elementari e quantità di memoria necessaria in funzione della dimensione dell'input.
Per valutare l'efficienza di un algoritmo, è necessario quantificare le risorse richieste senza essere influenzati dalla tecnologia specifica.
La Random Access Machine (RAM) è una macchina astratta con le seguenti caratteristiche:
Singolo processore che esegue le operazioni sequenzialmente.
Operazioni elementari con tempo di esecuzione costante.
Limite alla dimensione di ogni valore memorizzato.
Misura di Costo Uniforme
Sia d la dimensione di bit di ogni parola in memoria.
Se ogni dato in input è minore di 2d, ogni operazione elementare sui dati viene eseguita in tempo costante (misura di costo uniforme).
Se un dato non rispetta tale ipotesi, sono necessarie più parole di memoria, e le operazioni richiedono un tempo non costante.
In ambito scientifico si utilizza la misura di costo logaritmica, ma in questo corso non verrà analizzata.
Esempio:
Codice: def PotenzaDi2(n): x = 1 for i in range(n): x = x*2 return x
Il tempo di esecuzione è proporzionale a n, poiché il ciclo viene eseguito n volte, con tre operazioni di costo unitario ad ogni iterazione.
Notazione Asintotica
Notazione O grande, Omega e Teta
La notazione asintotica permette di valutare l'efficienza di un algoritmo confrontandone il costo computazionale con altri algoritmi.
Ci si limita a valutare il tasso di crescita, cioè la velocità con cui il tempo di esecuzione cresce all'aumentare della dimensione dell'input.
La valutazione è efficace quando la dimensione dell'input è sufficientemente grande.
La notazione asintotica confronta il tasso di crescita di una funzione rispetto a un'altra.
Esistono tre tipologie principali di notazione asintotica:
O grande: limite superiore asintotico.
Ω (Omega): limite inferiore asintotico.
Θ (Teta): limite asintotico stretto.
Notazione O grande
Definizione: Date due funzioni f(n),g(n)≥0, f(n) è in O(g(n)) se esistono due costanti c ed n<em>0 tali che 0≤f(n)≤c⋅g(n) per ogni n≥n</em>0.
O(g(n)) contiene tutte le funzioni "dominate" da g(n).
La notazione O grande definisce il limite superiore asintotico di f(n):
Oltre un certo valore n0, f(n) è limitata da c⋅g(n).
Esempi:
f(n)=3n+3 è in O(n2), con c=6 e n0=1
f(n)=3n+3 è in O(n), con c≥6 e n0=1
f(n)=n2+4n è in O(n2), con c≥5 e n0=1
Teorema: Sia f(n)=∑<em>i=0ma</em>ini=a<em>0+a</em>1n+a<em>2n2+…+a</em>mnm un polinomio di grado m, allora f(n) è in O(nm). Dimostrazione per induzione:
Caso base: m=0, per cui f(n)=a0⋅n0, dunque è una funzione costante e di conseguenza è in O(1), che coincide con O(n0)
Ipotesi induttiva: --> Affermiamo che ∑<em>i=0ka</em>ini è un O(nk) per ogni k < m, cioè esiste una costante c' tale che ∑<em>i=0ka</em>ini≤c′⋅nk
Passo induttivo: --> Dobbiamo dimostrare che f(n)=∑<em>i=0ma</em>ini≤c′′⋅nm Si osservi che, mettendo in evidenza l'ipotesi induttiva, f(n) può essere riscritto come f(n)=∑<em>i=0ma</em>ini=a<em>mnm+∑</em>i=0ka<em>ini per ogni ki=0kaini≤c′⋅nk dunque possiamo formulare la seguente catena di disuguaglianze f(n)=amnm+∑i=0kaini≤amnm+c′⋅nk≤amnm+c′⋅nm ricrivendo amnm+c′⋅nm come (am+c′)⋅nme ponendo c′′=am+c′ otteniamo che f(n)≤c′′⋅nm che per ipotesi sappiamo essere vera, dunque concludiamo che f(n), è in O(nm)
Notazione Omega
Definizione: Date due funzioni f(n),g(n)≥0, f(n) è in Ω(g(n)) se esistono due costanti c ed n0 tali che f(n)≥c⋅g(n) per ogni n≥n0.
Ω(g(n)) contiene tutte le funzioni che "dominano" la funzione g(n).
La notazione Omega definisce il limite inferiore asintotico di f(n):
Oltre un certo valore n0, f(n) è limitata inferiormente da c⋅g(n).
Esempi:
f(n)=2n2+3 è in Ω(n), con c=1
f(n)=2n2+3 è in Ω(n2), con c≤2
Teorema: Sia f(n)=∑i=0maini=a0+a1n+a2n2+…+amnm un polinomio di grado m, allora f(n) è in Ω(nm).
La dimostrazione è analoga alla dimostrazione del teorema per la notazione O grande.
Ordini di grandezza di O grande ed Omega
f(n)=log(n) è in O(n) e in Ω(1).
loga(n)=O(bn) per ogni a,b≥1
na=Ω(1) per ogni a
Un poli-logaritmo è dominato da qualunque radice e domina qualunque costante.
f(n)=an è in O(n) e in Ω(log(n)).
an=O(nb) per ogni a,b≥1
an=Ω(logb(n)) per ogni a,b≥1
Una radice è dominata da qualunque polinomio e domina qualunque poli-logaritmo.
f(n)=na è in O(2n) e in Ω(bn).
na=O(bn) per ogni a≥1 ed ogni b≥2
na=Ω(bn) per ogni a,b≥1
Un polinomio è dominato da qualunque esponenziale e domina qualunque radice.
Scala degli ordini di grandezza (per n→+∞): 1≺loga(n)≺bn≺nc≺dn≺n!≺nn
Notazione Teta
Definizione: Date due funzioni f(n),g(n)≥0, f(n) è in Θ(g(n)) se esistono tre costanti c1,c2 ed n0 tali che c1⋅g(n)≤f(n)≤c2⋅g(n) per ogni n≥n0.
Se f(n) è sia in O(g(n)) sia in Ω(g(n)), allora è anche in Θ(g(n)).
La notazione Teta rappresenta il limite stretto asintotico della funzione.
Calcolo delle Notazioni Asintotiche con i Limiti
\lim_{n\to+\infty} \frac{f(n)}{g(n)} = k > 0 \Rightarrow f(n) = \Theta(g(n))
limn→+∞g(n)f(n)=+∞⇒f(n)=Ω(g(n)) ma f(n)=Θ(g(n))
limn→+∞g(n)f(n)=0⇒f(n)=O(g(n)) ma f(n)=Θ(g(n))
Se il limite del rapporto tra f(n) e g(n) non esiste, allora è necessario procedere diversamente.
Algebra della Notazione Asintotica
Per semplificare il calcolo del costo computazionale tramite limite asintotico degli algoritmi, si possono utilizzare tre regole algebriche:
Regola delle costanti moltiplicative.
Regola della commutatività con somma.
Regola della commutatività con prodotto.
Regola delle Costanti Moltiplicative
Per ogni k > 0 e per ogni f(n)≥0, se f(n) è in O(g(n)), allora anche k⋅f(n) è in O(g(n)).
Per ogni k > 0 e per ogni f(n)≥0, se f(n) è in Ω(g(n)), allora anche k⋅f(n) è in Ω(g(n)).
Per ogni k > 0 e per ogni f(n)≥0, se f(n) è in Θ(g(n)), allora anche k⋅f(n) è in Θ(g(n)).
Le costanti moltiplicative possono essere ignorate durante il calcolo di un qualsiasi limite asintotico.
Attenzione: La costante moltiplicativa non deve essere all'esponente della funzione (es: in f(n)=2k⋅n non possiamo ignorare la k).
Regola della Commutatività con Somma
Sia f(n)=p(n)+q(n). Per ogni p(n), q(n) > 0, vale che:
Se p(n) è in O(g(n)) e q(n) è in O(h(n)), allora f(n) è in O(g(n)+h(n))=O(max(g(n),h(n))).
Se p(n) è in Ω(g(n)) e q(n) è in Ω(h(n)), allora f(n) è in Ω(g(n)+h(n))=Ω(max(g(n),h(n))).
Se p(n) è in Θ(g(n)) e q(n) è in Θ(h(n)), allora f(n) è in Θ(g(n)+h(n))=Θ(max(g(n),h(n))).
Un qualsiasi limite asintotico di f(n) è uguale al massimo tra il limite asintotico di p(n) e di q(n).
Regola della Commutatività con Prodotto
Sia f(n)=p(n)⋅q(n). Per ogni p(n), q(n) > 0, vale che:
Se p(n) è in O(g(n)) e q(n) è in O(h(n)), allora f(n) è in O(g(n)⋅h(n)).
Se p(n) è in Ω(g(n)) e q(n) è in Ω(h(n)), allora f(n) è in Ω(g(n)⋅h(n)).
Se p(n) è in Θ(g(n)) e q(n) è in Θ(h(n)), allora f(n) è in Θ(g(n)⋅h(n)).
Un qualsiasi limite asintotico di f(n) è uguale al prodotto tra il limite asintotico di p(n) e di q(n).
Esercizi svolti sull’algebra asintotica
Trovare il limite asintotico stretto di f(n)=3n2+7: f(n)=3n2+7=Θ(n2)+Θ(1)=Θ(n2)
Trovare il limite asintotico stretto di f(n)=3n2n+4n4: f(n)=3n2n+4n4=Θ(n2n)+Θ(n4)=Θ(n2n)
Trovare il limite asintotico stretto di f(n)=22n: f(n)=22n=2n⋅2n=Θ(2n)⋅Θ(2n)=Θ(22n)
Trovare il limite asintotico stretto di f(n)=logn(n)+8⋅2n⋅log(n)+3: f(n)=logn(n)+8⋅2n⋅log(n)+3=logn(n)+8⋅2log(nn)+3=logn(n)+8⋅nn+3=Θ(logn(n))+Θ(nn)+Θ(1)=Θ(nn)
Trovare il limite asintotico stretto di f(n)=n2⋅log(n): f(n)=n2⋅log(n)=Θ(n2)⋅Θ(log(n))=Θ(n2⋅log(n))
Trovare il limite asintotico stretto di f(n)=3n⋅log(n)+2n2: f(n)=3n⋅log(n)+2n2=Θ(n)⋅Θ(log(n))+Θ(n2)=Θ(n⋅log(n))+Θ(n2)=Θ(n2)
Trovare il limite asintotico stretto di f(n)=2log(n)/2+5n: f(n)=2log(n)/2+5n=(2log(n))1/2+5n=n+5n=Θ(n)+Θ(n)=Θ(n)
Trovare il limite asintotico stretto di f(n)=4log(n): f(n)=4log(n)=(22)log(n)=(2log(n))2=n2=Θ(n2)
Trovare il limite asintotico stretto di f(n)=2log(n) f(n)=2log(n)=2(1/2)⋅log(n)=2log(n)=n=Θ(n)
Sommatorie e Tecniche di dimostrazione
Dimostrare o confutare la seguente proposizione f(n)=4n è in O(2n) Tramite l’algebra asintotica, siamo già in grado di rispondere a tale proposizione:: 4n=22n=2n⋅2n=O(2n)⋅O(2n)=O(22n) Dunque, la proposizione è falsa. Dimostrazione per assurdo: supponiamo che f(n)=O(2n). Allora abbiamo che ∃c,n0∣f(n)≤c⋅2n,∀n≥n0 4n≤c⋅2n 2n⋅2n≤c⋅2n 2n≤c Falso una volta superato un certo valore n0
Dimostrare la seguente proposizione f(n)=(n+10)3 è in Θ(n3)
Per dimostrare che sia Θ(n3), dimostriamo che sia in O(n3) e in Ω(n3):
Al crescere di n, esisterà un valore n0=10 tale che (n+10)3=(n+n0)3≤(n+n)3=(2n)3=8n3=O(n3)
Poiché 10 > 0, si vede facilmente che (n+10)3≥(n+0)3=n3=Ω(n3) Poiché f(n) è sia in O(n3), sia in Ω(n3), allora è anche in Θ(n3)
Dimostrare la seguente proposizione Sn=∑k=1nk è in Θ(n2)
Come nell’esempio precedente, per dimostrare che sia Θ(n2), dimostriamo che sia in O(n2) e in Ω(n2):
Per dimostrare che Sn è in O(n2), è necessario fare un "salto logico". Partiamo riscrivendo la sommatoria in forma estesa Sn=1+2+3+…+(n−1)+n Notiamo come ogni singolo termine della sommatoria sia ≤n. Dunque, possiamo scrivere la seguente disequazione: 1+2+3+…+(n−1)+n≤n+n+n+…+n+n Nella parte destra della disequazione, dunque, abbiamo una somma di n volte n, riscrivibile come n⋅n Sn≤n⋅n⇒Sn≤n2 A questo punto, ci basta notare che n2 è in O(n2) e quindi che, poiché Sn≤n2, di conseguenza anche Sn è in O(n2). Dimostriamo ora che f(n)=Ω(n2) in modo analogo a quello precedente. Riscriviamo nuovamente la sommatoria in forma estesa Sn=1+2+3+…+(n−2)+(n−1)+n Questa volta, notiamo che essa può essere divisa a metà, ottenendo due categorie: 1+2+3+4+5+… Numeri ≤2n…+(n−2)+(n+1)+n Numeri ≥2n A questo punto, è necessario effettuare un ulteriore "salto logico": sappiamo che tutti i numeri minori di 2n sono anche maggiori di 0, mentre quelli maggiori di 2n sono ovviamente maggiori di 2n. Dunque, possiamo scrivere la seguente disequazione: 1+2+3+…+(n−2)+(n−1)+n≥0+0+0+…2n volte …+2n+2n+2n2n volte Sn≥2n⋅2n⇒Sn≥21n2 A questo punto, ci basta notare che 21n2 è in Ω(n2) e quindi che, poiché Sn≥21n2, di conseguenza anche Sn è in Ω(n2). Poiché Sn è sia in O(n2), sia in Ω(n2), allora è anche in Θ(n2)
Vediamo ora un ulteriore modo per poter dimostrare tale proposizione: Sn=∑k=1nk è in Θ(n2)
Anche in questa dimostrazione, riscriviamo nuovamente la sommatoria in forma estesa, ma anche in forma invertita: Sn=1+2+3+…+(n−2)+(n−1)+nSn=n+(n−1)+(n−2)+…+3+2+1
Sommando Sn con se stessa, otteniamo il seguente risultato: Sn 1 2 3 …… n − 2 n − 1 n Sn n n − 1 n − 2 …… 3 2 1 2Sn n + 1 n + 1 n + 1 …… n + 1 n + 1 n + 1 Dunque, 2Sn=(n+1)+(n+1)+…+(n+1)+(n+1)=n(n+1)
A questo punto, ci basta sfruttare alcune proprietà algebriche 2Sn=n(n+1)Sn=2n(n+1)=2n2+n=Θ(n2)
Dimostrare la seguente proposizione Sn=∑k=0n2k è in Θ(2n)
Riscriviamo la somma in forma estesa per poi moltiplicarla per 2 Sn=1+2+22+23+…+2n−1+2n2⋅Sn=2⋅(1+2+22+23+…+2n−1+2n) 2Sn=2+22+23+24+…+2n+2n+1
Gli unici termini non condivisi tra Sn e 2Sn sono 1 e 2n+1 2Sn=2+22+23+24+…+2n+2n+1Sn=1+2+22+23+…+2n
Dunque otteniamo che Sn=2Sn−Sn=2n+1−1
A questo punto calcoliamo il limite asintotico del risultato Sn=2n+1−1=2n⋅2−1=Θ(2n)+Θ(−1)=Θ(2n)
Dimostrare la seguente proposizione ∑k=1nk⋅2k è in Θ(n⋅2n)
Riscriviamo in forma estesa e moltiplichiamo per 2 Sn=21+2⋅22+3⋅23+…+(n−1)⋅2n−1+n⋅2n2Sn=22+2⋅23+3⋅24+…+(n−1)⋅2n+n⋅2n+1
Effettuiamo qualche passaggio algebrico Sn=2Sn−Sn=−21−22−23−24−…−2n−1+n⋅2n+1−Sn=−(−21−22−23−24−…−2n+n⋅2n+1) −Sn=21+22+23+24+…+2n−n⋅2n+1
A questo punto, è necessario ricordarsi che nella dimostrazione precedente abbiamo ottenuto che ∑k=0n2k=1+21+22+23+24+…+2n=2n+1−1 dunque, possiamo riscrivere −Sn come −Sn=21+22+23+24+…+2n−n⋅2n+1=(∑k=0n2k)−1−n⋅2n+1=2n+1−2−n⋅2n+1 per poi calcolare Sn −(−Sn)=−(2n+1−2−n⋅2n+1) =−2n+1+2+n⋅2n+1
Infine, calcoliamo il limite asintotico del risultato Sn=−2n+1+2+n⋅2n+1=Θ(2n)+Θ(2)+Θ(n⋅2n)=Θ(n⋅2n)
Dimostrare la seguente proposizione ∑k=1nlog(k) è in Θ(n⋅log(n))
Riscriviamo la sommatoria in forma estesa in modo da applicare le proprietà dei logaritmi Sn=log(1)+log(2)+log(3)+…+log(n) =log(1⋅2⋅3⋅…⋅n)=log(n!)
Dunque, ignorando la costante c, verifichiamo l’ipotesi log(n!)≤n⋅log(n)