Salta al contenuto
Note per Studenti Complessità in tempo e caso pessimo

Complessità in tempo e caso pessimo

In questa pagina 8

Perché serve un'analisi teorica

Il tempo di esecuzione di un programma dipende dall'istanza (a parità di taglia, input diversi possono costare molto diversamente: pensa a InsertionSort su un array già ordinato o invertito), dall'hardware e dal software. Misurarlo (in Java con System.currentTimeMillis() prima e dopo l'esecuzione) ha limiti: non copre tutti gli input, richiede di implementare l'algoritmo, e il confronto tra algoritmi dipende da macchina e implementazione.

Servono quindi un'analisi che consideri tutti gli input, permetta di confrontare algoritmi senza il tempo esatto, e parta dallo pseudocodice. L'approccio: caso pessimo in funzione della taglia, conteggio di passi nel modello RAM (vedi Problemi computazionali e algoritmiProblema computazionale come insieme di coppie (istanza, soluzione); algoritmo e modello di calcolo RAM; pseudocodice; taglia di un'istanza; ADT e struttura dati concreta; esempio svolto con ricerca lineare e binaria in un array ordinato.Problemi computazionali e algoritmi →), analisi asintotica (vedi Notazione asintoticaDefinizioni di O, Omega, Theta e o piccolo con le costanti c ed n0; esempi con costanti esplicite; proprietà (polinomi, esponenziali, logaritmi, somme, implicazioni tra notazioni); sommatorie notevoli; terminologia (logaritmica, lineare, polinomiale, esponenziale).Notazione asintotica →). In Fondamenti di Informatica lo stesso tema è trattato in modo più elementare in Complessità computazionaleCosto di un algoritmo in funzione della dimensione dell'input; caso peggiore, migliore e medio; notazione O-grande e classi di crescita; come contare i passi di cicli e ricorsioni; costo delle operazioni Python.Complessità computazionale →; qui si formalizza.

Complessità al caso pessimo

Definizione (complessità in tempo al caso pessimo). Sia tA,it_{A,i} il numero di operazioni eseguite da AA sull'istanza ii. La complessità in tempo al caso pessimo è

tA(n)=max⁡{ tA,i:i∈I, i ha taglia n }.t_A(n) = \max\{\, t_{A,i} : i \in I,\ i \text{ ha taglia } n \,\}.

Esempio. InsertionSort su n=4n=4 elementi fa 33 confronti sull'array ordinato [1,2,3,4][1,2,3,4] e 66 sull'array invertito [4,3,2,1][4,3,2,1]: il caso pessimo è il massimo, 66.

Altre analisi (caso medio, probabilistica) esistono, ma qui si usa il caso pessimo. Il caso medio richiede di assegnare una probabilità a ciascuna istanza e di calcolare un valore attesoIl valore atteso E[X] = Σ x p_X(x) è la media dei valori di X pesata con le loro probabilità (esiste se la serie converge assolutamente); per una funzione g vale E[g(X)] = Σ g(x) p_X(x) senza trovare la legge di g(X), ed E è lineare: E[aX + bY + c] = aE[X] + bE[Y] + c.Valore atteso → del costo; il caso pessimo no, ed è una garanzia valida per qualunque input.

Proprietà (stima con limiti superiore e inferiore). Trovare l'istanza peggiore e contare i passi esatti è difficile e inutile. Si cercano due funzioni semplici:

  • limite superiore tA(n)∈O(g(n))t_A(n) \in O(g(n)): si argomenta che per ogni nn abbastanza grande, ogni istanza di taglia nn costa al più c g(n)c\,g(n) operazioni;
  • limite inferiore tA(n)∈Ω(g(n))t_A(n) \in \Omega(g(n)): si argomenta che per ogni nn abbastanza grande esiste un'istanza di taglia nn che costa almeno c g(n)c\,g(n) (a volte è comodo mostrare che tutte le istanze costano tanto).

Se coincidono si ha Θ(g(n))\Theta(g(n)) (stima stretta; le definizioni di OO, Ω\Omega e Θ\Theta con le costanti cc ed n0n_0 sono in Notazione asintoticaDefinizioni di O, Omega, Theta e o piccolo con le costanti c ed n0; esempi con costanti esplicite; proprietà (polinomi, esponenziali, logaritmi, somme, implicazioni tra notazioni); sommatorie notevoli; terminologia (logaritmica, lineare, polinomiale, esponenziale).Notazione asintotica →). La funzione gg deve essere la più vicina possibile al vero andamento (tight) e la più semplice (niente costanti, niente termini di ordine inferiore).

Esempio. Se ogni istanza di taglia nn costa al più c1n+c2c_1n+c_2 operazioni e una di esse ne costa almeno c3nc_3n, allora t(n)∈O(n)∩Ω(n)=Θ(n)t(n)\in O(n)\cap\Omega(n)=\Theta(n).

Esempio: arrayMax

Algoritmo arrayMax(A, n)
Input: array A di n >= 1 interi
Output: massimo di A
currMax <- A[0]
for i <- 1 to n-1 do
    if currMax < A[i] then currMax <- A[i]
return currMax

Fuori dal ciclo ci sono operazioni in numero costante; il ciclo fa n−1n-1 iterazioni, ciascuna di costo costante. Esistono quindi c1,c2,c3,c4>0c_1, c_2, c_3, c_4 > 0 con c3n+c4≤t(n)≤c1n+c2c_3 n + c_4 \le t(n) \le c_1 n + c_2: t(n)∈Θ(n)t(n) \in \Theta(n). Non servono i valori delle costanti.

Esempio: prefix averages

Dato X[0..n−1]X[0..n-1], calcolare A[i]=1i+1∑j=0iX[j]A[i] = \frac{1}{i+1}\sum_{j=0}^{i} X[j].

prefixAverages1(X, n)            prefixAverages2(X, n)
for i <- 0 to n-1 do             s <- 0
    a <- 0                       for i <- 0 to n-1 do
    for j <- 0 to i do               s <- s + X[i]
        a <- a + X[j]                A[i] <- s / (i+1)
    A[i] <- a / (i+1)            return A
return A

Nella prima versione, per ogni valore di ii il ciclo interno (jj da 00 a ii) fa i+1i+1 iterazioni. Il costo totale è quindi proporzionale a ∑i=0n−1(i+1)=∑k=1nk=n(n+1)2∈Θ(n2),\sum_{i=0}^{n-1}(i+1) = \sum_{k=1}^{n} k = \frac{n(n+1)}{2} \in \Theta(n^2), dove si è posto k=i+1k=i+1 (la somma passa da i=0..n−1i=0..n-1 a k=1..nk=1..n) e si è usata la somma dei primi nn interi (vedi SommatorieIl simbolo di sommatoria, le sue proprietà (linearità, additività, cambio di indice) e le somme notevoli di Gauss e geometrica.Sommatorie →; la formula si dimostra per induzione: Principio di induzionePer dimostrare una proprietà per ogni n basta il passo base e il passo induttivo da n a n+1.Principio di induzione →). Ad esempio per n=4n=4: 1+2+3+4=10=4⋅521+2+3+4=10=\frac{4\cdot5}{2}. Il termine n(n+1)2=n22+n2\frac{n(n+1)}{2}=\frac{n^2}{2}+\frac{n}{2} è un polinomio di grado 22, quindi Θ(n2)\Theta(n^2). Nella seconda si riusa la somma precedente: s contiene già X[0]+⋯+X[i−1]X[0]+\dots+X[i-1], basta aggiungere X[i]X[i] (una sola addizione) invece di rifare tutte le i+1i+1; il ciclo fa nn iterazioni di costo costante, Θ(n)\Theta(n). Stesso problema, algoritmi di efficienza molto diversa.

Grafico interattivo: Iterazioni del ciclo interno di prefixAverages1, n(n+1)/2, e del ciclo unico di prefixAverages2, n, in funzione di n: a n = 50 sono 1275 contro 50, e il rapporto (n+1)/2 cresce con n

Esempio: InsertionSort

Algoritmo InsertionSort(S)
for i <- 1 to n-1 do
    curr <- S[i]; j <- i-1
    while j >= 0 AND S[j] > curr do
        S[j+1] <- S[j]; j <- j-1
    S[j+1] <- curr
  • Ogni istanza di taglia nn esegue al più c n2c\,n^2 operazioni: ciclo esterno n−1n-1 volte, interno al più ii volte, ∑i=O(n2)\sum i = O(n^2). Quindi f1(n)=n2f_1(n) = n^2.
  • Ogni istanza esegue almeno c nc\,n operazioni (il ciclo esterno gira sempre n−1n-1 volte): f2(n)=nf_2(n) = n.
  • Esiste un'istanza con c n2c\,n^2 operazioni: l'array in ordine decrescente, dove ogni elemento percorre tutto il prefisso (n(n−1)2\frac{n(n-1)}{2} confronti; verificato: per n=10n=10 sono 4545). f3(n)=n2f_3(n) = n^2.

Da f1f_1 e f3f_3: tIS(n)∈O(n2)∩Ω(n2)=Θ(n2)t_{IS}(n) \in O(n^2) \cap \Omega(n^2) = \Theta(n^2).

Perché n(n−1)2\frac{n(n-1)}{2} sull'array decrescente. All'iterazione ii (con i=1,…,n−1i=1,\dots,n-1) l'elemento curr = S[i] è minore di tutti i precedenti, quindi S[j] > curr è vero per j=i−1,…,0j=i-1,\dots,0: si contano ii confronti (più uno finale quando jj esce dall'array, che non confronta nulla). In totale ∑i=1n−1i=(n−1)n2\sum_{i=1}^{n-1} i = \frac{(n-1)n}{2} (stessa somma, vedi SommatorieIl simbolo di sommatoria, le sue proprietà (linearità, additività, cambio di indice) e le somme notevoli di Gauss e geometrica.Sommatorie →). Traccia su n=4n=4, array [4,3,2,1][4,3,2,1]:

ii curr array prima confronti con S[j] > curr veri array dopo
1 3 [4,3,2,1][4,3,2,1] 1 [3,4,2,1][3,4,2,1]
2 2 [3,4,2,1][3,4,2,1] 2 [2,3,4,1][2,3,4,1]
3 1 [2,3,4,1][2,3,4,1] 3 [1,2,3,4][1,2,3,4]

Totale 1+2+3=6=4⋅321+2+3=6=\frac{4\cdot3}{2}, come nell'esempio sopra. Sull'array già ordinato ogni curr è maggiore del precedente, il test fallisce subito e si fa un solo confronto per iterazione: n−1n-1 in tutto. Sull'array già ordinato servono solo n−1n-1 confronti: per questo si parla di caso pessimo.

Grafico interattivo: Confronti di InsertionSort in funzione di n: n(n−1)/2 sull'array decrescente (caso pessimo, 45 per n = 10) e n−1 sull'array già ordinato (caso migliore): la complessità al caso pessimo è quadratica, ma su input ordinato l'algoritmo è lineare

Efficienza asintotica e tempi

Se tA(n)∈o(tB(n))t_A(n) \in o(t_B(n)) allora AA è asintoticamente più efficiente di BB. Regola pratica: complessità polinomiale (o migliore) ⇒\Rightarrow algoritmo efficiente; esponenziale ⇒\Rightarrow inefficiente. Taglia massima nτn_\tau risolvibile in tempo τ\tau (con tAt_A in nanosecondi):

tA(n)t_A(n) 1 secondo 1 minuto 1 ora
log⁡2n\log_2 n 21092^{10^9} (illimitata) illimitata illimitata
nn 10910^9 6⋅10106 \cdot 10^{10} 3,6⋅10123{,}6 \cdot 10^{12}
n2n^2 ≈3⋅104\approx 3 \cdot 10^4 ≈2,4⋅105\approx 2{,}4 \cdot 10^5 ≈1,9⋅106\approx 1{,}9 \cdot 10^6
2n2^n ≈30\approx 30 ≈36\approx 36 ≈42\approx 42

Come si leggono i numeri. Un secondo sono 10910^9 ns, un minuto 6⋅10106\cdot10^{10} ns, un'ora 3,6⋅10123{,}6\cdot10^{12} ns. Per tA(n)=nt_A(n)=n si risolve n≤τn \le \tau e basta. Per tA(n)=n2t_A(n)=n^2 si risolve n2≤τn^2 \le \tau, cioè n≤τn \le \sqrt{\tau}: 109≈31 623\sqrt{10^9}\approx 31\,623, 6⋅1010≈244 949\sqrt{6\cdot10^{10}}\approx 244\,949, 3,6⋅1012≈1,9⋅106\sqrt{3{,}6\cdot10^{12}}\approx 1{,}9\cdot10^6. Per tA(n)=2nt_A(n)=2^n si risolve 2n≤τ2^n \le \tau, cioè n≤log⁡2τn \le \log_2 \tau (vedi Esponenziale e logaritmoLa funzione esponenziale a^x (base positiva diversa da 1) e la sua inversa, il logaritmo in base a, con grafici e proprietà.Esponenziale e logaritmo →): log⁡2109≈29,9\log_2 10^9\approx 29{,}9, log⁡2(6⋅1010)≈35,8\log_2(6\cdot10^{10})\approx 35{,}8, log⁡2(3,6⋅1012)≈41,7\log_2(3{,}6\cdot10^{12})\approx 41{,}7. Per tA(n)=log⁡2nt_A(n)=\log_2 n la condizione log⁡2n≤τ\log_2 n \le \tau dà n≤2τn \le 2^{\tau}, un numero con miliardi di cifre. Il messaggio: moltiplicare per 3600 il tempo a disposizione (da 1 s a 1 ora) fa passare l'algoritmo esponenziale da n≈30n\approx30 a n≈42n\approx42, mentre quello quadratico guadagna un fattore 6060 sulla taglia.

Limiti dell'analisi

Errori comuni

  • Dire "BB è sempre più veloce di AA" perché nlog⁡n<n2n \log n < n^2: l'asintotica vale per nn grandi, le costanti contano per nn piccoli.
  • Calcolare il caso pessimo scegliendo un'istanza a caso, o provare solo il limite superiore e dichiarare Θ\Theta.
  • Mettere costanti o termini minori nel risultato (Θ(3n+2)\Theta(3n + 2) invece di Θ(n)\Theta(n)).

Versione ripasso

Teoria collegata