Teoria della “Big-O” - Matematica e Informatica

Teoria dei numeri e Crittografia: lezione del 26 ottobre 2011
Nota storica: il più grande numero primo conosciuto attualmente è il numero 243.112.609-1 (è uno dei
cosiddetti numeri di Mersenne della forma 2n-1, che studieremo in seguito). Esso ha 12.978.189
cifre (in base 10) ed è stato trovato nell’Agosto 2008 nell’ambito del progetto GIMPS (Great
Internet Mersenne Primes Search: vedere il sito www.mersenne.org).
Un test di primalità è un algoritmo che permette di testare se un numero naturale a>1 dato in input
è primo o no: l’output di un tale algoritmo è dunque “a è primo” oppure “a non è primo”..
Il più “ingenuo” dei test di primalità consiste ovviamente nell’effettuare le divisioni di a per i
numeri naturali j=2,3,…,a-1 : se per uno di tali j la divisione ha resto 0 l’output è “a non è primo”;
in caso contrario l’output è “a è primo”. Se x=L(a) è la lunghezza binaria dell’input, ogni divisione
ha complessità di ordine non superiore ad O(x2), ma il numero di divisioni, come funzione
dell’input a, è f(a) = (a-2) (nel caso peggiore): poiché 2x-1-2  a-2 < 2x-2, se consideriamo il
numero di divisioni come funzione g(x) della lunghezza binaria x, in termini di ordine si ha
O(2x) = O(2x-1-2)  O(g(x)) O(2x-2)=O(2x)
dunque il numero di divisioni ha ordine esponenziale e tale algoritmo non è perciò “efficiente”.
Osservazione. Il ragionamento applicato sopra si può applicare in tutte le situazioni simili: se una
funzione f(a) dell’input a è di ordine lineare, la stessa funzione, considerata come funzione della
lunghezza binaria x di a, è di ordine esponenziale O(2x); con lo stesso ragionamento si può
dimostrare che più in generale se f(a) è di ordine polinomiale di grado m, allora la stessa funzione,
considerata come funzione della lunghezza binaria x di a, è di ordine esponenziale O((2m)x).
Il test di primalità sopra esposto può essere reso più efficiente limitandosi per esempio ad input a
dispari (un numero pari a non è primo tranne nel caso banale a=2) ed effettuando quindi le divisioni
solo per i valori dispari j con 1<j<a, ma anche in questo caso il numero delle divisioni è di ordine
lineare rispetto ad a, quindi di ordine esponenziale rispetto ad x.
Una efficienza ancora superiore si può ottenere con la seguente osservazione: se a non è primo
esiste un suo divisore non banale  a (se infatti a=bc con b,c divisori non banali di a, e se per
assurdo fosse b, c > a , seguirebbe a>( a )2 , contraddizione). Dunque nell’algoritmo precedente
si potrebbero effettuare solo le divisioni per i valori j con 1<j a quindi stavolta il numero di
divisioni sarebbe di ordine O( a )<O(a) come funzione di a, ma purtroppo ancora di ordine
esponenziale O((21/2)x) come funzione di x .
Problema
1) Esiste un test di primalità di complessità non superiore alla polinomiale ?
Il problema è rimasto aperto per molto tempo: dagli esempi precedenti era chiaro come la strategia
migliore fosse quella di trovare una proprietà “intrinseca” dei numeri primi che si potesse testare
con un algoritmo di complessità polinomiale, e non quella di trovare un eventuale divisore non
banale dell’input.
Il problema 1) è stato infine risolto nel 2003 da Agrawal, Kayal, Saxena nel loro articolo “Primes in
P”, con la costruzione di un test di primalità di complessità polinomiale, ormai noto come “test
AKS” (test di cui ci occuperemo in seguito).
I test di primalità (così come sono stati da noi finora) definiti sono test di primalità deterministici
nel senso che, dato in input un numero naturale a>1, essi forniscono come output “a è numero
primo” se e solo se l’input a è un numero primo.
Esistono però i cosiddetti test di primalità probabilistici nei quali vi è la scelta casuale di alcuni
elementi utilizzati durante l’esecuzione dell’algoritmo e nei quali, se l’input a è primo, l’output è
sempre (correttamente) “a è un numero primo”, ma se a non è primo l’output può essere talvolta
(non correttamente) “a è un numero primo”, e la probabilità di tale errore è maggiorata da una
costante C<1 indipendente sia dall’input che dagli elementi casuali. In questo caso eseguendo k
volte il test sullo stesso input a, se l’output fosse tutte le volte “a è primo” si potrebbe dichiarare che
l’input a è effettivamente un numero primo con una probabilità di errore maggiorata da Ck (quantità
che si può rendere piccola a piacere).
Sono ben noti da tempo test di primalità probabilistici di complessità polinomiale (come il test di
Rabin-Miller, anche questo oggetto in futuro del nostro studio, nel quale la costante C è 1/4).
E’ anche utile notare che esistono test di primalità deterministici di complessità polinomiale, ma che
sono validi solo per numeri naturali particolari (per esempio i numeri di Fermat della forma 2n+1,
oppure i numeri di Mersenne della forma 2n-1): anche di questi test parleremo in seguito.
Un algoritmo di fattorizzazione è invece un algoritmo che, dato in input un numero naturale a>1,
calcola tutti i fattori primi della sua fattorizzazione.
In generale un algoritmo di fattorizzazione si limita, dato l’input a>1, a cercare un divisore non
banale b di a (se b non esiste si conclude che a è primo e che ha la sola fattorizzazione banale
a=a): una volta trovato b si pone c = a/b in modo che si abbia a = bc, e si applica di nuovo
l’algoritmo ai fattori b, c : così procedendo dopo un numero finito di passi l’algoritmo trova i fattori
primi della fattorizzazione di a.
Illustreremo in seguito vari algoritmi di fattorizzazione più o meno efficienti (algoritmo di Fermat,
algoritmo di Pollard etc….).
Problema
2) Esiste un algoritmo di fattorizzazione di complessità non superiore alla polinomiale ?
Al contrario del problema 1), il problema 2) attualmente non è stato risolto (e molti matematici sono
convinti che non abbia soluzione).
Distribuzione dei numeri primi
Nella successione dei numeri naturali, i numeri primi sono distribuiti in modo irregolare.
Si possono per esempio costruire intervalli di k naturali consecutivi (con k numero naturale
arbitrariamente grande) che non contengono nessun numero primo: basta considerare i k numeri
naturali consecutivi della forma (k+1)!+j dove j assume i k valori 2,3,…,k+1 (ognuno di essi è
multiplo di j, perché j(k+1)!, quindi non è primo).
Se ordiniamo in ordine crescente i numeri primi in una successione p1, p2, ….., pn,….. (quindi p1=2,
p2=3, p3=5, etc..) non esiste attualmente una formula algebrica che permetta di calcolare il numero
primo pn di posto n, ma si può dare un maggiorante (un po’ “grezzo”) per l’ordine di grandezza di
pn:
Teorema.
n-1
Per ogni numero naturale n si ha pn 2(2 ) .
1-1
Dimostrazione:Per induzione su n (IIa forma). Per n=1 si ha p1=2= 2(2 ) . Dato n>1, supponiamo
n-1
la tesi vera per ogni k=1,….,n-1, e dimostriamola per n: se poniamo a=(  pi )+1, per il Teorema
i=1
di Fattorizzazione unica esisterà un primo p divisore di a, e sarà pp1,p2,…,pn-1 (altrimenti si
avrebbe p1, contraddizione), dunque (essendo i primi pi ordinati in modo crescente) :
n-1
n-1
pn p  a =(  pi )+1  (  2(2
i=1
i-1
)
)+1= 2
 n-1 i-1 
 2 


 i=1


