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 →).

Complessità 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 \,\}.

Altre analisi (caso medio, probabilistica) esistono, ma qui si usa il caso pessimo.

Il metodo pratico. 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). 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: 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 il ciclo interno fa i+1i+1 iterazioni, quindi il costo è proporzionale a ∑i=0n−1(i+1)=n(n+1)2∈Θ(n2)\sum_{i=0}^{n-1}(i+1) = \frac{n(n+1)}{2} \in \Theta(n^2). Nella seconda si riusa la somma precedente: Θ(n)\Theta(n). Stesso problema, algoritmi di efficienza molto diversa.

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). Sull'array già ordinato servono solo n−1n-1 confronti: per questo si parla di caso pessimo.

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

Limiti dell'analisi

  • Caso pessimo: può riguardare istanze patologiche mentre quelle di interesse costano meno (esempio: QuickSort); si può restringere il dominio delle istanze o analizzare il caso medio.
  • Asintotica: le costanti ignorate possono contare. tA(n)=10100nt_A(n) = 10^{100} n è Θ(n)\Theta(n) e tB(n)=n2t_B(n) = n^2 è peggiore asintoticamente, ma tA<tBt_A < t_B solo per n>10100n > 10^{100}, cioè mai in pratica. Così 400log⁡2n400 \log_2 n batte log⁡22n\log_2^2 n solo per n>2400n > 2^{400}.

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