Parte 9. Cenni della Teoria della Complessità Computazionale

Slides:



Advertisements
Presentazioni simili
Elementi di Informatica di base
Advertisements

Complessità Computazionale
Informatica 3 V anno.
2a + 10b abx2 3a + 1 y 2 a + 1 x + 2y a − Espressioni algebriche
I Polinomi Prof.ssa A.Comis.
Huffman Canonico: approfondimento. Come abbiamo visto, Huffman canonico ci permette di ottenere una decompressione più veloce e con un uso più efficiente.
Indici di Posizione Giulio Vidotto Raffaele Cioffi.
1 Prof.ssa A.Comis. 2 Introduzione Definizione Classificazione Principi di equivalenza Regole per la risoluzione.
POTENZE
Le Frazioni Prof.ssa A.Comis.
© 2007 SEI-Società Editrice Internazionale, Apogeo
Cosa nascondono i numeri naturali? N Loretta Ferrante.
SUMMERMATHCAMP TARVISIO, AGOSTO 2017
= 2x – 3 x Definizione e caratteristiche
Insiemi di numeri e insiemi di punti
Giovanni Finaldi Russo Pietro Bruno
Algoritmi Avanzati a.a.2014/2015 Prof.ssa Rossella Petreschi
x2 – 4x + 1 x – 3 6x 5y2 ; x2 – 4x + 1 x – 3 x – 3 ≠ 0 x ≠ 3
Definizione di logaritmo
Algoritmi Avanzati a.a.2015/2016 Prof.ssa Rossella Petreschi
L’integrale indefinito
La circonferenza nel piano cartesiano
x : variabile indipendente
Le primitive di una funzione
Insiemi e logica Insiemi e operazioni insiemistiche
La circonferenza nel piano cartesiano
(7x + 8x2 + 2) : (2x + 3) 8x2 + 7x + 2 2x + 3 8x2 + 7x + 2 2x + 3 4x
L'ABC della statistica LA MEDIA ARITMETICA
4 < 12 5 > −3 a < b a > b a ≤ b a ≥ b
IL CONCETTO DI ALGORITMO
x : variabile indipendente
Unità di apprendimento 7
Equazioni differenziali
Insiemi di punti: altre caratteristiche
Equazioni e disequazioni
FUNZIONI MATEMATICHE DANIELA MAIOLINO.
Sulla complessità Lezione n°2
Tipo di dato: array Un array è un tipo di dato usato per memorizzare una collezione di variabili dello stesso tipo. Per memorizzare una collezione di 7.
Usi (meno scontati) della visita DFS
Divide et Impera Quicksort Mergesort
23) Esponenziali e logaritmi
I MONOMI.
Limite di una funzione appunti.
Lezione n°12 Prof.ssa Rossella Petreschi
I RADICALI Definizione di radicali Semplificazione di radicali
Codicfiche Interi Complemento alla base.
© 2007 SEI-Società Editrice Internazionale, Apogeo
{ } Multipli di un numero M4 ESEMPIO 0, 4, 8, 12, 16, 20, 24, …
I numeri relativi DEFINIZIONE. Si dicono numeri relativi tutti i numeri interi, razionali e irrazionali dotati di segno (positivo o negativo). ESEMPI Numeri.
32 = 9 x2 = 9 x = 3 32 = 9 √9 = 3 L’estrazione di radice
PROCEDURA per la misura e la relativa stima
Scrivere programmi corretti
Le espressioni algebriche letterali
Ordinamento in tempo lineare
Ricorsione 16/01/2019 package.
Le primitive di una funzione
Codici rilevatori di errori
Algoritmi e Strutture Dati
Corso di Laurea Ingegneria Informatica Fondamenti di Informatica
LE SUCCESSIONI Si consideri la seguente sequenza di numeri:
* 07/16/96 Sez. 2: Ordinamento La consultazione di banche dati è sempre più cruciale in tutte le applicazioni dell’Informatica. Se vogliamo consultare.
Teoria della computabilità
Algoritmi e Strutture Dati
Backtracking Lezione n°17 Prof.ssa Rossella Petreschi
Esercizio Dato un albero binario, definiamo altezza minimale di un nodo v la minima distanza di v da una delle foglie del suo sottoalbero, definiamo invece.
Le Frazioni Prof.ssa A.Comis.
Ricerca 01/08/2019 package.
Modello matematico per la risoluzione dei problemi
Le Equazioni di 1°grado Prof.ssa A.Comis.
Corso di Fondamenti di Informatica
Transcript della presentazione:

Parte 9. Cenni della Teoria della Complessità Computazionale Il tempo di calcolo Parte 9. Cenni della Teoria della Complessità Computazionale Corso A: Prof. Stefano Berardi http://www.di.unito.it/~stefano Corso B: Prof. Ugo de’ Liguoro http://www.di.unito.it/~deligu

“Il Tempo” 9 - Tempo di calcolo 1954, Salvator Dalí