+1 (per l’ipotesi induttiva).
i=1
Ma è facile dimostrare (usando la Ia forma dell’ induzione) che
n-1
2
i-1
=2n-1-1 , dunque:
i=1
pn 2(2
n-1
-1)
+1=
2
(2 n-1 )
2
n-1
+1
2
(2 n-1 )
2
+
2
(2 n-1 )
2
= 2(2
n-1
)
e si ha la tesi.
Nota: il maggiorante 2(2 ) per pn è molto “debole”, e in generale 2(2
4-1
per esempio per n=4 si ha p4=7 ma 2(2 ) =256.
n-1
)
è molto più grande di pn;
Nell’ambito dello studio della distribuzione dei numeri primi, può essere interessante valutare il
numero di primi in un certo intervallo [0,x] della semiretta positiva dell’asse reale. Poniamo, per
ogni reale x>0 :
(x) = {p / p è primo, p  x}
(dunque (x) è il numero dei primi non superiori al numero reale x).
Per esempio (22,3)= {p / p è primo, p  22,3}={2,3,5,7,11,13,17,19}=8
Non esistono attualmente formule algebriche per il calcolo esatto di (x). Per esempio, usando i
computers, si è verificato che :
(1014) = 3.204.941.750.802
(i valori massimi attualmente calcolati sono relativi a valori di x intorno a 1022).
Sfruttando il Teorema precedente, possiamo trovare un minorante (anche questo un po’ “grezzo”)
per (x):
Teorema.
Per ogni reale x>0 si ha (x)  log2(log2x)+1.
Dimostrazione:
Sia n=log2(log2x)+1: dunque n è il più grande intero tale che
nlog2(log2x)+1
o equivalentemente
n-1
n-1log2(log2x), 2n-1log2x, 2(2 ) x.
Nella successione (crescente) dei primi, si ha (per il teorema precedente) che il numero primo pn di
n-1
posto n è  2(2 ) ; dunque:
n-1
p1<p2<….<pn 2(2 )  x.
Poiché almeno gli n primi distinti p1, p2, …., pn sono x, si conclude che n(x) e si ha la tesi.
Nota: il minorante log2(log2x)+1 per (x) è molto “debole”, e in generale log2(log2x)+1 è molto
più piccolo di (x); per esempio per x=109 si ha log2(log2x)+1=5 ma (x)5x107.
Nel corso del tempo, i matematici hanno cercato funzioni che “approssimassero” la funzione (x).
Il risultato più famoso è il seguente, che si dimostra con metodi analitici (e del quale omettiamo la
dimostrazione):
Teorema di Hadamard-De La Vallé Poussin.
Considerata la funzione f(x)=x/log(x) (definita per x>0, x1) dove log(x) è il logaritmo neperiano, si
π(x)
ha:
lim
=1
x  f(x)
π(x)
è “abbastanza vicino” ad 1, e in
f(x)
questo senso f(x)=x/log(x) è una buona approssimazione di (x).
π(1014 ) 3.204.941.750.802
14
Per esempio f(10 )  3.102.103.442.166,

