Riduzione della complessità degli algoritmi In generale con il termine complessità computazionale si intende il numero complessivo di operazioni aritmetiche necessarie per la implementazione di un algoritmo. Questo numero è, in prima approssimazione, proporzionale al tempo di esecuzione al calcolatore. In effetti una volta implementato l’algoritmo vanno tenuti presenti anche i tempi necessari per le varie scelte logiche che possono dare un contributo anche determinante sul tempo complessivo di elaborazione. Quindi quando si parla di complessità di un algoritmo si intende anche il numero delle variabili create, che corrisponde alla quantit à di memoria utilizzata nell’esecuzione del programma. Spesso ci sono situazioni in cui guardando certe proprietà del problema numerico da risolvere, si possono trovare degli accorgimenti che risultano avere un impatto non trascurabile sul costo computazionale dell’algoritmo di calcolo. Vediamo di seguito un paio di esempi di questo tipo. 1 Prodotto Matrice-Vettore Supponiamo che sia necessario compiere il prodotto matrice-vettore A ∗ b, in cui la matrice A, di dimensione n × n, è un campionamento della funzione f (x, y) = cos(x) ∗ cos(y) in una griglia cartesiana di punti nell’intervallo [0, pi] × [0, pi] , e b è un vettore colonna di lunghezza n . La matrice A può essere espressa nella forma A = vx0 ∗ vy dove • vx(i) = cos(x(i)), ottenuto scegliendo un vettore riga di coordinate x avente n elementi nell’intervallo [0, pi], • vy(i) = cos(y(i)), ottenuto scegliendo un vettore riga di coordinate y avente n elementi nell’intervallo [0, pi]. Per calcolare il prodotto A ∗ b, essendo A una matrice densa e b un generico vettore, si avrà un costo computazionale dell’ordine di O(n2 ). 1 Riduzione della complessità degli algoritmi Se, però, utilizziamo l’espressione con cui viene costruita A ed invertiamo la precedenza nei calcoli, avremo A ∗ b = vx0 ∗ (vy ∗ b) in questo caso la complessità si è all’ordine di O(n) perchè si presenta un prodotto tra un vettore e uno scalare, in quanto il prodotto (vy ∗ b) (vettore riga per vettore colonna) fornisce come risultato uno scalare. Esercizio: costruire un programma Matlab/Octave che esegua il prodotto matricevettore A · b nelle due modalità qui prospettate e si verifichi automaticamente che il risultato del prodotto nei due casi è il medesimo. Inoltre, verificare sperimentalmente, per alcuni valori di n, se l’andamento dei tempi di esecuzione segue l’andamento della complessità prospettato e costuire un grafico (n, tempi). 2 Potenze di matrici Per calcolare la potenza p-esima di una matrice quadrata A di ordine n cio è Ap := A . . ∗ A} | ∗ .{z p volte senza usare l’operatore di elevamento a potenza seguente algoritmo (pseudocodice) ∧ , si può implementare il Pa=A; for i=1:p-1 Pa=Pa*A; end Alternativamente (in maniera più stabile ed efficiente) si può decomporre p come M X p= ci 2i i=0 dove M = blog2 pc e ci = 0 oppure ci = 1. Si osserva facilmente che questa non è altro che la classica rappresentazione di p in base 2. Usando la propriet à della potenze M Y PM i i ci B = Ap = A i=0 ci 2 = A2 i=0 i i−1 i−1 ove ogni termine A2 può essere calcolato come A2 · A2 . Confrontiamo i due metodi per p = 6. Nel primo si calcola A 6 come A6 = A ∗ A ∗ A ∗ A ∗ A ∗ A 2 Riduzione della complessità degli algoritmi e quindi sono necessari 5 prodotti tra matrici. Nel secondo caso essendo 6 = 0 ∗ 20 + 1 ∗ 21 + 1 ∗ 22 si ha A6 = 2 Y A2 i ci 1 0 2 = (A2 )0 ∗ (A2 )1 ∗ (A2 )1 = I ∗ (A2 ) ∗ (A4 ). i=0 Calcolati A = A ∗ A ed in seguito A4 = (A2 ) ∗ (A2 ), abbiamo finalmente A6 con solo 3 prodotti tra matrici ma con lo storage addizionale di alcune matrici in memoria. Sia per esempio A è una matrice di ordine 7 e p = 22. Per poter utilizzare il secondo metodo bisogna trasformare p in binario e memorizzarlo nel vettore c. Il valore assunto da c è in questo caso [0 1 1 0 1] poiché, come si legge dalla rappresentazione binaria (da sinistra verso destra), 2 22 = 0 · 20 + 1 · 21 + 1 · 22 + 0 · 23 + 1 · 24 . Notiamo che il vettore c ha lunghezza M + 1 = floor(log2(22)) + 1 = 5. Vogliamo quindi calcolare B = (A)0 ∗ (A2 )1 ∗ (A4 )1 ∗ (A8 )0 ∗ (A16 )1 . Un esempio di struttura del codice può essere la seguente p=22; n=7; c=binario(p); A=rand(n); B=eye(n); C=A; M=floor(log2(p)); for index=0:M j=index+1; if c(j) == 1 B=B*C; end C=C*C; end Ad ogni passo la matrice B contiene la potenza di A calcolata fino a quel moi i−1 mento, mentre la matrice C contiene la potenza di A2 ottenuta come A2 ∗ i−1 A2 . Alla fine del processo il risultato Ap sarà memorizzato in B. Esercizio: costruire un programma Matlab/Octave che, data una generica matrice A ed assegnato un valore di q, esegua il calcolo di A q nelle due modalità qui esposte e verifichi automaticamente che il risultato nei due casi è il medesimo. Inoltre, valutare dal programma qual’è la complessità dei due algoritmi e verificare sperimentalmente, con una matrice A a scelta e valori di q = 4, 5, ...15 se l’andamento dei tempi di esecuzione segue l’andamento della complessit à atteso. Costuire un grafico (n, tempi). 3