Corso di laurea in Matematica SAPIENZA Università di Roma Note del corso di Laboratorio di Programmazione e Calcolo: Analisi della stabilità e della complessità computazionale di un algoritmo Dipartimento di Matematica Guido Castelnuovo SAPIENZA Università di Roma
Indice Capitolo 1. Stabilità di un algoritmo 1 1.1. Risolviamo una equazione di secondo grado 1 1.2. Calcoliamo π 3 Capitolo 2. Costo computazionale di un algoritmo 7 2.1. Calcoliamo la potenza di un numero reale 7 2.2. Valutiamo un polinomio 7 1
CAPITOLO 1 Stabilità di un algoritmo Si dice algoritmo una sequenza definita di operazioni che operano su dati assegnati (x) e forniscono il risultato (y). Definita significa che dopo ogni operazione della sequenza è definita la operazione da eseguire subito dopo. Come esempio di algoritmo e di sequenza definita di operazioni si può pensare al calcolo delle radici di una equazione algebrica di secondo grado. Un problema computazionale può indurre un numero rilevante di algoritmi per la sua implementazione. Un esempio è quello della somma di n numeri. Si hanno tanti algoritmi quanti sono i modi di ordinare gli n addendi (n!) e si può notare che essi non danno tutti il medesimo risultato al calcolatore. Quando un algoritmo di calcolo è tale che un piccolo errore relativo sui dati o nei calcoli può portare ad un grande errore relativo sul risultato finale, si dice instabile (e viceversa diremo stabile un algoritmo che non amplifica mai nel risultato finale gli errori relativi sui dati o nei calcoli). 1.1. Risolviamo una equazione di secondo grado Il nostro primo problema computazionale consiste nel calcolare le radici dell equazione ax 2 + bx + c = 0, (1.1) dove a, b e c sono numeri reali e = b 2 4ac > 0. Come è noto, la (1.1) ammette due radici reali e distinte, che possono essere calcolate con la formula Se b è un numero reale tale che b ±. (1.2) 2a b a e b c, si avrà che = b 2 4ac b 2 ovvero b. Dalla formula (1.2) segue allora che, per calcolare una delle due radici reali di (1.1), dovremo sottrarre due numeri molto vicini fra loro. Dato che rappresentiamo i numeri reali nel calcolatore con una precisione finita (e dato che, nel 1
2 STABILITÀ DI UN ALGORITMO calcolare, avremo necessariamente introdotto un errore dovuto all operazione di radice quadrata), dobbiamo fare i conti con l errore relativo sugli addendi della somma algebrica: tale errore potrà venire enormemente amplificato dalla sottrazione b. Esempio. L equazione assegnata è x 2 2 k x + 1 = 0, (1.3) con k intero positivo. L equazione (1.3) ha due radici reali x 1 e x 2 che, per valori di k grandi, sono molto prossime rispettivamente a 2 k e a 1 2 k. In particolare, se k = 14, le radici esatte sono x 1 = 1.6383999938964844 10 4 e x 2 = 6.1035156477373675 10 5. D altro canto, se implementiamo su calcolatore in singola precisione la formula (1.2) per k = 14, quindi con a = 1, b = 2 14 = 16384, c = 1, solo la prima radice viene approssimata correttamente: la seconda radice diventa zero e su di essa perdiamo ogni informazione, cioè non ne conosciamo più neanche l ordine di grandezza. Il tipo di comportamento riscontrato nell esempio considerato è dovuto essenzialmente al fenomeno di amplificazione dell errore relativo nella sottrazione fra due numeri molto vicini (infatti è proprio la radice x 2 quella che si ottiene nella (1.2) con una sottrazione). Dato che l algoritmo per calcolare le radici di una equazione di secondo grado fornito dalle formule (1.2) è instabile, ovvero può in alcuni casi non dare la risposta corretta, abbiamo la necessità di costruire un nuovo algoritmo che risulti stabile, ovvero che non amplifichi mai gli errori relativi sui dati o nei calcoli. Osserviamo che, se nelle formule (1.2) moltiplichiamo numeratore e denominatore per b, otteniamo 2c b. (1.4) È poi anche facile constatare che la radice che si ottiene nelle formule (1.4) utilizzando il segno + è la stessa radice che si ottiene nelle formule (1.2) utilizzando il segno, e analogamente la radice che si ottiene nelle formule (1.4) utilizzando il segno è la stessa radice che si ottiene nelle formule (1.2) utilizzando il segno +; ciò significa quindi che, se usiamo le formule (1.4) per calcolare le radici dell equazione (1.3) del nostro esempio, otterremo un risultato opposto a quello precedente, cioè otterremo sempre buone approssimazioni della radice più piccola x 2, mentre (per k grande) otterremo cattive approssimazioni della radice più grande x 1 ; si tratta quindi ancora di un algoritmo instabile. 1.1.1. Un algoritmo stabile. La soluzione del nostro problema consisterà nell usare contemporaneamente le formule (1.2) e le formule (1.4) selezionando il segno + o il segno in modo che non si debba mai fare una sottrazione, in questo modo otterremo entrambe le radici senza pericolo di amplificazione
1.2 CALCOLIAMO π 3 dell errore relativo. Ecco quindi come sarà un algoritmo stabile per calcolare le radici dell equazione di secondo grado (1.1): calcola s = b 2 4ac; se b < 0, calcola le radici con le formule x 1 = b + s 2a, x 2 = 2c b + s ; se b 0, calcola le radici con le formule x 1 = 2c b s, x 2 = b s 2a. 1.2. Calcoliamo π Esistono diversi modi per approssimare il numero trascendente π, uno dei quali, noto fin dall antichità e attribuito ad Archimede, parte dall osservazione che esso coincide con il valore della lunghezza della semicirconferenza di raggio unitario. Un modo per approssimarlo consiste nel costruire due poligoni regolari con n lati, rispettivamente inscritto nel e circoscritto al cerchio di raggio unitario. I semiperimetri dei due poligoni saranno due approssimazioni rispettivamente per difetto e per eccesso della lunghezza della semicirconferenza. Al crescere del numero dei lati dei due poligoni (per esempio partendo da n = 4 e raddoppiando n ad ogni iterazione), queste approssimazioni diventeranno sempre più stringenti, permettendoci così, in teoria, di determinare il valore di π con una precisione sempre più grande. FIGURA 1.1
4 STABILITÀ DI UN ALGORITMO Per poter realizzare il nostro algoritmo, dobbiamo disporre di una formula per calcolare la lunghezza del lato di un poligono regolare (inscritto o circoscritto) con n lati. A questo scopo, osserviamo il disegno in Figura 1.1: se supponiamo che il segmento l = AC sia il lato del poligono regolare con n lati inscritto nel cerchio di raggio unitario, il segmento l = AB sarà allora il lato del poligono regolare con 2n lati inscritto nel cerchio di raggio unitario. Si dimostra che l = 2 4 l 2. (1.5) Infatti, applicando il teorema di Pitagora ai due triangoli ABD e ADO, vale BD = AB 2 AD 2 = l2 l 2 /4 BD = BO DO = BO AO 2 AD 2 = 1 1 l 2 /4, dunque l 2 l 2 /4 = 1 + (1 l 2 /4) 2 1 l 2 /4, ovvero l 2 = 2 4 l 2, da cui la (1.5) FIGURA 1.2 Se osserviamo la Figura 1.2, e se supponiamo che il segmento l = AC sia il lato del poligono regolare con n lati inscritto nel cerchio di raggio unitario, il segmento L = EF sarà allora il lato del poligono regolare con n lati circoscritto al cerchio di raggio unitario. Si dimostra che L = 2l 4 l 2. (1.6) Infatti, per la similitudine dei triangoli ACO e EFO, vale BO : DO = EF : AC, dunque BO AC EF = DO = l 1 l 2 /4,
1.2 CALCOLIAMO π 5 da cui segue la (1.6). Siamo ora in condizione di costruire l algoritmo per il calcolo di π. Indichiamo con l n il lato del poligono regolare con 2 n lati inscritto nel cerchio di raggio unitario. Dalla (1.5) segue che il lato l n+1 del poligono regolare con 2 n+1 lati inscritto nel cerchio di raggio unitario sarà fornito dalla formula l n+1 = 2 4 l 2 n. (1.7) Poiché il lato del quadrato inscritto nel cerchio di raggio unitario è dato da l 2 = 2, (1.8) le (1.7) e (1.8) permettono facilmente di costruire la successione {l n }. A questo punto basterà moltiplicare ciascun l n per 2 n 1 per ottenere il semiperimetro del poligono regolare con 2 n lati inscritto nel cerchio di raggio unitario. In, se indichiamo con L n il lato del poligono regolare con 2 n lati circoscritto al cerchio di raggio unitario, dalla (1.6) segue che L n = 2l n, (1.9) 4 l 2 n e moltiplicando ciascun L n per 2 n 1 si ottiene il semiperimetro del poligono regolare con 2 n lati circoscritto al cerchio di raggio unitario. Le (1.7), (1.8) e (1.9) consentono di costruire facilmente in modo ricorsivo le due successioni {l n } e {L n } che, al crescere di n, ci daranno approssimazioni per difetto e per eccesso di π sempre più stringenti. Purtroppo, un algoritmo di questo tipo non è stabile: se lo implementiamo, possiamo osservare che per i primi valori di n le due successioni {l n } e {L n } si avvicinano effettivamente al valore di π, ma da un certo punto in poi, al crescere di n i valori delle due successioni cominciano ad oscillare e ad allontanarsi da π. Quali sono le ragioni dell instabilità dell algoritmo? Se osserviamo la formula (1.7) (che è la formula base del nostro algoritmo), vediamo che all interno della radice quadrata più esterna, dobbiamo effettuare la sottrazione tra 2 e 4 l 2 n. Poiché ovviamente deve essere lim l n = 0, n si avrà di conseguenza che, per n grande risulterà 4 l 2 n 2 e quindi di nuovo l instabilità nasce dalla sottrazione di due numeri molto vicini fra loro, che provoca una cancellazione di cifre significative. 1.2.1. Un algoritmo stabile. È possibile ovviare a questo inconveniente e modificare il nostro algoritmo in modo da renderlo stabile? Per fortuna anche in questo caso la soluzione del problema è semplice (e assomiglia molto a quella per l algoritmo che calcola le radici di una equazione di secondo grado).
6 STABILITÀ DI UN ALGORITMO Moltiplichiamo e dividiamo la (1.7) per 2 + 4 l 2 n, ottenendo la formula alternativa l n+1 = 2 + l n 4 l 2 n. (1.10) Ora, la formula (1.10), insieme con le (1.8) e (1.9), ci permette di calcolare, senza rischio di cancellazione numerica, le successioni {l n } e {L n } e di approssimare π. Se implementiamo l algoritmo su calcolatore, possiamo osservare che le due successioni continueranno ad approssimare π sempre meglio al crescere di n. L algoritmo così modificato è un algoritmo stabile.
CAPITOLO 2 Costo computazionale di un algoritmo A parità di accuratezza del risultato e di memoria richiesta, tra due algoritmi stabili ha senso scegliere quello che richiede un minor numero di operazioni, ovvero quello che dà luogo a un minor tempo di calcolo (CPU time). 2.1. Calcoliamo la potenza di un numero reale Vogliamo calcolare x n, con n 0. Possiamo ricorrere ad un ciclo moltiplicativo. L algoritmo, che esegue n operazioni, è il seguente poni p = 1; per i = 1 : n calcola p = px; Alternativamente, con il risparmio di una operazione se vale n > 0, possiamo se n > 0 ricorrere al seguente algoritmo con un ciclo moltiplicativo più breve: poni p = x; per i = 1 : n 1 calcola p = px; OSSERVAZIONE 2.1. Esistono algoritmi molto più economici che per esempio tengono conto del fatto che x n può essere scritto come prodotto di fattori quali x 1, x 2, x 4, x 8,..., che sono semplici da costruire (x 2 = x 1 x 1, x 4 = x 2 x 2, x 8 = x 4 x 4,...) e appaiono quando nella rappresentazione binaria dell esponente c è 1 nella posizione corrispondente. Per esempio x 11 = x (8+2+1) = x 8 x 2 x perché il numero 11 corrisponde nel sistema binario a 1011, oppure x 17 = x (16+1) = x 16 x perché il numero 17 corrisponde nel sistema binario a 10001. Tuttavia in questo corso non faremo uso di tali algoritmi ottimizzati. Un polinomio a coefficienti in R, 2.2. Valutiamo un polinomio p n (x) = a n x n + a n 1 x n 1 + + a 0 7
8 COSTO COMPUTAZIONALE DI UN ALGORITMO può essere identificato con una funzione p : R R. Un problema computazionale è quindi quello di valutare p in un dato x R, cioè calcolare p(x). L algoritmo più semplice che esegue questa operazione consiste nel calcolare i termini del tipo a i x i e sommarli. poni s = a 0 ; per i = 1 : n poni p = x; se i > 1 per j = 1 : i 1 calcola calcola s = s + a i p; p = px; Serviranno 1 + 2 + + (n 2) + (n 1) = n(n 1) 2 operazioni solo per calcolare le potenze x 2, x 3,..., x n 1, x n, e ancora n prodotti e n somme. Dunque n(n+3) 2 operazioni. Tuttavia, se si parte dai termini di grado minore, quando si sta calcolando a i x i, per i > 1, al passo precedente si è calcolato a i 1 x i 1, e quindi si può ottenere x i = x i 1 x con una sola operazione. L algoritmo che tiene conto di ciò è il seguente poni s = a 0 ; poni p = x; per i = 1 : n calcola s = s + a i p; se i < n calcola p = px; Si osserva che l algoritmo esegue 3n 1 operazioni aritmetiche. Confrontando i due algoritmi: per n = 2 le operazioni aritmetiche sono in entrambi i casi 5, per n = 3 sono rispettivamente 9 e 8, per n = 4 sono rispettivamente 14 e 11, per n = 5 sono rispettivamente 20 e 14, ecc. 2.2.1. Un algoritmo efficiente. Con un piccolo trucco è possibile fare di meglio. Consideriamo dapprima il caso n = 3 e scriviamo equivalentemente p 3 (x) = (((a 3 x + a 2 )x + a 1 )x + a 0 ); si osserva che sono richieste due operazioni per ogni coppia di parentesi. In generale, ovvero per un polinomio di grado n qualsiasi, si può estendere la formula
2.2 VALUTIAMO UN POLINOMIO 9 precedente: p n (x) = a n x n + a n 1 x n 1 + + a 0 = (...((a n x + a n 1 )x + a n 2 )x + + a 1 )x + a 0 ). Scriviamo dunque l algoritmo nel modo seguente poni p = a n ; per i = n 1 : 1 : 0 calcola p = px + a i ; Si osserva che questa volta le operazioni richieste sono 2n, quindi il metodo è più conveniente. Questo algoritmo di valutazione, ovvero lo schema di Horner, è dunque più efficiente dal punto di vista computazionale. A titolo esemplificativo, volendo mettere in evidenza lo scarto rispetto agli algoritmi precedenti, per n = 2 le operazioni aritmetiche sono 4, per n = 3 sono 6, per n = 4 sono 8, per n = 5 sono 10, ecc.