Esiste un sistema dietro la magia dell'analisi algoritmica?


159

Ci sono un sacco di domande su come analizzare il tempo di esecuzione degli algoritmi (vedi, ad esempio, e ). Molti sono simili, ad esempio quelli che richiedono un'analisi dei costi di loop nidificati o algoritmi di divisione e conquista, ma la maggior parte delle risposte sembra essere fatta su misura.

D'altra parte, le risposte a un'altra domanda generale spiegano il quadro più ampio (in particolare per quanto riguarda l'analisi asintotica) con alcuni esempi, ma non come sporcarsi le mani.

Esiste un metodo strutturato e generale per analizzare il costo degli algoritmi? Il costo potrebbe essere il tempo di esecuzione (complessità temporale) o qualche altra misura del costo, come il numero di confronti eseguiti, la complessità dello spazio o qualcos'altro.

Questo dovrebbe diventare una domanda di riferimento che può essere usata per indicare ai principianti; da qui il suo ambito più ampio del solito. Prestare attenzione a dare risposte generali, presentate in modo didattico, illustrate da almeno un esempio, ma che coprano comunque molte situazioni. Grazie!


3
Grazie all'autore o agli autori di StackEdit per aver reso conveniente la scrittura di post così lunghi, e i miei lettori beta FrankW , Juho , Gilles e Sebastian per avermi aiutato a risolvere una serie di difetti che le bozze precedenti avevano.
Raffaello

1
Hey @Raphael, questa è roba meravigliosa. Ho pensato di suggerire di metterlo insieme come PDF per circolare? Questo genere di cose potrebbe diventare un riferimento davvero utile.
avuto il

1
@hadsed: Grazie, sono felice che ti sia utile! Per ora, preferisco che circoli un link a questo post. Tuttavia, il contenuto degli utenti SE è "concesso in licenza in cc by-sa 3.0 con attribuzione obbligatoria" (vedere piè di pagina) in modo che chiunque possa creare un PDF da esso, purché sia ​​data l'attribuzione.
Raffaello

2
Non sono particolarmente competente al riguardo, ma è normale che non vi sia alcun riferimento al teorema del Maestro in nessuna risposta?
babou,

1
@babou Non so cosa significhi "normale" qui. Dal mio punto di vista, il teorema del Maestro non ha nulla a che fare con questo: si tratta di analizzare algoritmi, il teorema del Maestro è uno strumento molto specifico per risolvere (alcune) ricorrenze (e in modo molto approssimativo). Poiché la matematica è stata trattata altrove (ad esempio qui ), ho scelto di coprire solo la parte dall'algoritmo alla matematica qui. Fornisco riferimenti a post che trattano del lavoro di matematica nella mia risposta.
Raffaello

Risposte:


134

Traduzione del codice in matematica

Data una semantica operativa (più o meno) formale , puoi tradurre il codice (pseudo-) di un algoritmo letteralmente in un'espressione matematica che ti dà il risultato, a condizione che tu possa manipolare l'espressione in una forma utile. Questo funziona bene per misure di costo additivo come numero di confronti, scambi, dichiarazioni, accessi alla memoria, cicli di cui alcune macchine astratte hanno bisogno e così via.

Esempio: confronti in Bubblesort

Considera questo algoritmo che ordina un determinato array A:

 bubblesort(A) do                   1
  n = A.length;                     2
  for ( i = 0 to n-2 ) do           3
    for ( j = 0 to n-i-2 ) do       4
      if ( A[j] > A[j+1] ) then     5
        tmp    = A[j];              6
        A[j]   = A[j+1];            7
        A[j+1] = tmp;               8
      end                           9
    end                             10
  end                               11
end                                 12

Supponiamo di voler eseguire la consueta analisi dell'algoritmo di ordinamento, ovvero contare il numero di confronti tra elementi (riga 5). Notiamo immediatamente che questa quantità non dipende dal contenuto dell'array A, ma solo dalla sua lunghezza . Quindi possiamo tradurre i loop (nidificati) letteralmente in somme (nidificate); la variabile loop diventa la variabile di somma e l'intervallo viene trasferito. Noi abbiamo:nfor

Ccmp(n)=i=0n2j=0ni21==n(n1)2=(n2) ,

dove è il costo per ogni esecuzione della riga 5 (che contiamo).1

Esempio: scambi in Bubblesort

Indicheremo con il sottoprogramma che consiste in righe per e con i costi per l'esecuzione di questo sottoprogramma (una volta).Pi,jijCi,j

Supponiamo ora di contare gli swap , ovvero la frequenza con cui viene eseguito . Questo è un "blocco di base", ovvero un sottoprogramma che viene sempre eseguito atomicamente e ha un costo costante (qui, ). Contrarre tali blocchi è un'utile semplificazione che spesso applichiamo senza pensarci o parlarne.P6,81

Con una traduzione simile a quella precedente, arriviamo alla seguente formula:

Cswaps(A)=i=0n2j=0ni2C5,9(A(i,j)) .

A(i,j) indica lo stato dell'array prima dell'iterazione di .(i,j)P5,9

Si noti che uso invece di come parametro; vedremo presto perché. Non aggiungo e come parametri di poiché i costi non dipendono da questi qui (nel modello di costo uniforme , cioè); in generale, potrebbero semplicemente.AnijC5,9

Chiaramente, i costi di dipendono dal contenuto di (i valori e , nello specifico), quindi dobbiamo tenerne conto. Ora affrontiamo una sfida: come "scartare" ? Bene, possiamo rendere esplicita la dipendenza dal contenuto di :P5,9AA[j]A[j+1]C5,9A

C5,9(A(i,j))=C5(A(i,j))+{1,A(i,j)[j]>A(i,j)[j+1]0,else .

Per ogni dato array di input, questi costi sono ben definiti, ma vogliamo una dichiarazione più generale; dobbiamo fare ipotesi più forti. Esaminiamo tre casi tipici.

  1. Il caso peggiore

    Solo guardando la somma e notando che , possiamo trovare un limite superiore banale per il costo:C5,9(A(i,j)){0,1}

    Cswaps(A)i=0n2j=0ni21=n(n1)2=(n2) .

    Ma può succedere , cioè c'è una per questo limite superiore raggiunto? A quanto pare, sì: se immettiamo un array inversamente ordinato di elementi distinti a coppie, ogni iterazione deve eseguire uno scambio¹. Pertanto, abbiamo derivato il numero esatto di swap di Bubblesort nel caso peggiore.A

  2. Il caso migliore

    Al contrario, esiste un limite inferiore banale:

    Cswaps(A)i=0n2j=0ni20=0 .

    Questo può accadere anche: su un array già ordinato, Bubblesort non esegue un singolo scambio.

  3. Il caso medio

    Il peggiore e il migliore dei casi apre abbastanza un vuoto. Ma qual è il numero tipico di swap? Per rispondere a questa domanda, dobbiamo definire cosa significa "tipico". In teoria, non abbiamo motivo di preferire un input a un altro e quindi di solito assumiamo una distribuzione uniforme su tutti i possibili input, ovvero ogni input è ugualmente probabile. Ci limitiamo alle matrici con elementi distinti a coppie e quindi assumiamo il modello di permutazione casuale .

    Quindi, possiamo riscrivere i nostri costi in questo modo²:

    E[Cswaps]=1n!Ai=0n2j=0ni2C5,9(A(i,j))

    Ora dobbiamo andare oltre la semplice manipolazione delle somme. Osservando l'algoritmo, notiamo che ogni scambio rimuove esattamente una inversione in (scambiamo sempre e solo i vicini³). Cioè, il numero di scambi effettuati su è esattamente il numero di inversioni di . Quindi, possiamo sostituire le due somme interne e ottenereAAinv(A)A

    E[Cswaps]=1n!Ainv(A) .

    Fortunatamente per noi, il numero medio di inversioni è stato determinato essere

    E[Cswaps]=12(n2)

    quale è il nostro risultato finale. Si noti che questa è esattamente la metà del costo peggiore.


  1. Si noti che l'algoritmo è stato accuratamente formulato in modo che i = n-1non venga eseguita "l'ultima" iterazione del ciclo esterno che non fa mai nulla.
  2. " " è la notazione matematica per "valore atteso", che qui è solo la media.E
  3. Impariamo lungo la strada che nessun algoritmo che scambia solo elementi vicini può essere asintoticamente più veloce di Bubblesort (anche in media) - il numero di inversioni è un limite inferiore per tutti questi algoritmi. Questo vale ad esempio per Inserimento inserzione e Selezione ordinamento .

Il metodo generale

Nell'esempio abbiamo visto che dobbiamo tradurre la struttura di controllo in matematica; Presenterò un insieme tipico di regole di traduzione. Abbiamo anche visto che il costo di un dato sottoprogramma può dipendere dallo stato corrente , ovvero (approssimativamente) i valori correnti delle variabili. Poiché l'algoritmo (di solito) modifica lo stato, il metodo generale è leggermente complicato da notare. Se inizi a sentirti confuso, ti suggerisco di tornare all'esempio o inventare il tuo.

Indichiamo con lo stato corrente (immaginatelo come un insieme di assegnazioni di variabili). Quando eseguiamo un programma che inizia in stato , finiamo in stato (fornito termina).ψPψψ/PP

  • Dichiarazioni individuali

    Dato solo un singolo estratto conto S;, lo assegni costa . Questa sarà in genere una funzione costante.CS(ψ)

  • espressioni

    Se si dispone di un'espressione Edel modulo E1 ∘ E2(ad esempio, un'espressione aritmetica in cui può essere aggiunta o moltiplicazione, si sommano i costi in modo ricorsivo:

    CE(ψ)=c+CE1(ψ)+CE2(ψ) .

    Nota che

    • il costo dell'operazione non sia costante, ma dipende dai valori di e ecE1E2
    • la valutazione delle espressioni può cambiare lo stato in molte lingue,

    quindi potresti dover essere flessibile con questa regola.

  • Sequenza

    Dato un programma Pcome sequenza di programmi Q;R, si aggiungono i costi a

    CP(ψ)=CQ(ψ)+CR(ψ/Q) .

  • Condizionali

    Dato un programma Pdel modulo if A then Q else R end, i costi dipendono dallo stato:

    CP(ψ)=CA(ψ)+{CQ(ψ/A),A evaluates to true under ψCR(ψ/A),else

    In generale, la valutazione Apuò benissimo cambiare lo stato, quindi l'aggiornamento per i costi delle singole filiali.

  • Per-Loops

    Dato un programma Pdel modulo for x = [x1, ..., xk] do Q end, assegnare i costi

    CP(ψ)=cinit_for+i=1kcstep_for+CQ(ψi{x:=xi})

    dove è lo stato prima dell'elaborazione per value , ovvero dopo l'iterazione con essere impostato su , ..., .ψiQxixx1xi-1

    Nota le costanti extra per la manutenzione del circuito; la variabile loop deve essere creata ( ) e assegnata i suoi valori ( ). Questo è rilevante da alloracinit_forcstep_for

    • calcolare il prossimo xipuò essere costoso e
    • a for-loop con corpo vuoto (ad es. dopo aver semplificato l'impostazione di un caso migliore con un costo specifico) non ha costo zero se esegue iterazioni.
  • Mentre-Loops

    Dato un programma Pdel modulo while A do Q end, assegnare i costi

    CP(ψ) =CA(ψ)+{0,A evaluates to false under ψCQ(ψ/A)+CP(ψ/A;Q), else

    Ispezionando l'algoritmo, questa ricorrenza può essere spesso rappresentata come una somma simile a quella per i for-loop.

    Esempio: considerare questo breve algoritmo:

    while x > 0 do    1
      i += 1          2
      x = x/2         3
    end               4
    

    Applicando la regola, otteniamo

    C1,4({i:=i0;x:=x0}) =c<+{0,x00c+=+c/+C1,4({i:=i0+1;x:=x0/2}), else

    con alcuni costi costanti per le singole dichiarazioni. Partiamo dal presupposto implicito che questi non dipendono dallo stato (i valori di e ); questo può o non può essere vero nella "realtà": pensa agli overflow!cix

    Ora dobbiamo risolvere questa ricorrenza per . Notiamo che né il numero di iterazioni né il costo del corpo del loop dipendono dal valore di , quindi possiamo lasciarlo cadere. Siamo rimasti con questa ricorrenza:C1,4i

    C1,4(x)={c>,x0c>+c+=+c/+C1,4(x/2), else

    Questo risolve con mezzi elementari per

    C1,4(ψ)=log2ψ(x)(c>+c+=+c/)+c> ,

    reintrodurre simbolicamente lo stato completo; if , quindi .ψ={,x:=5,}ψ(x)=5

  • Chiamate di procedura

    Dato un programma Pdel modulo M(x)per alcuni parametri in xcui Mè una procedura con parametro (denominato) p, assegnare i costi

    CP(ψ)=ccall+CM(ψglob{p:=x}) .

    Nota di nuovo la costante extra (che potrebbe in effetti dipendere da !). Le chiamate di procedura sono costose a causa del modo in cui sono implementate su macchine reali e talvolta addirittura dominano il tempo di esecuzione (ad esempio, valutando in modo ingenuo la ricorrenza del numero di Fibonacci).ccallψ

    Cerco alcuni problemi semantici che potresti avere con lo stato qui. Dovrai distinguere lo stato globale e tale locale dalle chiamate di procedura. Supponiamo di passare qui solo lo stato globale e di Mottenere un nuovo stato locale, inizializzato impostando il valore di pto x. Inoltre, xpuò essere un'espressione che (di solito) assumiamo di essere valutata prima di passarla.

    Esempio: considerare la procedura

    fac(n) do                  
      if ( n <= 1 ) do         1
        return 1               2
      else                     3
        return n * fac(n-1)    4
      end                      5
    end                        
    

    Secondo le regole, otteniamo:

    Cfac({n:=n0})=C1,5({n:=n0})=c+{C2({n:=n0}),n01C4({n:=n0}), else=c+{creturn,n01creturn+c+ccall+Cfac({n:=n01}), else

    Si noti che ignoriamo lo stato globale, poiché facchiaramente non si accede a nessuno. Questa ricorrenza particolare è facile da risolvere

    Cfac(ψ)=ψ(n)(c+creturn)+(ψ(n)1)(c+ccall)

Abbiamo coperto le funzionalità della lingua che incontrerai nel tipico pseudo codice. Fai attenzione ai costi nascosti quando analizzi lo pseudo codice di alto livello; in caso di dubbio, spiega. La notazione può sembrare ingombrante ed è certamente una questione di gusti; i concetti elencati non possono essere ignorati, comunque. Tuttavia, con una certa esperienza sarai in grado di vedere subito quali parti dello stato sono rilevanti per quale misura di costo, ad esempio "dimensione del problema" o "numero di vertici". Il resto può essere lasciato cadere - questo semplifica notevolmente le cose!

Se pensi ora che sia troppo complicato, tieni presente: lo è ! Derivare i costi esatti degli algoritmi in qualsiasi modello così vicino alle macchine reali da consentire previsioni di runtime (anche relative) è uno sforzo arduo. E questo non sta nemmeno prendendo in considerazione la memorizzazione nella cache e altri effetti negativi su macchine reali.

Pertanto, l'analisi dell'algoritmo è spesso semplificata al punto da essere matematicamente trattabile. Ad esempio, se non sono necessari costi esatti, è possibile sovrastimare o sottostimare in qualsiasi momento (per limiti superiori o inferiori): ridurre l'insieme di costanti, eliminare i condizionali, semplificare le somme e così via.

Una nota sul costo asintotico

Ciò che di solito troverai in letteratura e sui siti web è l '"analisi Big-Oh". Il termine corretto è analisi asintotica , il che significa che invece di derivare costi esatti come abbiamo fatto negli esempi, si danno solo costi fino a un fattore costante e nel limite (approssimativamente parlando, "per big ").n

Questo è (spesso) giusto in quanto affermazioni astratte hanno in realtà alcuni costi (generalmente sconosciuti), a seconda della macchina, del sistema operativo e di altri fattori, e tempi di esecuzione brevi possono essere dominati dal sistema operativo che imposta il processo in primo luogo e quant'altro. Quindi ottieni qualche perturbazione, comunque.

Ecco come l'analisi asintotica si collega a questo approccio.

  1. Identificare le operazioni dominanti (che inducono costi), ovvero le operazioni che si verificano più spesso (fino a fattori costanti). Nell'esempio di Bubblesort, una possibile scelta è il confronto nella riga 5.

    In alternativa, vincolare tutte le costanti per le operazioni elementari dal loro massimo (dall'alto) risp. il loro minimo (dal basso) ed eseguono la consueta analisi.

  2. Eseguire l'analisi utilizzando i conteggi di esecuzione di questa operazione come costo.
  3. Quando si semplifica, consentire le stime. Assicurati di consentire le stime dall'alto solo se il tuo obiettivo è un limite superiore ( ) risp. dal basso se vuoi limiti inferiori ( ).OΩ

Assicurati di comprendere il significato dei simboli Landau . Ricorda che tali limiti esistono per tutti e tre i casi ; l'uso di non implica un'analisi del caso peggiore.O

Ulteriori letture

Ci sono molte altre sfide e trucchi nell'analisi dell'algoritmo. Ecco alcune letture consigliate.

Ci sono molte domande intorno che usano tecniche simili a questa.


1
forse qualche riferimento ed esempi al teorema principale (e alle sue estensioni ) per l'analisi asintotica
Nikos M.

@NikosM È fuori portata qui (vedi anche i commenti sulla domanda sopra). Si noti che collego al nostro post di riferimento sulla risoluzione delle ricorrenze che presenta il teorema del Maestro et al.
Raffaello

@Nikos M: il mio $ 0,02: mentre il teorema principale funziona per diverse ricorrenze, non per molte altre; ci sono metodi standard per risolvere le ricorrenze. E ci sono algoritmi per i quali non avremo nemmeno una ricorrenza dando il tempo di esecuzione; potrebbero essere necessarie alcune tecniche di conteggio avanzate. Per qualcuno con una buona preparazione matematica, suggerisco il libro eccellente di Sedgewick e Flajolet, "Analisi degli algoritmi", che ha capitoli come "relazioni di ricorrenza", "funzioni generatrici" e "approssimazioni asintotiche". Le strutture di dati si presentano come esempi occasionali e l'attenzione è rivolta ai metodi!
Jay,

@Raphael Non riesco a trovare alcuna menzione sul web per questo metodo "Tradurre il codice in matematica" basato sulla semantica operativa. Potete fornire riferimenti a libri, documenti o articoli che trattano in modo più formale? O nel caso sia stato sviluppato da te, hai qualcosa di più approfondito?
Wyvern666,

1
@ Wyvern666 Sfortunatamente no. L'ho inventato da solo, per quanto qualcuno possa pretendere di inventare qualcosa del genere. Forse scriverò un'opera citabile a un certo punto. Detto questo, l'intero corpus di lavoro sulla combinatoria analitica (Flajolet, Sedgewick e molti altri) è il fondamento di questo. Non si preoccupano della semantica formale del "codice" per la maggior parte del tempo, ma forniscono la matematica per gestire i costi additivi degli "algoritmi" in generale. Sinceramente penso che i concetti qui esposti non siano molto profondi, ma lo sono anche i concetti matematici.
Raffaello

29

Conti di esecuzione delle dichiarazioni

C'è un altro metodo, sostenuto da Donald E. Knuth nella sua serie The Art of Computer Programming . Contrariamente a tradurre l'intero algoritmo in una formula , funziona indipendentemente dalla semantica del codice sul lato "mettere insieme le cose" e consente di passare a un livello inferiore solo quando necessario, a partire da una vista "a volo d'aquila". Ogni affermazione può essere analizzata indipendentemente dal resto, portando a calcoli più chiari. Tuttavia, la tecnica si presta bene a un codice piuttosto dettagliato, non molto pseudo codice di livello superiore.

Il metodo

È abbastanza semplice in linea di principio:

  1. Assegna a ogni istruzione un nome / numero.
  2. Assegna a ogni istruzione un costo .SiCi
  3. Determina per ogni istruzione suo numero di esecuzioni .Siei
  4. Calcola i costi totali

    C=ieiCi .

È possibile inserire stime e / o quantità simboliche in qualsiasi momento, indebolendo risp. generalizzando il risultato di conseguenza.

Tenere presente che il passaggio 3 può essere arbitrariamente complesso. Di solito è lì che devi lavorare con stime (asintotiche) come " " per ottenere risultati.e77O(nlogn)

Esempio: ricerca della prima profondità

Considera il seguente algoritmo di attraversamento dei grafici:

dfs(G, s) do
  // assert G.nodes contains s
  visited = new Array[G.nodes.size]     1
  dfs_h(G, s, visited)                  2
end 

dfs_h(G, s, visited) do
  foo(s)                                3
  visited[s] = true                     4

  v = G.neighbours(s)                   5
  while ( v != nil ) do                 6
    if ( !visited[v] ) then             7
      dfs_h(G, v, visited)              8
    end
    v = v.next                          9
  end
end

Partiamo dal presupposto che il grafico (non indirizzato) sia dato dagli elenchi di adiacenza sui nodi . Sia il numero di spigoli.{0,,n1}m

Solo guardando l'algoritmo, vediamo che alcune istruzioni vengono eseguite ugualmente spesso come altre. Presentiamo alcuni segnaposto , e per i conteggi di esecuzione :ABCei

i123456789eiAABBBB+CCB1C

In particolare, poiché ogni chiamata ricorsiva nella riga 8 provoca una chiamata nella riga 3 (e una è causata dalla chiamata originale da ). Inoltre, perché la condizione deve essere verificata una volta per iterazione ma poi ancora una volta per lasciarla.e8=e31foodfse6=e5+e7while

È chiaro che . Ora, durante una prova di correttezza, mostreremo che viene eseguito esattamente una volta per nodo; cioè . Ma poi, passiamo in rassegna ogni elenco di adiacenza esattamente una volta e ogni fronte implica due voci in totale (una per ogni nodo incidente); otteniamo iterazioni in totale. Usando questo, deriviamo la seguente tabella:A=1fooB=nC=2m

i123456789ei11nnn2m+n2mn12m

Questo ci porta a costi totali di esattamente

C(n,m)=(C1+C2C8)+ n(C3+C4+C5+C6+C8)+ 2m(C6+C7+C9).

Istanziando valori adeguati per possiamo ricavare costi più concreti. Ad esempio, se vogliamo contare gli accessi alla memoria (per parola), utilizzeremoCi

i123456789Cin00110101

e prendi

Cmem(n,m)=3n+4m .

Ulteriori letture

Vedi in fondo all'altra mia risposta .


8

L'analisi dell'algoritmo, come la dimostrazione del teorema, è in gran parte un'arte (ad esempio ci sono programmi semplici (come il problema di Collatz ) che non sappiamo come analizzare). Possiamo convertire un problema di complessità dell'algoritmo in un problema matematico, come ha risposto in modo esauriente Raphael , ma per esprimere un limite al costo di un algoritmo in termini di funzioni note, siamo lasciati a:

  1. Usa tecniche che conosciamo da analisi esistenti, come trovare limiti basati su ricorrenze che comprendiamo o somma / integrali che possiamo calcolare.
  2. Cambia l'algoritmo in qualcosa che sappiamo analizzare.
  3. Vieni con un approccio completamente nuovo.

1
Immagino di non vedere come questo aggiunga qualcosa di utile e di nuovo, oltre alle altre risposte. Le tecniche sono già descritte in altre risposte. Questo mi sembra più un commento che una risposta alla domanda.
DW

1
Oserei dire che le altre risposte dimostrano che si tratta di non un'arte. Potresti non essere in grado di farlo (cioè la matematica), e un po 'di creatività (su come applicare la matematica nota) potrebbe essere necessaria anche se lo sei, ma questo è vero per qualsiasi compito. Presumo che non aspiriamo a creare nuova matematica qui. (In effetti, questa domanda o le sue risposte avevano lo scopo di demistificare l'intero processo.)
Raffaello

4
@Raphael Ari sta parlando di inventare una funzione riconoscibile come limite, piuttosto che "il numero di istruzioni eseguite dal programma" (che è ciò a cui la tua risposta si rivolge). Il caso generale è un'arte: non esiste un algoritmo che possa inventare un limite non banale per tutti gli algoritmi. Il caso comune, tuttavia, è un insieme di tecniche note (come il teorema principale).
Gilles,

@Gilles Se tutto ciò per cui non esiste un algoritmo fosse un'arte, gli artigiani (in particolare i programmatori) sarebbero pagati peggio.
Raffaello

1
@AriTrachlenberg sottolinea tuttavia un punto importante, ci sono una miriade di modi per valutare la complessità temporale di un algoritmo. Le stesse definizioni di notazione O suggeriscono o dichiarano direttamente la loro natura teorica a seconda dell'autore. Lo "scenario peggiore" lascia chiaramente spazio a congetture e / o nuovi fatti, tra cui qualsiasi N di persone nella stanza che stanno discutendo. Per non parlare della natura stessa delle stime asintotiche essendo qualcosa di ... ben poco inesatto.
Brian Ogden,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.