Indice Parte 9: Tempo di calcolo Misure di complessità di programmi Come confrontare funzioni: la relazione fO(g) tra ordini di grandezza. Esempi: scomposizione di un numero in fattori primi, crivello di Eratostene. Complessità di un problema: come ottenere una stima esatta. 9 - Tempo di calcolo

1. Misure di complessità di programmi. Proviamo a chiederci: tra due algoritmi, quale è il più veloce? Non c’è una risposta facile né univoca. Ci interessa rispondere: per capire quanto tempo ci vuole per eseguire un programma che implementa un algoritmo per stimare la grandezza massima del valore in ingresso per una esecuzione “ragionevole” per confrontare l’efficienza di diversi algoritmi che risolvono lo stesso problema.

Altre misure oltre al tempo di calcolo Spazio di memoria = quanta memoria occorre per eseguire l’algoritmo hardware = di quale hardware abbiamo bisogno: il numero di processori, il numero dei componenti (o “porte”) di un circuito, ecc... richiesti da un dato programma 9 - Tempo di calcolo

Il tempo di calcolo è funzione del valore in ingresso Quanto tempo impiega il mio programma? Un tempo che dipende dal valore in ingresso Uscita, detta risposta Ingresso, detto istanza Programma 9 - Tempo di calcolo

Definizione del “tempo di calcolo” Sotto condizioni ragionevoli, tutte queste misure differiscono tra loro per un fattore costante! Ci sono molte definizioni di “tempo di calcolo”. Potremmo voler calcolare: il numero dei secondi (dipendente dalla macchina) il numero delle operazioni eseguite, ciascuna con un proprio coefficiente che ne rappresenta la durata (anche per operazioni non elementari, che non corrispondono a una singola istruzione della CPU). il numero delle volte che una specifica operazione viene eseguita, senza usare coefficienti di durata. 9 - Tempo di calcolo

Es.: calcolo minimo di un vettore int Minimo (int v, int j, int k) c1 1 //Pre: v vettore di dim. > k  j //Post: restituisce il min. in v[j..k] { int min = v[j] ; c2 1 for( i=j + 1;i<=k;++i) c3 k  j + 1 if (v[i] < min) then c4 k  j min = v[i]; c5  k  j return min;} c6 1 costo volte c5 vale (k-j) quando v[j..k] è ord. in senso decrescente: in tal caso il valore di min cambia ad ogni passo del ciclo, quindi eseguiamo min=v[i] ad ogni passo del ciclo.

Esempio: minimo in un vettore Per calcolare una limitazione superiore al tempo di calcolo è sufficiente sommare tutti i costi parziali, prendendo ogni volta il caso peggiore. Nel nostro esempio, posto n = k  j + 1 il numero degli elementi tra i quali cerchiamo il minimo, si ha: con a e b costanti che “in linea principio” sappiamo calcolare, ma che di fatto è difficile conoscere.

La dimensione del valore in ingresso Nel caso di Minimo (v, j, k) ciò che conta è il numero degli elementi in v[j..k], non il loro valore In generale come dimensione del dato in ingresso di una funzione prendiamo una misura (a meno di una costante) dei bits necessari a rappresentare quel dato. Per esempio, se m è un intero e v un vettore: |m| = dimensione di m numero intero = num. bits per rappresentare m = log2 (m + 1)  |v [0..n  1] |= dimensione di v[0..n  1] vettore = n c, dove c = num. bits del generico el. di v 9 - Tempo di calcolo

Tempo di calcolo T(n) su dati di dimensione n Il tempo di calcolo T(x) può variare enormemente al variare di x. Supponiamo quindi, per semplicità, di voler esprimere T(n) in funzione della dimensione n in bits del dato x in ingresso, invece che di x stesso. La difficoltà naturalmente è che: n=|x| = |y| (stesso numero n di bits in x,y) non implica certo T(x) = T(y) (stesso tempo di calcolo per x,y), dato che, appunto, due dati della stessa dimensione possono richiedere tempi molto diversi Come definire T(n) sulla dimensione n=|x| del dato in ingresso x? 9 - Tempo di calcolo

Tempo di calcolo su dati di dimensione n Dobbiamo rendere T(n) indipendente dal particolare dato x di dimensione n: Distinguiamo allora varie possibili scelte. La prima è: Il caso migliore: un qualunque dato x tale che Tmigliore (|x|) = T(x) Minimo (v, j, k): caso migliore quando il minimo è proprio v[j] 9 - Tempo di calcolo

Tempo di calcolo sui dati in ingresso di dimensione n Dobbiamo rendere T(n) indipendente dal particolare dato x di dimensione n: Distinguiamo allora varie possibili scelte. Un’altra è: caso peggiore: x t.c. Tpeggiore (|x|) = T(x) Minimo (v, j, k): caso peggiore quando v[j..k] è ordinato in senso decrescente, a ogni passo riassegnamo “min”. 9 - Tempo di calcolo

