Sunteți pe pagina 1din 10

Lectia 9. Exponențiere rapidă. Algoritmul lui Euclid extins.

Invers modular

Exponențiere rapidă.
Doresc să pun în evidenţă o modalitate simplă de calcul a valorii expresiei xn, unde x este un
număr real, iar n este număr natural. O primă variantă, cea care se învaţă odată cu
instrucţiunile repetitive, este bazată pe relaţia:

xn = x * x * ... * x, adică înmulţire repetată de n ori a lui x

Funcţia care realizează acest lucru este:

double pow (double x, int n)


{
double p = x ;
for (int i = 1 ; i < n ; i++)
p *= x ;
return p ;
}

Există deja o funcție pow definită în biblioteca cmath, dar dacă definiți funcția de mai sus
înainte de main este OK. Compilatorul va ști că nu vă referiți la cea din biblioteca respectivă.

Că tot vorbeam de funcția pow din cmath. Este periculoasă, deoarece lucrează cu tipul double,
așa că uneori un apel de genul pow (10, 2) poate returna 99. Prefer să-mi definesc propria
funcție pentru ridicarea la putere decât să o folosesc pe asta.

Există însă o soluţie mai rapidă, bazată pe relaţiile următoare:

• dacă n este impar, atunci xn = xn-1 * x


• dacă n este par, atunci xn = xn/2 * xn/2

În acest fel, numărul total de operaţii de înmulţire va scădea semnificativ. De exemplu pentru
calculul lui x13 se vor efectua numai 5 înmulţiri, iar x100 doar 8 înmulţiri. Funcţia este
următoarea:

double pow(double x, int n){


double p = 1 ;
while (n > 0)
{
if (n & 1) // n este impar
{
p *= x;
n-- ;
}
x = x * x ;
n >>= 1 ; // sau n = n / 2
}
return p ;
}
Lectia 9. Exponențiere rapidă. Algoritmul lui Euclid extins. Invers modular

Varianta recursiva a acestei functii este:

double pow(double x, int n) {


if (!n) return 1;
if (n % 2)
return x * pow(x * x, n / 2);
return pow(x * x, n / 2);
}

Știm deja că acest algoritm are complexitatea O(log2n), dar hai să o și demonstrăm. De
exemplu, pentru apelul pow(3, 618), lanțul de apeluri recursive arată cam așa:

pow(3, 618)
pow(3^2, 309)
3^ 2 * pow(3^4, 154)
3^ 2 * pow(3^8, 77)
3^ 10 * pow(3^16, 38)
3^ 10 * pow(3^32, 19)
3^ 42 * pow(3^64, 9)
3^106 * pow(3^128, 4)
3^106 * pow(3^256, 2)
3^106 * pow(3^512, 1)
3^618 * pow(3^1024, 0)

Se observă că, la fiecare pas al doilea parametru se înjumătățește, exact cum la căutarea binară, la
fiecare pas se înjumătățește intervalul de căutare. Numărul de pași făcuți de exponențierea
logaritmică este [log2n]+1. Acel +1 vine de la apelul final, când se returnează 1. Iată deci, cum
pentru 618 se efectuează doar 11 apeluri, pe când prin ridicarea la putere obișnuită, s-ar fi executat
618 pași.

Ei bine, de obicei, pentru exponenți suficienți de mari ca să se simtă diferența dintre cele două
variante de ridicare la putere, rezultatul final ar depăși long long int-ul. De aceea, de cele
mai multe ori, se cere doar restul lui modulo un anumit număr prim. Aplicând regulile clasice
de aritmetică modulară, o funcție ce calculează acest rest arată așa:

long long int pow(long long int x, long long int n) {


if (!n)
return 1;
if (n % 2)
return x * pow(x * x % MOD, n / 2) % MOD;
return pow(x * x % MOD, n / 2) % MOD;
}

Generarea rapidă a șirurilor recurente prin exponențiere logaritmică

Cele mai importante două aplicații ale exponențierii rapide sunt calcularea inversului modular
și generarea celui de-al n-lea termen dintr-un șir ce se bazează pe o recurență liniară, ambele,
desigur, în timp logaritmic.
Lectia 9. Exponențiere rapidă. Algoritmul lui Euclid extins. Invers modular

Voi lua ca exemplu șirul lui Fibonacci, care este definit astfel:

Pentru a genera al n-lea termen din șirul lui Fibonacci (tot modulo un număr dat, pentru că
numerele lui Fibonacci cresc foarte repede), ar trebui, în mod obișnuit, să generăm mai întâi
toți termenii mai mici decât n. Complexitatea este O(n). Există totuși o soluție mult mai bună,
care necesită niște matematică de a 11-a, ce se bazează pe înmulțirea matricelor. Voi da mai
întâi soluția și o explic după. Am notat al n-lea termen din șirul lui Fibonacci cu f(n).

Folosim inducție matematică. Presupunând că ceea ce am scris mai sus este adevărat, demonstrăm
că:

Păi, respectând regulile de înmulțire a matricelor, obținem următoarele relații:

