Algoritmica (Anno Accademico 2004-05): note di sintesi sulla prima
Transcript
Algoritmica (Anno Accademico 2004-05): note di sintesi sulla prima
Algoritmica (Anno Accademico 2004-05): note di sintesi sulla prima parte del corso March 5, 2005 • Nel corso consideriamo algoritmi progettati per un esecutore ideale, o modello di calcolo che astrae le caratteristiche essenziali di un calcolatore. Tale modello è in grado di eseguire un certo insieme di operazioni elementari nonché operazioni di lettura e scrittura. Gli algoritmi verranno scritti in uno pseudocodice (facilmente traducibile in un linguaggio di programmazione ad alto livello) il quale comprenderà ovviamente la possibilità di indicare le operazioni elementari eseguibili dall’automa. • L’esecuzione di ogni singola operazione elementare richiede un certo tempo nel modello. A seconda delle situazioni, si assume che tale tempo sia costante oppure proporzionale alla lunghezza (in bit) degli operandi. Nel primo caso abbiamo il cosiddetto criterio di costo uniforme, nel secondo il criterio di costo logaritmico. Il secondo può essere grossolanamente sbagliato nel caso in cui la lunghezza degli operandi cresca a dismisura, perché nessun calcolatore reale (e dunque nessun modello realistico) può trattare in tempo costante operandi arbitrariamente lunghi. • Siamo certamente interessati a determinare la correttezza degli algoritmi rispetto al problema da risolvere. Siamo però soprattutto interessati a determinare il costo complessivo di esecuzione degli algoritmi su questo modello. Il costo sarà principalmente il tempo di esecuzione (come “somma” dei tempi delle operazioni elementari), ma anche lo spazio di memoria utilizzato dall’algoritmo. Ovviamente, perché il tutto non rimanga uno sterile esercizio teorico, le indicazioni dovranno essere utili per valutare nella pratica il comportamento di programmi 1 “veri” che realizzano tali algoritmi. Questo sarà uno degli obiettivi delle esercitazioni di laboratorio. • In funzione di che cosa valutiamo il costo di esecuzione? La risposta ovvia è che il costo sarà funzione dell’input su cui l’algoritmo lavora. Questo è vero ma, in generale, troppo complesso. Per motivi di trattabilità analitica (ma non solo) è preferibile utilizzare una misura più compatta, e questa è la dimensione dell’input. Si tratta ovviamente di capire che cosa sia la dimensione. • Ci sono due risposte possibili. La prima, sempre corretta, è che la dimensione è il numero totale di bit necessari per rappresentare l’input stesso. Questa misura si accoppia al criterio di costo logaritmico per le operazioni, ed è la sola corretta nel caso di problemi (come il calcolo dei numeri di Fibonacci ) che possono coinvolgere numeri arbitrariamente grandi. In altri casi, misurare il numero totale di bit introduce inutili complicazioni. • La seconda possibile risposta è che la dimensione dell’input conta il numero di “oggetti” (ad esempio, numeri di lunghezza limitata, caratteri o anche “brevi” sequenze di caratteri) che compongono l’input e che sono singolarmente manipolabili in tempo costante su un modello realistico di calcolatore. Questa scelta, che va di pari passo con l’assunzione di costo uniforme per le operazioni elementari, è risultata quella giusta per gli algoritmi di ricerca sequenziale e binaria. • Dunque misuriamo il costo di esecuzione, spazio e/o tempo, in funzione di un singolo numero che è la dimensione dell’input. Matematicamente il tempo TA di esecuzione di un algoritmo A è una funzione definita sugli interi positivi: TA = TA (n). Analoga definizione si può dare per lo spazio. • La definizione data crea però seri problemi. Infatti, molti algoritmi che vedremo durante il corso, hanno comportamenti anche molto diversi pur in presenza di input della stessa dimensione. Un esempio è ancora costituito dall’algoritmo di ricerca sequenziale (ma anche quello di ricerca binaria, anche se in modo meno pronunciato), che in alcuni casi può “trovare” subito la soluzione, mentre in altri deve esaminare tutta la sequenza. Dunque, a rigore, quella che abbiamo definito non è neppure una funzione. 2 • Per ovviare all’inconveniente, si considerano due alternative di estrema utilità pratica. Ragioniamo sul tempo di esecuzione (per lo spazio valgono le stesse considerazioni). Fissato un valore n della dimensione, si misura il tempo di esecuzione di un algoritmo A come il tempo impiegato da A nel caso più sfavorevole (per input di quella dimensione) oppure nel caso medio. In inglese si usano i termini worst-case e average-case analysis. • Il caso più sfavorevole garantisce che nessun input potrà avere costi superiori, mentre il caso medio fornisce indicazioni sul comportamento “tipico”. Il caso più sfavorevole è solitamente più semplice da analizzare. Si tratta di studiare le caratteristiche dell’input che costringono l’interprete a lavorare più a lungo (si pensi alla ricerca sequenziale, in cui l’analisi worst-case è immediata). Il caso medio, oltre ad essere matematicamente più difficile, richiede la conoscenza della distribuzione di probabilità degli input di una stessa dimensione, che non sempre si ha. A volte si può sostituire la probabilità con la frequenza osservata su periodi più o meno lunghi, ottenendo risultati approssimati. • Una volta stabilito il modello, il criterio di costo adottato per le operazioni elementari e la metodologia di analisi (worst- o average-case), il calcolo del tempo di esecuzione di un algoritmo procede analizzando matematicamente l’algoritmo stesso “sommando” i costi delle singole operazioni elementari. Questo spesso richiede la conoscenza di tecniche di analisi e di calcolo combinatorio. • Nel calcolo del costo, cioè della forma analitica della funzione “tempo di esecuzione” o “spazio di memoria”, ci concentriamo sulla determinazione del termine dominante, spesso trascurando anche le costanti moltiplicative del termine dominante. Ad esempio, anziché arrivare alla conclusione (poniamo) che T (n) = 21 n2 + 8n + 2 log n + 5, ci basterà stabilire che T (n) = O(n2 ). Questa semplificazione (che va sotto il nome di analisi asintotica) può sembrare grossolana. Ci sono però buone ragioni per farla, a parte la maggiore semplicità. Matematicamente, possiamo usare due argomenti distinti, che si applicano rispettivamente ai termini di ordine inferiore e alle costanti moltiplicative. • La ragione per cui si trascurano i termini di ordine inferiore (la parte 3 8n+2 log n+5 nell’esempio del punto precedente) è che, al crescere della dimensione n, essi incidono percentualmente sempre meno sul valore totale. Sempre con riferimento all’esempio precedente, se n = 10, il contributo dei termini inferiori sul totale è circa il 65%, ed è ancora relativamente elevato (circa 14%) per n = 100, ma scende sotto al 2% per n = 1000 ed è di fatto trascurabile per n ≥ 10000. • Il motivo per cui spesso ignoriamo la costante moltiplicativa del termine dominante dipende invece da un’altra osservazione. Immaginiamo che il numero di operazioni eseguite da un certo algoritmo sia stato calcolato analiticamente e risulti T (n) = cn2 , dove c è una costante, più un eventuale contributo di ordine inferiore che abbiamo già provveduto a trascurare (per le ragioni esposte nel punto precedente). Il tempo di esecuzione “vero e proprio”, che indicheremo con t(n), dell’algoritmo su una particolare piattaforma potrà essere quindi approssimato mediante la formula t(n) = vcn2 (1) dove v è il tempo di esecuzione di una singola operazione1 . Il valore , dove t(n̄) di v può essere approssimato mediante la formula v ≈ t(n̄) cn̄2 è il tempo, misurato sperimentalmente, dell’esecuzione dell’algoritmo su input di dimensione fissata n̄ (o eventualmente calcolato mediando i risultati di più esecuzioni). Disponendo di c e v possiamo ovviamente stimare il tempo di esecuzione su input di dimensione n qualsiasi mediante la formula (1). L’osservazione cruciale è che, nella (1), v e c compaiono come prodotto. Questo giustifica un approccio in cui, analiticamente, si ignora la costante moltiplicativa c. In questo caso, il tempo di esecuzione stimato delle singole operazioni risulterà v 0 ≈ t(n̄) ; n̄2 0 tuttavia, t(n̄) è lo stesso di prima e questo implica che v = cv. La costante moltiplicativa trascurata analiticamente viene quindi “scaricata” sul tempo di esecuzione delle singole operazioni. In altri termini, sembrerà semplicemente di avere un processore più veloce o più lento (a seconda che risulti c < 1 o c > 1), senza che venga inficiata la capacità predittiva del modello. 1 Questa è una semplificazione, che sarà tanto più vicina alla realtà quanto più le operazioni che contribuiscono al termine dominante sono fra loro “omogenee” (ad esempio, tutte addizioni e sottrazioni oppure moltiplicazioni e divisioni, oppure tutti confronti fra chiavi della stessa lunghezza). 4 • L’input per un algoritmo descrive una partcolare istanza di un problema. Ad esempio, un elenco di chiavi e una chiave descrivono un’istanza del problema generale “ricerca in una lista di chiavi” che l’elgoritmo di ricerca sequenziale è chiamato a risolvere nella sua generalità (cioè per ogni particolare input/istanza). In questo corso vedremo molti altri problemi computazionali, come l’ordinamento di un insieme di chiavi o il calcolo del percorso di lunghezza minima su una mappa. Molti dei problemi che analizzeremo hanno rilevanza applicativa diretta; tuttavia, ogni problema studiato compare come elemento costitutivo di un ben più grande numero di problemi applicativi di grande rilevanza scientifica e/o economica, e questo giustifica il loro studio. • La nozione di complessità computazionale (o semplicemente complessità) di un problema si riferisce alla quantità di risorse di calcolo necessaria e sufficiente alla sua risoluzione rispetto ad un certo modello di calcolo. Ci riferiamo qui ancora al tempo, ricordando che definizioni analoghe si applicano allo spazio. • Per molti problemi la complessità non è nota. Sono però in generale note limitazioni superiori ed inferiori alla complessità. Anche nel caso delle limitazioni, come già per il costo degli algoritmi, utilizziamo la notazione asintotica. Esprimiamo cioè tali limitazioni mediante le notazioni O, Ω e Θ. • Il tempo di esecuzione di un algoritmo A che risolve un problema P fornisce una limitazione superiore alla complessità di P. Ad esempio, un algoritmo di costo O(n2 ) per P stabilisce anche che la complessità di quest’ultimo è al più quadratica. • La determinazione di limitazioni inferiori “non banali” comporta in generale enormi difficoltà. Dimostrare una limitazione inferiore significa infatti far vedere, con argomentazioni logico/matematiche rigorose, che nessun algoritmo potrà mai fare meglio di quanto prescritto dalla limitazione. • Una limitazione inferiore banale, nel caso di algorimi per il modello della macchina a registri (che astrae le caratteristiche di un computer mono-processore) è Ω(n). Infatti, se l’input deve essere letto tutto è evidente che non si può pensare di risolvere il problema in tempo 5 sublineare. Ad esempio, nel caso della ricerca di una chiave in un insieme non ordinato, è facile rendersi conto che qualunque algoritmo deve comunque esaminare tutte le chiavi nel caso più sfavorevole (ad esempio, quando la chiave non è presente). • La determinazione di limitazioni inferiori superlineari, ad esempio Ω(n2 ), si è rivelata un compito di difficoltà formidabile per moltissimi problemi di natura combinatoriale che pure, nella pratica, sembrano richiedere solo algoritmi onerosi dal punto di vista del tempo di calcolo. Addirittura, per una certa classe di problemi molto importanti, che gli studenti di Scienze dell’Informazione vedranno in un insegnamento successivo, la determinazione della complessità è annoverata fra uno dei dieci problemi irrisolti più importanti della matematica moderna. 6