Un esempio di tempo di calcolo nel caso minimo e massimo “istanze” in “ingresso” aventi una dimensione data n 5 ms 3 ms 1 ms A B C D E F G Questo è il tempo T(n) nel caso peggiore nel caso migliore In questo grafico non supponiamo alcun ordine sui dati in ingresso 9 - Tempo di calcolo

Sui dati in ingresso di dimensione n Come potremmo definire un caso “tipico”, o “medio”? L’unica risposta precisa sarebbe: il “valore medio” è “valore atteso di una variabile casuale” Ma la definizione del valore atteso richiederebbe un integrale: non possiamo parlarne qui 9 - Tempo di calcolo

2. Come confrontare funzioni Sappiamo che il tempo di calcolo non è un numero ma una funzione Dunque: per confrontare il tempo di calcolo di due algoritmi dobbiamo confrontare tra loro due funzioni Ma come è possibile confrontare tra loro oggetti infiniti? La risposta è: “trascurando un numero finito di casi” e “trascurando i fattori costanti” 9 - Tempo di calcolo

Confrontare funzioni trascurando un numero finito di casi f (x) = 7x g(x) = x2 /75 Un esempio: in questo caso, abbiamo f (x) < g(x) per ogni x > 525. Possiamo concludere che f<g? 9 - Tempo di calcolo

Possiamo concludere f<g se trascuriamo un numero finito di casi Sempre che non siano proprio loro i casi più frequenti! Decidiamo allora di trascurare un numero finito di casi (per es. i valori da 0 a 50) 9 - Tempo di calcolo

Perché trascurare i fattori costanti? I fattori costanti nel tempo di calcolo sono importanti, ma non “così importanti” quando il tempo di calcolo cresce molto rapidamente. Per esempio, supponiamo di usare un algoritmo che impiega tempo T(n) = 2n. Supponiamo che la dimensione massima in bits per il dato in ingresso di un problema “trattabile” col computer C1 sia D. Ci viene spontaneo pensare: costruiamo un computer C2 1000 volte più veloce, o definiamo un algoritmo 1000 volte più veloce, e vedrete di quanto migliorerà D! Il computer C2 ha il “turbo” …

Perché trascurare i fattori costanti? Supponiamo ora di disporre di un computer C2 1000 volte più veloce di C1, oppure di un algoritmo 1000 volte più veloce. Adesso T’(n) = 2n/1000, dunque nello stesso tempo 2D in cui trattavamo un dato di dimensione D possiamo trattare un dato di dimensione D’ > D, tale che 2D’ /1000 = 2D 9 - Tempo di calcolo

Perché trascurare i fattori costanti? Ma di quando aumenta la massima dimensione D dei dati che riusciamo a trattare se il tempo di calcolo si riduce di 1000 volte? Di soli 10 bits! Trovate il calcolo qui sotto. Dunque quando il problema richiede tempo T(n) = 2n, accelerare il tempo di calcolo di 1000 volte ha un effetto finale trascurabile. Ecco perché si è scelto di trascurare i fattori costanti

Migliorare i programmi è meglio che migliorare i computer TA (n) = 100n TB (n) = 2n2 L’algoritmo A è migliore dell’algoritmo B per n > 50; Rimpiazzare B con A è meglio che raddoppiare la velocità del computer. Confrontiamo i tempi di calcolo di A e B quando n=1000: A è 20 volte meglio di B

Riassumendo, la Teoria della Complessità trascura le costanti ... … perché quando la complessità è più che polinomiale (es.: T(n) = 2n) moltiplicando la velocità del computer per una costante la dimensione massima di un problema trattabile cresce di pochissimo. Anche quando la complessità è polinomiale (espressa da un polinomio) comunque la “velocità” di crescita di una funzione (il suo comportamento qualititativo) non dipende dalla scelta della costante. 9 - Tempo di calcolo

La Teoria della Complessità trascura le costanti anche perché … … la stima esatta delle costanti non è una richiesta realistica (bisognerebbe considerare la diversità delle architetture, la diversità di efficienza dei compilatori, e del tempo impiegato da istruzioni di diverso tipo). Conviene quindi astrarre dal valore delle costanti nella teoria, ottenendo previsioni di tipo qualitativo, ma comunque utili per evitare gli errori più comuni. 9 - Tempo di calcolo

Ordini di grandezza: la relazione O-grande tra funzioni c g(n) f (n) n0 La relazione O, introdotta da P. Bachmann nel 1892, trascura sia un numero finito di casi che i fattori costanti, ed è stata adottata dalla Teoria della Complessità per confrontare i tempi di calcolo.

Un’altra relazione tra funzioni: il confronto asintotico f  g se “f(x)  g(x) da un certo z in poi”, ovvero:  z  x  z. (f(x)  g(x)) Una definizione equivalente e molto usata è: f  g  x. (f(x)  g(x)) dove abbiamo usato l’abbreviazione: x. P(x)   z  x  z. P(x) x. P(x) si legge: “P vale quasi per ogni x” o “P vale per ogni x, tranne che per un numero finito”. f g z 9 - Tempo di calcolo