Care, se vede clar că sunt adevărate. Ei bine, știind că înmulțirea matricelor este asociativă, putem
folosi exponențierea logaritmică pentru ridicarea matricei de mai sus la puterea n. Iată o sursă
elegantă, de 100 de puncte, pentru problema Kfib de pe InfoArena (care cere generarea celui de-al
k-lea termen Fibonacci):
#include <fstream>
#define MOD 666013
using namespace std;
ifstream fin("kfib.in");
ofstream fout("kfib.out");
// Struct ce reține o matrice 2x2
struct Mat {
long long int mat[2][2];
};

// Elementul neutru la înmulțirea matricelor 2x2


const Mat nullMat = {
{{1, 0},
{0, 1}}
Lectia 9. Exponențiere rapidă. Algoritmul lui Euclid extins. Invers modular

};

// Matricea pe care o ridicăm la puterea k


const Mat initMat = {
{{1, 1},
{1, 0}}
};

int k;

/// Funcție ce calculează a * b


Mat prod(Mat a, Mat b) {
Mat ret;
ret.mat[0][0] = (a.mat[0][0] * b.mat[0][0] + a.mat[0][1] * b.mat[1][0]) % MOD;
ret.mat[0][1] = (a.mat[0][0] * b.mat[0][1] + a.mat[0][1] * b.mat[1][1]) % MOD;
ret.mat[1][0] = (a.mat[1][0] * b.mat[0][0] + a.mat[1][1] * b.mat[1][0]) % MOD;
ret.mat[1][1] = (a.mat[1][0] * b.mat[0][1] + a.mat[1][1] * b.mat[1][1]) % MOD;
return ret;
}

/// Funcție recursivă pentru calcularea mat^n


Mat pow(Mat mat, int n) {
if (!n)
return nullMat;
if (n & 1) // n % 2
return prod(mat, pow(prod(mat, mat), n >> 1)); // n /= 2
return pow(prod(mat, mat), n >> 1); // n /= 2
}

int main() {
fin >> k;
fout << pow(initMat, k).mat[0][1] << '\n';
fout.close();
return 0;
}

Constanta din spatele complexității logaritmice din această problemă este dată de numărul de
înmulțiri pe scalari ce se efectuează la înmulțirea a două matrice. Care, pentru matricele pătratice
este dimensiunea lor la puterea a treia; în cazul nostru 8.

Generalizare

Mai rămâne să găsim generalizarea acestei metode pentru generarea celui de-al n-lea termen al
șirului recurent:
Lectia 9. Exponențiere rapidă. Algoritmul lui Euclid extins. Invers modular

Fără alte introduceri, relația este:

Luați-vă un exemplu mic și veți vedea că este chiar o relație intuitivă; un antrenament bun este
problema Iepuri de pe InfoArena. Recurența asta se poate aplica la fel de bine și la șirul lui Fibonacci.
Însă acolo, întâmplător:

Așa că, următoarele două relații sunt la fel de corecte și de utile:

Am ales-o totuși pe prima, pentru că necesită un pic mai puțin cod.

Probleme recomandate

• Kfib
• Iepuri
• fibgcd
• 2sah
• Sumdiv

Sper că am explicat suficient de clar cum stă treaba cu exponențierea logaritmică și cu șirurile
recurente.

Algoritmul lui Euclid extins.


Acest algoritm poate fi extins, in sensul gasirii x si y astfel incat a * x + b * y = d. In acest
articol vom incerca sa deducem modul de calculare al lui x si y. Cei grabiti sau certati cu
Lectia 9. Exponențiere rapidă. Algoritmul lui Euclid extins. Invers modular

matematica pot sari direct la codul sursa, dar vor avea probleme in a tine minte algoritmul pe
viitor.

Vom extinde procedura recursiva de calculare a cmmdc pentru a include si x si y. Calculam x


si y incepand de la "capatul recurentei". Daca b = 0, atunci a * 1 + b * 0 = a(cmmdc) evident,
asa ca initial x = 1 si y = 0. Incercam sa calculam x, y in functie de x0, y0 obtinuti pentru b, a
% b. Noi stim urmatoarele:

• b * x0 + (a % b) * y0 = d
• a*x+b*y=d

Trebuie sa aflam o solutie pentru x si y. Vom nota ca mai sus parte intreaga din a / b cu c.

• b * x0 + (a - b * c) * y0 = a * x + b * y
• b * (x0 - c * y0 - y) = a * (x - y0)

O solutie este acum evidenta (Una, sunt o infinitate de perechi x, y)

• x0 - c * y0 - y = 0, De unde rezulta y = x0 - c * y0
• x - y0 = 0, De unde rezulta x = y0

Acum nu mai pare asa de "magic", nu?

Sursa modificata pentru a calcula si x si y nu este mult mai complexa. Acum intelegeti de ce
am trimis d ca pointer mai sus, si de ce am folosit varianta recursiva a algoritmului lui euclid.
Implementat iterativ, este nevoie de un vector care sa tina toate valorile c (a / b) obtinute pe
parcurs.

