Come descrivere algoritmi, dimostrarli e analizzarli?


20

Prima di leggere The Art of Computer Programming (TAOCP) , non ho esaminato a fondo queste domande. Userei lo pseudo codice per descrivere gli algoritmi, comprenderli e stimare il tempo di esecuzione solo sugli ordini di crescita. Il TAOCP mi cambia completamente idea.

TAOCP usa l'inglese mescolato con passaggi e goto per descrivere l'algoritmo e usa i diagrammi di flusso per rappresentare l'algoritmo più facilmente. Sembra di basso livello, ma trovo che ci siano alcuni vantaggi, soprattutto con il diagramma di flusso, che ho ignorato molto. Possiamo etichettare ciascuna delle frecce con un'affermazione sullo stato attuale delle cose nel momento in cui il calcolo attraversa quella freccia e fare una prova induttiva per l'algoritmo. L'autore dice:

È la tesi dell'autore che capiamo veramente perché un algoritmo è valido solo quando arriviamo al punto in cui le nostre menti hanno implicitamente riempito tutte le asserzioni, come è stato fatto in Fig.4.

Non ho sperimentato queste cose. Un altro vantaggio è che possiamo contare il numero di volte che viene eseguito ogni passaggio. È facile verificare con la prima legge di Kirchhoff. Non ho analizzato esattamente il tempo di esecuzione, quindi alcuni potrebbero essere stati omessi durante la stima del tempo di esecuzione.±1

L'analisi degli ordini di crescita è talvolta inutile. Ad esempio, non possiamo distinguere quicksort da heapsort perché sono tutti , dove è il numero atteso della variabile casuale , quindi dovremmo analizzare la costante, diciamo, ed , quindi possiamo confrontare e meglio. Inoltre, a volte dovremmo confrontare altre quantità, come le varianze. Solo un'analisi approssimativa degli ordini di crescita del tempo di esecuzione non è sufficiente. Come TAOCPE(T(n))=Θ(nlogn)EXXE(T1(n))=A1nlgn+B1n+O(logn)E(T2(n))=A2lgn+B2n+O(logn)T1T2 traduce gli algoritmi in linguaggio assembly e calcola il tempo di esecuzione, è troppo difficile per me, quindi voglio conoscere alcune tecniche per analizzare il tempo di esecuzione un po 'più approssimativamente, il che è utile anche per linguaggi di livello superiore come C, C ++ o pseudo codici.

E voglio sapere quale stile di descrizione viene utilizzato principalmente nei lavori di ricerca e come trattare questi problemi.


6
Dovresti stare molto attento quando confronti i tempi di esecuzione degli algoritmi così da vicino. I computer reali hanno cache, registri e pipeline, che possono cambiare drasticamente il tempo di esecuzione. Se vuoi scoprire quale algoritmo è effettivamente più veloce, devi effettivamente eseguirlo su un computer.
svick,

1
In realtà, analizzare un assemblatore come Knuth utilizza è molto più semplice dell'analisi del codice della vita reale perché nulla è nascosto e il flusso di controllo è facile. Stai chiedendo pratica; Penso che si applichi il commento di Dave . I professionisti hanno maggiori probabilità di progettare i propri algoritmi utilizzando misure di runtime piuttosto che effettuare analisi rigorose. Ma poi, non sono un praticante, quindi prendi quello che dico con un granello di sale.
Raffaello

1
@Raphael My in pratica significa che nella pratica dei lavori di ricerca , non della programmazione .
Yai0Phah,

@Frank, cosa intendi per varianza ? I miei test delle prestazioni mi danno variazioni temporali.
edA-qa mort-ora-y

@Raphael, il tuo primo punto non è più vero. I chip moderni riordinano il tuo assemblaggio, memorizzano / caricano fuori servizio e eseguono e caricano in modo predittivo. Per la concorrenza e le questioni precedenti, è necessaria un'analisi approfondita, ma non lo faccio in una forma formale.
edA-qa mort-ora-y

Risposte:


18

Esiste un'enorme varietà di approcci fattibili. Da quale dipende il più adatto

  • cosa stai cercando di mostrare,
  • quanti dettagli vuoi o hai bisogno.

Se l'algoritmo è ampiamente noto e utilizzato come subroutine, spesso si rimane a un livello superiore. Se l'algoritmo è l'oggetto principale oggetto di indagine, probabilmente vorrai essere più dettagliato. Lo stesso si può dire per le analisi: se è necessario un limite di runtime superiore approssimativo, si procede in modo diverso da quando si desidera un conteggio preciso delle istruzioni.