Per capire una definizione, attenti all’ordine dei quantificatori Scrivere un esistenziale prima anziché dopo un universale definisce una proprietà molto più forte. L’enunciato gb.loves(b,g) è falso nell’esempio a destra, in cui bg.loves(b,g) è vero ... bg.loves(b,g) gb.loves(b,g) Sam Mary Bob Beth John Marilyn Monroe Fred Ann Sam Mary Bob Beth John Marilyn Monroe Fred Ann ... dunque bg.loves(b,g) non implica gb.loves(b,g) 9 - Tempo di calcolo

Le relazioni fg ed fO(g) sono preordini La relazione fg è soltanto un preordine (riflessiva e transitiva), dunque fg f è una relazione di equivalenza. f  g e fO(g) non sono dei pre-ordine totali, esistono cioè funzioni f, g non confrontabili, come le due seguenti: 0 se x pari 1 se x dispari 1 se x pari 0 se x dispari g(x) = f(x) = 9 - Tempo di calcolo

Le relazioni fg ed fO(g) sono preordini f  g non è un ordine parziale, non essendo antisimmetrica. Infatti fg f non equivale a f=g, possiamo solo provare che: f  g  f  g   z  x  z. f(x) = g(x) Dunque se f  g e g  g , allora f, g sono “uguali da un certo z in poi”. Analogamente: fO(g) e gO(f) e’ una relazione di equivalenza, che individua classi di funzioni con lo stesso “ordine di infinito”. 9 - Tempo di calcolo

Le costanti non contano nella relazione f  O(g) Per ogni f, g, e per ogni costante c > 0 f (n)  O(c  g(n))  f (n)  O(g(n)) ) se q.o. f (n)  d  c  g(n) allora d  c è la costante cercata. ) esistono d > 0 e n0 tali che f (n)  d  g(n), per ogni n  n0. Posto b = d/c (esiste perché c  0), abbiamo b > 0 e f (n)  d  g(n) = b  c  g(n) per ogni n  n0. O(1) include l’insieme delle funzioni costanti 9 - Tempo di calcolo

Ordini di grandezza : un esempio O-grande 3n2 + 7n + 8  O(n2) c g(n) f(x)  O(g(x)) f (n) Cerchiamo c, n0: ? 3n2 + 7n + 8  c·n2 9 - Tempo di calcolo

Ordini di grandezza : un esempio O-grande 3n2 + 7n + 8  O(n2) c g(n) f(x)  O(g(x)) f (n) ? 3n2 + 7n + 8 ≤ 4·n2 4 9 - Tempo di calcolo

Ordini di grandezza : un esempio O-grande 3n2 + 7n + 8  O(n2) c g(n) f(x)  O(g(x)) f (n) 1 3·12 + 7·1 + 8 ≤ 4·12 4 9 - Tempo di calcolo

Ordini di grandezza : un esempio O-grande 3n2 + 7n + 8  O(n2) c g(n) f(x)  O(g(x)) f (n) 9 3·92 + 7·9 + 8 ≤ 4·92 4 314 324 9 - Tempo di calcolo

Ordini di grandezza : un esempio O-grande 3n2 + 7n + 8  O(n2) c g(n) f(x)  O(g(x)) f (n) 8 3·82 + 7·8 + 8 ≤ 4·82 4 3·82 + (7+1)·8 9 - Tempo di calcolo

Ordini di grandezza : proviamo che 3n2 + 7n + 8  O(n2) Basta provare: La tesi equivale a Dividendo per n: Ma e quindi 9 - Tempo di calcolo

Ordine di grandezza di un polinomio (a coefficienti non negativi) Se p(n) è un polinomio di grado k allora p(n)  O(nk) dove a = max{ai : 0  i  k} (consideriamo solo coefficienti non negativi). Prova: I termini di grado inferiore si possono ignorare 9 - Tempo di calcolo

Ordini di grandezza di un polinomio Invece: se p(n) è un polinomio di grado h>k e coeff. dir. positivo, allora p(n)  O(nk) P.a. se p(x)  O(xk), allora se a è il coeff. direttore di p, q.o.: Ma allora: da cui una contraddizione quando n > d. Tutto ciò che conta in un polinomio è il grado 9 - Tempo di calcolo

Ordini di grandezza di un polinomio Riassumendo: se h < k allora O(nh)  O(nk) (e dunque O(nk)  O(nh)) Dunque esiste un ordine di infinito per ogni grado possibile di un polinomio. Ma esistono anche, come vedremo, molti altri ordini di infinito, sia compresi tra due ordini di infinito per polinomi (come n*log(n)), sia maggiori di tutti gli ordini di infinito dei polinomi (come n!, oppure 2n). 9 - Tempo di calcolo

