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