Vi darò tre esempi per il noto algoritmo Mergesort che, si spera, possa illustrarlo.

Alto livello

L'algoritmo Mergesort prende un elenco, lo divide in due (circa) parti ugualmente lunghe, ricorre su quegli elenchi parziali e unisce i risultati (ordinati) in modo che il risultato finale sia ordinato. Sugli elenchi singleton o vuoti, restituisce l'input.

Questo algoritmo è ovviamente un algoritmo di ordinamento corretto. La suddivisione dell'elenco e la sua fusione possono essere implementate ciascuna nel tempo , il che ci dà una ricorrenza per il peggior tempo di esecuzione T ( n ) = 2 T ( nΘ(n). Secondo il teorema del Maestro, questo valutaT(n)Θ(nlogn).T(n)=2T(n2)+Θ(n)T(n)Θ(nlogn)

Livello medio

L'algoritmo Mergesort è dato dal seguente pseudo-codice:

procedure mergesort(l : List) {
  if ( l.length < 2 ) {
    return l
  }

  left  = mergesort(l.take(l.length / 2)
  right = mergesort(l.drop(l.length / 2)
  result = []

  while ( left.length > 0 || right.length > 0 ) {
    if ( right.length == 0 || (left.length > 0 && left.head <= right.head) ) {
      result = left.head :: result
      left = left.tail
    }
    else {
      result = right.head :: result
      right = right.tail
    }
  }

  return result.reverse
}

Dimostriamo correttezza per induzione. Per elenchi di lunghezza zero o uno, l'algoritmo è banalmente corretto. Come ipotesi di induzione, supponiamo che mergesortfunzioni correttamente su liste di lunghezza al massimo per alcuni n > 1 arbitrari, ma fissi . Ora lascia che L sia un elenco di lunghezza n + 1 . Per ipotesi di induzione, e tenere versioni (non decrescenti) ordinate del primo resp. seconda metà di L dopo le chiamate ricorsive. Pertanto, il ciclo seleziona in ogni iterazione l'elemento più piccolo non ancora studiato e lo aggiunge a ; quindi è un elenco non sempre più ordinato che contiene tutti gli elementi dinn>1Ln+1leftrightLwhileresultresultlefte right. Il contrario è una versione non decrescente di , che è il risultato restituito - e desiderato -.L

Per quanto riguarda il runtime, contiamo i confronti degli elementi e le operazioni di elenco (che dominano il runtime in modo asintotico). Elenchi di lunghezza inferiore a due non causano nessuno. Per elenchi di lunghezza , abbiamo quelle operazioni causate dalla preparazione degli input per le chiamate ricorsive, quelle dalle chiamate ricorsive stesse più il loop e uno . Entrambi i parametri ricorsivi possono essere calcolati con al massimo n operazioni di lista ciascuna. Il ciclo viene eseguito esattamente n volte e ogni iterazione provoca al massimo un confronto tra elementi e esattamente due operazioni di elenco. Il finale può essere implementato per utilizzare 2 nn>1whilereversenwhilenreverse2noperazioni dell'elenco: ogni elemento viene rimosso dall'input e inserito nell'elenco di output. Pertanto, il conteggio delle operazioni soddisfa la seguente ricorrenza:

T(0)=T(1)=0T(n)T(n2)+T(n2)+7n

Poiché è chiaramente non decrescente, è sufficiente considerare n = 2 k per la crescita asintotica. In questo caso , la ricorrenza si semplificaTn=2k

T(0)=T(1)=0T(n)2T(n2)+7n

Con il teorema del Maestro, otteniamo che si estende al tempo di esecuzione di .TΘ(nlogn)mergesort

Livello ultra basso

Considera questa implementazione (generalizzata) di Mergesort in Isabelle / HOL :

types dataset  =  "nat * string"

fun leq :: "dataset \<Rightarrow> dataset \<Rightarrow> bool" where
   "leq (kx::nat, dx) (ky, dy) = (kx \<le> ky)"

fun merge :: "dataset list \<Rightarrow> dataset list \<Rightarrow> dataset list" where
"merge [] b = b" |
"merge a [] = a" |
"merge (a # as) (b # bs) = (if leq a b then a # merge as (b # bs) else b # merge (a # as) bs)"

function (sequential) msort :: "dataset list \<Rightarrow> dataset list" where
  "msort []  = []" |
  "msort [x] = [x]" |
  "msort l   = (let mid = length l div 2 in merge (msort (take mid l)) (msort (drop mid l)))"
by pat_completeness auto
  termination
  apply (relation "measure length")
by simp+

Ciò include già prove di correttezza e risoluzione. Trova una (quasi) completa prova di correttezza qui .

Per il "runtime", ovvero il numero di confronti, è possibile impostare una ricorrenza simile a quella nella sezione precedente. Invece di usare il teorema del Maestro e dimenticare le costanti, puoi anche analizzarlo per ottenere un'approssimazione asintoticamente uguale alla quantità reale. Puoi trovare l'analisi completa in [1]; ecco un profilo approssimativo (non si adatta necessariamente al codice Isabelle / HOL):

Come sopra, la ricorrenza per il numero di confronti è

f0=f1=0fn=fn2+fn2+en

enn

{f2m=2fm+e2mf2m+1=fm+fm+1+e2m+1

fnen

k=1n1(nk)Δfk=fnnf1

Δfk

W(s)=k1Δfkks=112sk1Δekks=: (s)

che insieme alla formula di Perron ci porta a

fn=nf1+n2πi3i3+i(s)ns(12s)s(s+1)ds

(s)

fnnlog2(n)+nA(log2(n))+1

A[1,0.9]


  1. Trasformazioni e asintotiche di Mellin: la fusione di ricorrenza di Flajolet e Golin (1992)
  2. en=n2
    en=n1
    en=nn2n2+1n2n2+1

αβT(n)=T(n/2)+T(n/2)+αn+β

@Frank: la risposta breve è Non puoi ; le costanti dipendono dai dettagli di implementazione - tra cui l'architettura della macchina, il linguaggio e il compilatore - che sono irrilevanti per l'algoritmo sottostante.
JeffE,

αβ

@JeffE ad esempio, MIX / MMIX in taocp lo è, ma è troppo difficile tradurre un algoritmo in un tale linguaggio macchina.
Yai0Phah,

@FrankScience: per avvicinarti alla pratica, dovresti contare tutte le operazioni (come fa Knuth). Quindi, è possibile creare un'istanza del risultato con costi operativi specifici della macchina per ottenere un runtime reale (ignorando gli effetti che potrebbe avere l'ordine delle operazioni, memorizzazione nella cache, pipeline, ...). Di solito, le persone contano solo alcune operazioni, e in quel caso correggonoα e βnon ti dice molto.
Raffaello

3

"Una disciplina di programmazione" di Dijkstra è incentrato sull'analisi e la dimostrazione di algoritmi e la progettazione per la provabilità. Nella prefazione di quel libro, Dijkstra spiega come un mini-linguaggio costruito molto semplice che sia adeguatamente progettato per essere analizzato sia sufficiente a spiegare formalmente molti algoritmi:

All'inizio di un libro come questo, ci si pone immediatamente la domanda: "Quale linguaggio di programmazione userò?", E questo non èuna semplice domanda di presentazione! Un aspetto più importante, ma anche più sfuggente, di qualsiasi strumento è la sua influenza sulle abitudini di coloro che si allenano nel suo uso. Se lo strumento è un linguaggio di programmazione, questa influenza è - che ci piaccia o no - un'influenza sulle nostre abitudini di pensiero. Avendo analizzato quell'influenza al meglio delle mie conoscenze, ero giunto alla conclusione che nessuno dei linguaggi di programmazione esistenti, né un sottoinsieme di essi, sarebbe stato adatto al mio scopo; d'altra parte mi conoscevo così poco già per la progettazione di un nuovo linguaggio di programmazione che avevo promesso di non farlo per i successivi cinque anni, e avevo la netta sensazione che quel periodo non fosse ancora trascorso! (Prima di questo, tra molte altre cose, questa monografia doveva essere scritta.

Più tardi spiega quanto è riuscito a ottenere il suo mini-linguaggio.

Devo al lettore una spiegazione del perché ho tenuto il mini-linguaggio così piccolo da non contenere nemmeno procedure e ricorsioni. ... Il punto è che non ne ho sentito il bisogno per trasmettere il mio messaggio, vale a dire. come una scelta accurata delle preoccupazioni sia essenziale per la progettazione di programmi di alta qualità sotto tutti gli aspetti; gli strumenti modesti del mini-linguaggio ci davano già una latitudine più che sufficiente per progetti non banali, ma molto soddisfacenti.

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.