1.033.
f(1014 ) 3.102.103.442.166
Quindi, per x abbastanza grande, il rapporto “percentuale”
Una funzione che approssima ancora meglio (x) è la funzione (definita per x2):
x
1
dt
li(x) = 
log(t)
2
detta “logaritmo integrale”.
In pratica il valore li(x) misura l’area sottesa dalla curva del grafico della funzione 1/log(t)
relativamente all’intervallo [2,x].
π(x)
Anche per tale funzione si può dimostrare che si ha lim
=1 , ma per esempio:
x®¥ li(x)
li(1014)  3.204.942.065.692
π(1014 ) 3.204.941.750.802

 0,9999999
li(1014 ) 3.204.942.065.692
(approssimazione migliore di quella ottenuta con la funzione x/log(x)).
Per x>7 la funzione li(x) sembra approssimare sempre per eccesso (x): con i moderni calcolatori si
è arrivati al calcolo relativo a valori di x intorno a 41022, e si è sempre verificato che li(x)>(x).
Molti matematici (tra cui Gauss e Riemann) congetturarono che ciò fosse vero per ogni valore x>7,
ma Littlewood (1914) dimostrò che la differenza li(x)-(x) cambia segno infinite volte per x che
tende a infinito (dimostrazione non costruttiva). Tuttavia non è stato ancora trovato effettivamente
34
un valore di x per cui li(x)< (x). Nel 1933 Skewes dimostrò che il più piccolo di tali x è < 10(10 )
(un numero con 1034+1 cifre in base 10). Tale maggiorazione è stata nel tempo migliorata: nel 2000
si é dimostrato che il più piccolo di tali x è <1,4 10 316 (un numero però ancora al di là delle
capacità dei calcolatori moderni).
Calcoliamo approssimativamente la probabilità che, scelto casualmente un numero naturale y di n
cifre (in base 10), esso sia primo. Si ha 10n-1 y <10n, quindi i numeri naturali di n cifre sono in
numero di (10n-10n-1)=910n-1, mentre fra di essi i valori primi sono in numero di (10n)-(10n-1);
utilizzando l’approssimazione di (x) mediante x/log(x), la probabilità che y sia primo è:
[(10n)-(10n-1)]/( 910n-1)  [(10n/log(10n))- (10n-1/log(10n-1))]/(910n-1) =
= [(10n/nlog(10))- (10n-1/(n-1)log(10))]/(910n-1) =
= (9n-10)/[9n(n-1)log(10)]  1/[nlog(10)] (perché (9n-10)  9(n-1)).
Tenendo conto che log(10)2,3, la probabilità che un numero x di n cifre in base 10, scelto
casualmente, sia primo è 1/[2,3n]. Per esempio la probabilità che un numero di 100 cifre in base
10 sia primo è 1/230: se scegliamo casualmente 230 numeri di 100 cifre, statisticamente
dovremmo aspettarci che uno di essi sia primo.
Quindi per trovare un numero primo di un fissato numero n di cifre (in base 10), si può scegliere
casualmente un numero di n cifre, sottoporlo a un “test di primalità” e se il test non è superato
cambiare la scelta del numero, e così via: statisticamente dovrebbero bastare circa 2,3n tentativi per
trovare un numero primo.