Ordini di grandezza intermedi tra 1, n, n2 1. O(1)  O(log n) L’inclusione opposta è falsa perché log n è superiormente illimitata, mentre f (n)  O(1)   c  n. f (n)  c 2. O(log n)  O(n) Infatti log n  n  2log n  2n , e quest’ultima disequazione vale perché 2log n = n < 2n per ogni n. Anche questa inclusione non è reversibile. 3. O(n)  O(n log n) Quest’ultima inclusione segue da n > 2  log n > 1  n log n > n. Anche questa inclusione non è reversibile. 9 - Tempo di calcolo

O(2n) è più grande di ogni ordine di infinito polinomiale Tesi: O(np)  O(2n) Dimostriamo prima il Lemma: Prova di fO(g). Per ipotesi, per ogni c > 0, abbiamo 0  f (n)/g(n) < c quasi per ogni n. Quindi f (n) < c g(n) quasi per ogni n. 9 - Tempo di calcolo

Questa contraddizione termina la prova Ogni polinomio è O(2n) O(np)  O(2n) Per dimostrare il Lemma qui sotto resta da provare: gO(f). Procediamo per assurdo. Questa contraddizione termina la prova Prova per assurdo 9 - Tempo di calcolo

Ogni polinomio è O(2n) O(np)  O(2n) In altre parole, tutte le funzioni esponenziali crescono più velocemente di tutte le funzioni polinomiali Per il Lemma qui sopra, basta osservare che abbiamo:

O-piccolo implica o-grande ma non viceversa La prossima osservazione è dedicata a chi ha studiato la relazione f (n)o(g(n)), ovvero “f è infinitesima rispetto a g”, nei corsi di Analisi. Se abbiamo f(n)  o(g(n)) allora ne segue: fO(g) e gO(f) Osserviamo infatti che, se g è positiva non nulla: 9 - Tempo di calcolo

Classificazione delle funzioni di interesse per l’Informatica Poli. Esp Costante Logaritmica   Polinomiale    Doppio Esp  Esponentziale Poli-logaritmica Esempi: 25n 2 2 n5 5 5 log n (log n)5 n5 25n 9 - Tempo di calcolo

Funzioni ordinate per velocità di crescita

O-grande non è una relazione di equivalenza Nei testi di Teoria della Complessità spesso leggiamo eguaglianze del tipo Prese alla lettera conducono a: assurdo ?!? f(n) = O(g(n)) è solo una abbreviazione di f(n)  O(g(n)), una relazione asimmetrica (cioè “a senso unico”), in cui rimpiazziamo una funzione f con il suo ordine di grandezza g 9 - Tempo di calcolo

Operazioni “modulo O-grande” Le operazioni aritmetiche hanno come corrispondenti delle operazioni su “ordini di grandezza”. Definiamo: f (n)+O(g(n)) = {h : gO(g(n)) .n. h(n)  f(n)+g(n)} f (n)O(g(n)) = {h : gO(g(n)) .n. h(n)  f (n)  g(n)} Allora ne segue: f (n) + O(g(n)) = O(f (n) + g(n)) f (n)  O(g(n)) = O(f (n)  g(n)). Similmente definiamo O(f (n)) + O(g(n)) = {h :  f  O(f (n))  g O(g(n))  n. h(n)  f (n) + g(n)}, ed O(f (n))  O(g(n)) analogamente. Queste operazioni definiscono un’algebra. 9 - Tempo di calcolo

Alcune eguaglianze vere “modulo O-grande” Possiamo dedurre: f (n) = O(f (n)) c  O(f (n)) = O(f (n)) (per c costante) O(f (n)) + O(g(n)) = O(f (n) + g(n)) O(f (n)) + O(f (n)) = O(f (n)) f  g  O(f (n)) + O(g(n)) = O(g(n)) O(f (n))  O(g(n)) = O(f (n)  g(n)) Perciò ad esempio: O(1) + O(1) = O(2) = O(1) mentre n  O(1) = O(n) (n è una variabile, indica la funzione identità) 9 - Tempo di calcolo

4. Esempi. “Quanto tempo ci vuole” a costruire la matrice identità? MatriceIdentità (Matrice A, int n) // Post. A è la matrice identità nxn { for (int i = 1; i <= n; i++) for (int j = 1; j <= n; j++) A[i][j] = 0; for (int i = 1; i <= n; i++) A[i][i] = 1; } for interno T2 for T5 for esterno T3 corpo T1 corpo T4 9 - Tempo di calcolo

4. Esempi. “Quanto tempo ci vuole” a costruire la matrice identità? Sommiamo i tempi di calcolo parziali: T1(n)  c1 = O(1) T2(n) = n  T1(n) = n  O(1) = O(n) T3(n) = n  T2(n) = n  O(n) = O(n2) T4(n)  c2 = O(1) T5(n) = n  T4(n) = n  O(1) = O(n) T(n) = T3(n) + T5(n) = O(n2) + O(n) = O(n2 + n) = O(n2). Questa è la risposta. 9 - Tempo di calcolo