void euclid(int a, int b, int *d, int *x, int *y) {


if (b == 0) {
*d = a;
*x = 1;
*y = 0;
} else {
int x0, y0;
euclid(b, a % b, d, &x0, &y0);
*x = y0;
*y = x0 - (a / b) * y0;
}
}

Si iterativ:

void euclidIter(int a, int b, int* d, int* x, int* y) {


int x1, y1, tmp;
*x = 1;
*y = 0;
x1 = 0;
y1 = 1;
Lectia 9. Exponențiere rapidă. Algoritmul lui Euclid extins. Invers modular

while (b) {
int q = a / b;
tmp = b;
b = a - q * b;
a = tmp;

tmp = *x;
*x = x1 - q * *x;
x1 = tmp;

tmp = *y;
*y = y1 - q * *y;
y1 = tmp;
//cout<< a<<" "<< b<<" "<< *x<<" "<< *y<<" " << x1<<" " y1<<"\n";
}
*d = a;
*x = y1;
*y = x1;
}

Invers modular

Aplicatie:

Aplicatia invers modular de pe infoarena

Se dau doua numere A si N, cu 1 ≤ A ≤ N-1, prime intre ele (cel mai mare divizor
comun al lor este 1). Sa se determine X intre 1 si N-1 astfel incat A * X sa fie congruent cu 1,
modulo N (restul impartirii lui A * X la N sa fie 1). Numarul X se va numi inversul modular al
lui A.

Solutii:

Varianta bruta
Un algoritm evident ar fi incercarea tuturor numerelor X intre 1 si N-1 si verificarea proprietatii din
enunt pentru X.
Lectia 9. Exponențiere rapidă. Algoritmul lui Euclid extins. Invers modular

Algoritmi optimi

Varianta 1
O complexitate mai buna se obtine folosind teorema lui Euler, din care avem ca
, unde reprezinta numarul numerelor mai mici decat N si prime cu N.
Cum rezulta ca este inversul modular al lui A.

Solutia problemei va fi deci . Putem folosi exponentierea in timp


logaritmic pentru a calcula aceasta putere in complexitate O(log2N). In plus, putem calcula in
complexitate . Pentru cazul particular cand N este prim, , deci raspunsul
N-2
va fi A (dupa cum reiese si din mica teorema a lui Fermat).
#include <fstream>
using namespace std;
long long Phi (long long n ) {
long long val, p ;
p = 2 ;
val = n ;
while (n > 0 && p * p <= n ) {
if (n % p == 0 ) {
val = val * ( p - 1 ) / p ;
while (n % p == 0 )
n /= p ;
}
p++;
}
if (n > 1 )
val = val * ( n - 1 ) / n ; /// a mai ramas un nr. prim
return val ;
}

long long Putere (long long a, long long n, long long mod ) { /// a ^ n
long long prod ;
prod = 1 ;
while (n > 0 ) {
if (n % 2 == 1 ) {
prod = ( (prod % mod) * (a % mod) ) % mod ;
n--;
} else {
a = (a % mod) * (a % mod) % mod ;
n /= 2 ;
}
}
return prod ;
Lectia 9. Exponențiere rapidă. Algoritmul lui Euclid extins. Invers modular

int main() {
ifstream fin ("inversmodular.in") ;
ofstream fout("inversmodular.out") ;
long long n, a ;
fin>>a>>n;
fout<<Putere(a, Phi(n)-1, n) ;
return 0;
}

Varianta 2
Complexitatea optima pentru determinarea inversului modular este O(log2N). Putem folosi
principiul extins al lui Euclid: oricare ar fi A si N numere intregi exista doua numere X si Y de
asemenea intregi astfel incat A * X + N * Y = cmmdc(A, N). Cum in problema determinarii inversului
modular avem cmmdc(A, N) = 1, exista X si Y astfel incat A * X + N * Y = 1.

Considerand ecuatia modulo N, deoarece N * Y este divizibil cu N, avem A * X congruent cu 1


(modulo N), deci X este inversul modular pentru A. Coeficientii X si Y pot fi determinati in timp
logaritmic.

Obs! X poate sa fie si negativ, deci trebuie sa adaugam N la X pana cand devine pozitiv.
#include <fstream>
using namespace std;
ifstream fin("inversmodular.in");
ofstream fout("inversmodular.out");
int n,a,i;
long long y,s=3;
void inv(long long &x,long long &y,int a,int b ) {
if (!b) {
x=1;
y=0;
return;
}
inv(x,y,b,a%b);
i=x;
x=y;
y=i-(a/b)*y;
}
int main() {
fin>>a>>n;
inv(s,y,a,n);
if (s<0) fout<<s%n+n; /// In loc de while(x<0) x+=n;
else fout<<s;
return 0;
}
Lectia 9. Exponențiere rapidă. Algoritmul lui Euclid extins. Invers modular

Alte aplicatii ce folosesc notiunile prezentate mai sus se regasesc in urmatoarele probleme
infoarena :

• Functii
• Jap2

S-ar putea să vă placă și