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=0n−2∑j=0n−i−21=⋯=n(n−1)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,ji
j
Ci,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=0n−2∑j=0n−i−2C5,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))+{10,A(i,j)[j]>A(i,j)[j+1],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.
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=0n−2∑j=0n−i−21=n(n−1)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
Il caso migliore
Al contrario, esiste un limite inferiore banale:
Cswaps(A)≥∑i=0n−2∑j=0n−i−20=0 .
Questo può accadere anche: su un array già ordinato, Bubblesort non esegue un singolo scambio.
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!∑A∑i=0n−2∑j=0n−i−2C5,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.
- Si noti che l'algoritmo è stato accuratamente formulato in modo che
i = n-1
non venga eseguita "l'ultima" iterazione del ciclo esterno che non fa mai nulla.
- " " è la notazione matematica per "valore atteso", che qui è solo la media.E
- 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 E
del 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 ec∘E1E2
- la valutazione delle espressioni può cambiare lo stato in molte lingue,
quindi potresti dover essere flessibile con questa regola.
Sequenza
Dato un programma P
come sequenza di programmi Q;R
, si aggiungono i costi a
CP(ψ)=CQ(ψ)+CR(ψ/Q) .
Condizionali
Dato un programma P
del modulo if A then Q else R end
, i costi dipendono dallo stato:
CP(ψ)=CA(ψ)+{CQ(ψ/A)CR(ψ/A),A evaluates to true under ψ,else
In generale, la valutazione A
può benissimo cambiare lo stato, quindi l'aggiornamento per i costi delle singole filiali.
Per-Loops
Dato un programma P
del 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 , ..., .ψiQ
xi
x
x1
xi-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
xi
può 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 P
del modulo while A do Q end
, assegnare i costi
CP(ψ) =CA(ψ)+{0CQ(ψ/A)+CP(ψ/A;Q),A evaluates to false under ψ, 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<+{0c+=+c/+C1,4({i:=i0+1;x:=⌊x0/2⌋}),x0≤0, 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!c…i
x
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>c>+c+=+c/+C1,4(⌊x/2⌋),x≤0, 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 P
del modulo M(x)
per alcuni parametri in x
cui 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 M
ottenere un nuovo stato locale, inizializzato impostando il valore di p
to x
. Inoltre, x
può 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})C4({n:=n0}),n0≤1, else=c≤+{creturncreturn+c∗+ccall+Cfac({n:=n0−1}),n0≤1, else
Si noti che ignoriamo lo stato globale, poiché fac
chiaramente 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.
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.
- Eseguire l'analisi utilizzando i conteggi di esecuzione di questa operazione come costo.
- 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 all'analisi degli algoritmi che usano tecniche simili a questa.