“Quanto tempo ci vuole” a scomporre un numero in fattori primi? Iniziamo ad analizzare un algoritmo “ingenuo”, che applica la definizione di scomposizione in fattori primi alla lettera Test di primalità void Fattorizzazione (int n) // Pre: n > 0 // Post: stampa la fattorizzazione in numeri primi di n { for (int i = 2; i <= n-1; i++) { if (Primo(i)) { int k = Potenza (i, n); if (k != 0) cout << i << "^" << k << endl;}}} Calcola il massimo k tale che ik divide n

Tempo per la Fattorizzazione void Fattorizzazione (int n) {for (int i = 2; i <= n-1; i++) {if (Primo(i)) {int k = Potenza (i, n); if (k != 0) cout << i << "^" << k<<endl;}}} Per stimare il tempo di fattorizzazione dobbiamo prima stimare il tempo per decidere se un numero è primo. Non confondete i due problemi, la fattorizzazione richiede una lista di primi mentre la decisione se un numero è primo solo una risposta vero/falso.

Test di primalità: il tempo richiesto da una soluzione “forza bruta” bool Primo (int n) // Pre: n positivo // Post: ritorna true se n è primo {bool primo = true; for (int i=2; i<n && primo==true; i++) primo = (n % i != 0); return primo;} È una soluzione per “forza bruta”: applico la definizione di primo alla lettera Il numero eccessivo di divisioni di n richiesto dalla funzione “Primo” è il punto debole di questo metodo di fattorizzazione: 9 - Tempo di calcolo

Il tempo richiesto dalla funzione “Potenza” int Potenza (int p, int n) // Pre: p, n > 0, p è primo // Post: ritorna l'esponente di p nella fattorizzaz. di n { int potenza = 0; while (n % p == 0) { n = n/p; potenza++;} return potenza;} Questa funzione richiede tempo logaritmico, perché il massimo numero di k volte in cui posso dividere n per p è logp(n), dato che dobbiamo avere pk divisore n:

Tempo di Fattorizzazione con un algoritmo “forza bruta” void Fattorizzazione (int n) {for (int i = 2; i <= n; i++) {if (Primo(i)) {int k = Potenza (i, n); if (k != 0) cout << i << "^" << k << endl;}}} Qual è l’ordine di grandezza di questa somma? per opportune costanti a, b, c 9 - Tempo di calcolo

L’ordine di grandezza di Poniamo S = 1 + 2 + . . . + n – 1 + n S = n + n – 1 + . . . + 2 + 1 2S = (n + 1) + (n + 1) + … + (n + 1) + (n + 1) n 9 - Tempo di calcolo

Tempo di Fattorizzazione con un algoritmo “forza bruta” void Fattorizzazione (int n) {for (int i = 2; i <= n; i++) {if (Primo(i)) {int k = Potenza (i, n); if (k != 0) cout<<i<< "^" << k<<endl;}}} Possiamo ignorare nlog n perché n log n  O(n2) Il costo O(n2) dipende essenzialmente dal costo O(n2) della funzione Primo, che è eccessivo: con poco sforzo si può fare meglio. 9 - Tempo di calcolo

Test di primalità (migliorato) Partiamo dall’osservazione: se n è composto allora ha un divisore proprio  n Ipotesi assurda: sia n composto con solo divisori propri > n. Se p|n allora n = pq per qualche q, che pure divide n, dunque: Contraddizione. Analogamente si può dimostrare: n ha al più un fattore primo > n. Basta quindi cercare i fattori primi  n per scomporre n in fattori primi.

Test di primalità (migliorato) bool Primo_rad (int n) // Pre: n positivo // Post: ritorna true se n è primo { bool primo = true; float radice_n = sqrt(n); for (int i = 2; i <= radice_n && primo; i++) primo = n % i != 0; return primo;} La funzione “Primo” migliorata richiede solo un numero di divisioni pari a O(n), dunque: 9 - Tempo di calcolo

Fattorizzazione: seconda versione Un miglioramento da O(n2) a O(nn), si ottiene da Fattorizzazione sostituendo Primo con Primo_rad. Basta ripetere il calcolo precedente e osservare come cambia il totale. Qual è l’ordine di grandezza di questa somma? 9 - Tempo di calcolo

Ordine di grandezza di Un buon modo per stimare questa somma è trasformarla nell’integrale di x da 1 ad (n+1) 1 n + 1 9 - Tempo di calcolo

Fattorizzazione: seconda versione Come dicevamo, un primo miglioramento, da O(n2) a O(nn), si ottiene sostituendo Primo con Primo_rad.

Fattorizzazione: seconda versione O(nn) è una classe di complessità intermedia tra O(n) ed O(n2). Il prossimo algoritmo che vedremo, il Crivello di Eratostene, genera la lista dei primi tra 1 e n. Per generare la lista dei fattori primi di un intero n, è sufficiente usare il Crivello di Eratostene per generare la lista dei primi tra 1 e n: questi primi includono tutti i fattori primi di n. In seguito, con una seconda selezione, individuiamo i fattori primi di n. Questo nuovo algoritmo migliora ancora il tempo della fattorizzazione, portandolo a O(log2(n)n).

Il crivello di Eratostene Idea. Dato n costruiamo l’elenco di tutti gli interi 2  i  n. Quindi da 2 in poi, e per ogni 2  i  n, depenniamo tutti i multipli propri di i (escluso quindi i stesso), purché i sia ancora sulla lista. Alla fine del lavoro, gli interi superstiti sono esattamente gli interi che non sono multipli propri di interi  2. Dunque gli interi superstiti sono esattamente i numeri primi. 9 - Tempo di calcolo

Il crivello di Eratostene applicato a n=50 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 9 - Tempo di calcolo

Il crivello di Eratostene: il programma bool* Eratostene (int n) /* Pre: n > 0. Post: crea e restituisce un vettore v[2..n] di booleani tale che v[p] == true se e solo se p e' primo */ {int i; bool* v = new bool[n+1]; for (i = 2; i <= n; i++) v[i] = true; for (i = 2; i <= n; i++) if (v[i]==true) for (int j = i+i; j <= n; j=j+i) v[j] = false; return v;} “Allocazione dinamica” (creazione) di un vettore di n+1 booleani 9 - Tempo di calcolo

Stima della complessità TErat(n) bool* Eratostene (int n) {int i; bool* v = new bool[n+1]; for (i = 2; i <= n; i++) v[i] = true; for (i = 2; i <= n; i++) if (v[i]==true) for (int j = i+i; j <= n; j=j+i) v[j] = false; return v;} Tempo O(n) Per ogni i tra 2 e n, questo ciclo viene eseguito un numero di volte pari ai multipli propri di i  n, dunque O(n/i) volte

Stima della complessità TErat(n) In base a quando detto nel lucido precedente, il crivello di Eratostene consiste in una parte di costo O(n) ripetuta una volta sola, più una parte di costo O(n/i) ripetuta una volta per ogni valore di i compreso tra 2 ed n. Ne deduciamo che: Quindi per stimare TErat dobbiamo stimare la somma: 1+(1/2)+(1/3)+ ...+(1/n) al variare di n.

Stimiamo l’ordine di grandezza di: Di nuovo, è conveniente ridursi al calcolo di un integrale 1 Dai calcoli del lucido precedente deduciamo che Eratostene ha costo O(n*log(n)). Ora scriveremo un algoritmo di fattorizzazione basato sul Crivello di Eratostene che richiede tempo O(log2(n)n), un ordine di infinito maggiore di O(n), ma molto inferiore a O(n). 1/2 1 2 3 4

Fattorizzazione: una soluzione in tempo n*log(n) void Fattorizzazione (int n) {bool* Primo = Eratostene (sqrt(n)); for (int i = 2; i <= sqrt(n); i++) if (Primo[i]) {int k = Pot_Prim (i, n); if (k != 0) {cout <<i<<"^"<< k << endl; n=n/pow(i,k);}} if (n!=1) cout << n << "^" << 1 << endl;} Ora “Primo” richiede tempo O(1)

Fattorizzazione: discussione Finora, nel calcolo di TFatt(n), abbiamo usato come parametro il valore n dell’intero. In tutti i casi abbiamo ottenuto soluzioni del tipo (all’incirca) O(nc), per c=2, (3/2), (1/2). Possiamo riformulare le nostre conclusioni in termini di |n|, ovvero la dimensione in bits (il numero delle cifre) dell’intero n. Poiché |n|  log2(n), ovvero 2|n|  n, tutte le funzioni viste sopra, se espresse in funzione del numero di bits |n| di n, hanno ordine di grandezza esponenziale nel numero delle cifre: 2c|n|, sempre per c=2, (3/2), (1/2).

Fattorizzazione: discussione Una soluzione che richiede tempo esponenziale nel numero delle cifre non è effettivamente utilizzabile se il numero delle cifre è grande (decine o centinaia). Per il test di primalità si conosce un algoritmo in tempo |n|k (polinomiale nel numero delle cifre di n: Agrawal-Kayal-Saxena 2002). Non si conoscono, invece, algoritmi polinomiali per la fattorizzazione in primi, ma neppure una ragione per cui non possano esistere. Per maggiori informazioni rimandiamo a: mathworld.wolfram.com/ PrimeFactorizationAlgorithms.html

4. Complessità di un problema: come ottenere il confine “stretto” Naturalmente si tratta di un’asserzione vera, ma il confine 2n non è stretto: ci sono molti ordini di grandezza tra n e 2n 9 - Tempo di calcolo

Complessità di un problema: un esempio Somma 17. Dato un vettore di n interi positivi decidere se ne contiene due la cui somma sia 17. Chiediamoci quali sono i tempi di calcolo sufficienti e necessari, ovvero le limitazioni di tempo superiori e inferiori per la soluzione di questo problema. Esistono per la “Somma 17” soluzioni “ottime”, tali cioè che nessuna funzione di complessità inferiore risolva il problema? 9 - Tempo di calcolo

Complessità di un problema: il confine superiore Qual è un tempo di calcolo sufficiente alla risoluzione del problema “Somma 17”? Confine superiore alla complessità di un problema: un confine superiore per il tempo di calcolo (nel caso peggiore) di un algoritmo che risolve il problema 9 - Tempo di calcolo

Complessità di un problema: il confine superiore g T Confine superiore alla complessità di un problema: un confine superiore per il tempo di calcolo (nel caso peggiore) di un algoritmo che risolve il problema 9 - Tempo di calcolo

Complessità di un problema: una limitazione superiore Somma 17. Dato un vettore di n interi positivi decidere se ne contiene due la cui somma sia 17. bool Somma17 (int v[], int n) { bool b = false; for (int i = 0; i < n; i++) for (int j = i+1; j < n; j++) if (v[i] + v[j] == 17) b = true; return b;} 9 - Tempo di calcolo

Complessità di un problema: il tempo necessario Qual è un tempo di calcolo necessario alla risoluzione del problema “Somma 17”? Confine inferiore alla complessità di un problema: un confine inferiore per i tempi di calcolo (nel caso peggiore) di tutti gli algoritmi che risolvono il problema 9 - Tempo di calcolo

Complessità di un problema: il confine inferiore Tk … f Confine inferiore alla complessità di un problema: un confine inferiore per i tempi di calcolo (nel caso peggiore) di tutti gli algoritmi che risolvono il problema 9 - Tempo di calcolo

Confini banali Dimensione dei dati: quando è necessario esaminare tutti i dati in ingresso, ovvero generare tutti i dati in uscita. Es. La moltiplicazione di due matrici quadrate di ordine n richiede l’ispezione di 2n2 entrate. Eventi “contabili”: quando c’è un evento la cui ripetizione un numero calcolabile di volte sia necessaria alla soluzione del problema. Es. La determinazione del massimo tra n elementi richiede n  1 confronti, in cui altrettanti elementi non massimi risultino minori. 9 - Tempo di calcolo

Complessità di un problema Contando il numero n degli elementi del vettore e poiché ognuno di essi deve essere considerato almeno una volta, un confine inferiore alla complessità del problema Somma17 è n. Questo è un esempio del criterio della “dimensione dei dati”. A questo punto, se esiste una soluzione O(n) a “Somma 17” sarà una soluzione ottima, non ulteriormente migliorabile (tranne che di un fattore costante) 9 - Tempo di calcolo

Una soluzione O(n) per Somma17 (dunque una soluzione “ottima”) Calcoliamo il “vettore caratteristico” dell’insieme C = {i | i17 e i=v[j] per qualche j<dim. di v[] } dato che v[] ha solo interi positivi, la risposta sarà true se e solo se esistono i, j  C tali che i+j=17 9 - Tempo di calcolo

Una soluzione O(n) per Somma17: il programma bool Somma17_veloce (int v[], int n) { bool c[18]; int i, j; for (i = 0; i < 18; i++) c[i] = false; for (i = 0; i < n; i++) if (v[i] <= 17) c[v[i]] = true; for (i = 0, j = 17; i < j && !(c[i] && c[j]); i++, j--){}; return i < j; } 9 - Tempo di calcolo

Una soluzione O(n) per Somma17: un esempio di calcolo v[5] = 1 9 25 2 8 c[18] = F T T F F F F F T T F F F … F F F F F 0 1 2 3 4 5 6 7 8 9 10 11 12 … 13 14 15 16 17 9 - Tempo di calcolo

Una soluzione O(n) per Somma17 bool Somma17_vel (int v[], int n){bool c[18]; int i, j; for (i = 0; i < 18; i++) c[i] = false; for (i = 0; i < n; i++) if (v[i] <= 17) c[v[i]] = true; for (i=0,j=17; i< j && !(c[i] && c[j]); i++,j--){}; return i < j;} Soluzione ottima: O(n) è un confine inferiore per il problema 9 - Tempo di calcolo

Riassumendo La complessità temporale di un algoritmo è funzione della dimensione dell’ingresso, stimando il “tempo” di calcolo nel caso peggiore. Le funzioni tempo sono confrontate asintoticamente ed a meno di costanti moltiplicative. 9 - Tempo di calcolo

Riassumendo Per stabilire un confine superiore alla complessità di un problema basta un algoritmo. Per stabilire un confine inferiore bisogna considerarli tutti, e controllare che siano tutti oltre il confine inferiore. Esiste una soluzione ottima di un problema quando il confine superiore al tempo di un algoritmo che lo risolve coincide con quello inferiore alla complessità del problema. 9 - Tempo di calcolo