Sunteți pe pagina 1din 12

Operaţii pe biţi

http://campion.edu.ro/arhiva/www/arhiva_2009/papers/paper21.pdf

Lista operatorilor pe biţi: p q p&q p|q p^q


~ - negaţie pe biţi
0 0 0 0 0
& - şi pe biţi (and) 0 1 0 1 1

| - sau pe biţi (or) 1 0 0 1 1

^ - sau exclusiv pe biţi (xor) 1 1 1 1 0

<< deplasare la stânga pe biţi (shift left)


>> deplasare la dreapta pe biţi (shift right)

Să menţionăm că operatorii logici pe biţi se aplică numai operanzilor de tip întreg.


În exemplele următoare, vom considera că numerele sunt reprezentate pe 16 biţi fără semn (adică de tip
unsigned short).

Negaţia pe biţi este operator unar şi are ca efect schimbarea biţilor 0 în 1 şi biţilor 1 în 0. În consecinţă,
dacă a = 0000000000001110(2), atunci ~a = 1111111111110001(2)

Operatorii &, |, ^ sunt operatori binari.


Tabelele operaţiilor sunt următoarele:
Considerând op ca fiind oricare din operatorii &, |, ^, o expresie de forma x op y operează asupra biţilor
operanzilor x şi y.
Astfel, dacă x = 1110(2) şi y = 1101(2) , atunci x & y = 1100(2), x | y = 1111(2), iar x ^ y = 0011(2).
Operatorii de deplasare << şi >>, sunt operatori binari.
Expresia x << i este echivalentă cu expresia x * 2i.
De fapt acest operator elimină cei mai din stânga i biţi ai lui x şi adaugă la dreapta i biţi de 0. De
exemplu, dacă x = 0000000000001110(2), atunci x << 2 înseamnă 0000000000111000(2) = 56 (adică 14 *
4).
Expresia x >> i este echivalentă cu x div 2i (împărţirea întreagă a lui x prin 2i). Operatorul de deplasare la
dreapta elimină cei mai din dreapta i biţi ai lui x şi adaugă la stânga i biţi de 0. De exemplu, dacă x =
0000000000001110(2), atunci x >> 2 înseamnă 0000000000000011(2) = 3 (adică 14 div 4).

Probleme rezolvate
1. Se consideră un număr natural n. Să se verifice dacă n este par sau impar.
Rezolvare: Utilizăm operatorul &. Acesta are rol de testare a biţilor. Dacă n este impar, atunci
reprezentarea sa în baza 2 va avea cel mai din dreapta bit pe 1. De exemplu, n = 13 se scrie în baza 2
ca 1101. Atunci 1101 & 1 = 1. Dacă n este par, atunci cel mai din dreapta bit va fi 0. De exemplu, n = 14
se scrie în baza 2 ca 1110. Atunci 1110 & 1 = 0. Iată că pentru orice număr n, expresia n & 1 furnizează
ca rezultat cel mai din dreapta bit. Putem scrie atunci următoarea secvenţă:
cin >> n ;
if (( n & 1 ) == 1) cout << "Numar impar" ;
else cout << "Numar par" ;
De menţionat faptul că operatorii pe biţi au o prioritate mică, de aceea am preferat o pereche
suplimentară de paranteze în expresia instrucţiunii if. Expresia n & 1 == 1 este interpretată de compilator
ca fiind n & (1 == 1)

2. Se citeşte un număr natural k <= 15. Să se afişeze valoarea 2k.


Rezolvare: Ne bazăm pe operatorul de deplasare la stânga pe biţi. Expresia care furnizează răspunsul
corect este 1 << k. Deci secvenţa va fi:
cin >> k ;
cout << (1 << k) ;

3. Se consideră un număr natural n. Să se determine câtul şi restul împărţirii lui n la 8. Generalizare: să


se determine câtul şi restul împărţirii lui n la un număr care este putere a lui 2.
Rezolvare: Pentru determinarea câtului se utilizează deplasarea la dreapta cu 3 biţi (ceea ce este
echivalent cu împărţirea prin 23). Pentru rest se utilizează expresia n & 7 (unde 7 vine de la 23- 1 ).
Deoarece 7 = 1112 atunci toţi biţii lui n vor fi anulaţi, cu excepţia ultimilor 3 cei mai din dreapta.
Generalizarea se obţine imediat, înlocuind 23 cu 2k.
cin >> n ;
cout << "Catul este : " << (n >> 3) ;
cout << "Restul este : " << (n & 7) ;
Operatorii pe biti

Operatorii la nivel de bit se aplica fiecarui bit din reprezentarea operanzilor intregi, spre deosebire de restul
operatorilor care se aplica valorilor operanzilor.

Din aceasta categorie fac parte operatorii urmatori, care apar in ordinea descrescatoare a prioritatii:
~ complementare
>> deplasare la dreapta
<< deplasare la stanga
& si
^ sau exclusiv
| sau

Operatorul ~ transforma fiecare bit din reprezentarea operandului in complementarul sau -- bitii 1 in 0 si cei 0 in 1.
Operatorii &, ^, | realizeaza operatiile si, sau exclusiv, respectiv sau intre toate perechile de biti corespunzatori ai
operanzilor. Daca b1 si b2 reprezinta o astfel de pereche, tabelul urmator prezinta valorile obtinute prin aplicarea
operatorilor &, ^, |.

b1 b2 b1&b2 b1^b2 b1|b2


0 0 0 0 0
0 1 0 1 1
1 0 0 1 1
1 1 1 0 1

Din tabela de mai sus se observa ca aplicand unui bit:

 operatorul & cu 0, bitul este sters


 operatorul & cu 1, bitul este neschimbat
 operatorul | cu 1, bitul este setat
 operatorul ^ cu 1, bitul este complementat.

Exemplu:

char a,b;

a b ~a !a ~b !b a&b a^b a|b


00000000 00000001 11111111 1 11111110 0 00000000 00000001 00000001
11111111 10101010 00000000 0 01010101 0 10101010 01010101 11111111

In cazul operatorilor de deplasare, care sunt binari, primul operand este cel al carui biti sunt deplasati, iar al doilea
indica numarul de biti cu care se face deplasarea -- deci numai primul operand este prelucrat la nivel de bit:
a<<n
a>>n

La deplasarea la stanga cu o pozitie, bitul cel mai semnificativ se pierde, iar in dreapta se completeaza cu bitul 0.
La deplasarea la dreapta cu o pozitie, bitul cel mai putin semnificativ se pierde, iar in stanga se completeaza cu un bit
identic cu cel de semn.
Cu exceptia cazurilor cand se produce depasire, deplasarea la stanga cu n biti echivaleaza cu inmultirea cu 2 la
puterea n.
Analog, deplasarea la dreapta cu n biti echivaleaza cu impartirea cu 2 la puterea n. Este indicat a se realiza inmultirile
si impartirile cu puteri ale lui 2 prin deplasari, ultimele realizandu-se intr-un timp mult mai scurt.
Exemple:

 Cele doua secvente din tabela conduc la aceleasi rezultate:

int i;
i*=8; i<<=3;
i/=4; i>>=2;
i*=10; i=i<<3+i<<1;

 In tabela urmatoare apar valorile obtinute ( in binar si zecimal ) prin aplicarea operatorilor de deplasare:

char a;
a a<<1 a<<2 a>>1 a<<2
00000001 00000010 00000100 00000000 00000000
1 2 4 0 0
00001110 00011100 00111000 00000111 00000011
14 28 56 7 3
11111111 11111110 11111100 11111111 11111111
-1 -2 -4 -1 -1
11011101 10111010 01110100 11101110 11110111
-35 -70 116(depasire) -18 -9
Mai jos este prezentata o secventa care construieste o masca ce contine n biti de 1 incepand cu pozitia p, spre
dreapta ( bitul cel mai putin semnificativ se considera ca fiind de pozitie 0 )-- secventa poate fi utilizata in rezolvarea
problemei : (Se citesc intregii x,n,p. Sa se afiseze:
-numarul format din n biti incepand de la pozitia p (spre dreapta )
-numarul obtinut prin setarea celor n biti de la pozitia p
-numarul obtinut prin stergerea celor n biti de la pozitia p
-numarul obtinut prin complementarea celor n biti din pozitia p
( cei n biti incepind din pozitia p sunt bitii p,p-1,...,p+1-n ). )

int masca=0, unu=1;


int i,p=5,n=3;

unu<<=p; // bitul 1 este pe pozitia p


for(i=1;i<=n;i++){
masca|=unu;
unu>>=1;
}

4. Se consideră un număr natural n. Să se verifice dacă n este sau nu o putere a lui 2.


Rezolvare: Acesta este o problemă destul de cunoscută. Dacă n este o putere a lui 2, atunci
reprezentarea sa în baza 2 are un singur bit 1, restul fiind 0. O primă idee ar fi deci să numărăm câţi biţi
de 1 are n în baza 2. Dar o soluţie mai rapidă se bazează pe ideea că dacă n este o putere a lui 2, deci
de forma 0000000000100000, atunci n-1 are reprezentarea de forma 0000000000011111, adică bitul 1
s-a transformat în 0, iar biţii de la dreapta sunt acum toţi 1. Deci o expresie de forma n & (n-1) va furniza
rezultatul 0 dacă şi numai dacă n este o putere a lui 2.
cin >> n ;
if ( (n & (n-1)) == 0 ) cout << "n este putere a lui 2" ;
else cout << "n nu este putere a lui 2" ;

5. Se consideră un număr natural n. Să se afişeze reprezentarea lui n în baza 2.


Rezolvare: Ne bazăm pe faptul că în memorie n este deja reprezentat în baza 2, deci trebuie să-i afişăm
biţii de la stânga la dreapta. Presupunând că n este reprezentat pe 16 biţi, pe aceştia îi numerotăm de la
dreapta la stânga cu numere de la 0 la 15. Pentru a obţine bitul de pe poziţia i (0 <= i <= 15), utilizăm
expresia (n >> i) & 1. Nu rămâne decât să utilizăm expresia pentru fiecare i între 0 şi 15.
cin >> n ;
for (int i=15 ; i >= 0 ; i--)
cout << ((n >> i) & 1) ;
6. Se consideră două numere naturale n şi i (0 <= i <= 15). Să se marcheze cu 1 bitul i al lui n.
Rezolvare: Vom seta valoarea 1 la bitul i, indiferent de valoarea memorată anterior (0 sau 1). Pentru
setare, utilizăm operatorul sau pe biţi. Expresia care realizează aceasta este n | (1 << i) . Să ne amintim
dintr-un exerciţiu anterior că 1 << i înseamnă 2i. Expresia n | 2i nu va modifica decât bitul i care ne
interesează, restul rămânând nemodificaţi, datorită faptului că dacă b este un bit atunci b | 0 este egal cu
b.

7. Se consideră două numere naturale a şi b, ambele cuprinse între 0 şi 255. Se cere să se memoreze
cele două numere într-un întreg n reprezentabil pe 16 biţi fără semn (deci de tip unsigned short).
Rezolvare: Cele două numere a şi b pot fi reprezentate pe 8 biţi. De aceea cei 16 biţi ai lui n sunt
suficienţi. Stocăm a pe primii 8 biţi ai lui n, iar pe b pe ultimii 8 biţi ai lui n. n = a * 256 + b ; De asemenea,
dacă se cunoaşte n, se pot obţine valorile lui a şi b astfel: a = n >> 8 ; // sau a = n / 256
b = n & 255 ; // sau b = n % 256 ;

8. Codificare/decodificare. Considerăm un număr pe care dorim să-l codificăm, apoi să-l decodificăm.
Rezolvare: O modalitate simplă de codificare şi decodificare este utilizarea operatorului pe biţi XOR.
Pentru aceasta considerăm o mască (o parolă) şi ne bazăm pe o proprietate interesantă a operatorului
XOR: a ^ b ^ b = a. Deci a ^ b realizează codificarea lui a, iar (a ^ b) ^ b realizează decodificarea.
Proprietatea se bazează pe faptul că b ^ b = 0, pentru orice b, iar a ^ 0 = a. Metoda poate fi aplicată şi
pentru codificarea unui text utilizând o parolă dată. Cu ajutorul parolei aplicată peste textul iniţial se
obţine codificarea, iar o a doua aplicare a parolei peste codificare se obţine textul iniţial. Secvenţa care
realizează codificarea/decodificarea unui număr este:
int n, masca;
n = 65 ;
masca = 100 ;
cout << "\nValoarea initiala a lui n : " << n ;
n = n ^ masca ;
cout<< "\nValoarea codificata a lui n : " << n ;
n = n ^ masca ;
cout<< "\nValoarea decodificata a lui n : " << n ;
În cadrul acestui articol vă vom prezenta câteva metode prin care programele se pot optimiza dacă utilizăm
eficient operaţiile pe biţi sau folosim operaţii pe biţi în locuri în care, la prima vedere, nu s-ar părea că ar fi
necesare.

De cele mai multe ori, atât în concursuri cât şi în viaţa de zi cu zi a programatorului, atunci când implementăm o
metodă care este folosită de o aplicaţie şi vrem să facem această metodă eficientă ca timp de execuţie, suntem
învăţaţi din şcoală (sau ar trebui sã fim învăţaţi, chiar dacă unii dintre profesori consideră că cel mai important
algoritm învăţat în liceu este backtraking-ul, soluţia tuturor problemelor) să ne uităm la complexitatea algoritmului
implementat şi dacă observăm că în practică algoritmul este mai încet decât ne dorim noi să fie să încercăm să
găsim un algoritm cu un ordin de complexitate mai mic.

Nu trebuie să uităm că ceea ce numim complexitatea unui algoritm este o aproximare a vitezei unui algoritm şi nu
o măsură absolută. Pentru dimensiuni mici ale datelor, câteodată nu se observă diferenţă dintre O(n) şi O(n log
n) sau O(n3/2). Autorului i s-a întâmplat ca la implementarea soluţiei unei probleme care în engleză se numeşte
"Bottleneck Minimal Spanning Tree" să observe că rezolvarea în O(m log m) (folosind algoritmul de găsire a
arborelui parţial de cost minim al lui Kruskal) să fie mai rapidă decât rezolvarea mai laborioasă în O(m) a acestei
probleme. Deci trebuie dată o atenţie egală cu cea acordată complexităţii algoritmului şi dimensiunii factorilor
constanţi care apar.

În continuare vom încerca să găsim soluţii mai rapide decât cele naive pentru unele operaţii de bază (toate
operaţiile vor fi implementate pentru întregi pozitivi reprezentaţi pe 32 de biţi).

Aplicaţia #1
Să se determine numărul de biţi de 1 din reprezentarea binară a lui n.

Rezolvare
Rezolvarea naivă a acestei probleme ar consta în parcurgerea secvenţială a biţilor lui n. În continuare vă prezint
această rezolvare:
neformatat
print?
1.int count(long n) {
2. int num = 0;
3. for (int i = 0; i < 32; i++)
4. if (n & (1 << i)) num++;
5. return num;
6.}

Dacă ne uităm cu atenţie şi analizăm rezultatul operaţiei n & (n - 1) putem obţine o soluţie mai bună. Să luăm
un exemplu:

110111010100002 = n
110111010011112 = n - 1
110111010000002 = n & (n - 1)

Se vede clar de aici că efectul operaţiei n & (n - 1) este anularea celui mai nesemnificativ bit cu valoarea 1.

De aici ideea algoritmului:

neformatat

print?
1.int count(long n) {
2. int num = 0;
3. if (n)
4. do num++; while (n &= n - 1);
5. return num;
6.}

Dar este acest algoritm mai rapid? Am testat pentru toate numerele de la 1 la 224 şi rezultatele au fost 2,654
secunde folosind metoda naivă şi 0.821 folosind a doua metodă. Rezultatul mult mai bun al celei de-a doua
metode se bazează în principal pe faptul că ea execută un număr de paşi egali cu numărul de biţi cu valoarea 1 din
număr, deci în medie jumătate din numărul de paşi efectuaţi de prima metodă.

Aplicaţia #2
Să se determine paritatea numărului de biţi de 1 din reprezentarea binară a unui număr n.

Rezolvare
Din cele prezentate mai sus se pot determina două metode evidente:
neformatat

print?
1.int parity(long n) {
2. int num = 0;
3. for (int i = 0; i < 32; i++)
4. if (n & (1 << i)) num ^= 1;
5. return num;
6.}

neformatat

print?
1.int parity(long n) {
2. int num = 0;
3. if (n)
4. do num ^= 1; while (n &= n - 1);
5. return num;
6.}

Putem obţine o a treia rezolvare făcând câteva observaţii pe un exemplu. Considerăm n = 110110112. Rezultatul
căutat este dat de valoarea 1 ^ 1 ^ 0 ^ 1 ^ 1 ^ 0 ^ 1 ^ 1. Împărţim pe n în partea lui superioară şi partea lui
inferioară: 1 1 0 1 ^ 1 0 1 1 = 0 1 1 0. Aplicăm asupra rezultatului acelaşi procedeu: 0 1 ^ 1 0 = 1 1 şi 1 ^
1 = 0.

Să scriem algoritmul care reiese din acest exemplu, luând în considerare faptul că numărul n este reprezentat pe
32 de biţi:

neformatat

print?
1.int parity(long n) {
2. n = ((0xFFFF0000 & n) >> 16) ^ (n & 0xFFFF);
3. n = ((0xFF00 & n) >> 8) ^ (n & 0xFF);
4. n = ((0xF0 & n) >> 4) ^ (n & 0xF);
5. n = ((12 & n) >> 2) ^ (n & 3);
6. n = ((2 & n) >> 1) ^ (n & 1);
7. return n;
8.}

Deşi constanta multiplicativa este mai mare, numărul de operaţii are ordin logaritmic faţă de numărul de biţi ai
unui cuvânt al calculatorului.

Aplicaţia #3
Să se determine cel mai puţin semnificativ bit de 1 din reprezentarea binară a lui n.

Rezolvare
Rezolvarea naivă se comportă bine în medie, deoarece numărul de cicluri până la găsirea unui bit cu valoarea 1
este de obicei mic, dar din cele discutate mai sus putem găsi ceva mai bun.

Asa cum am arătat n & (n - 1) are ca rezultat numărul n din care s-a scăzut cel mai puţin semnificativ bit.
Folosind această idee obţinem următoarea funcţie:

neformatat

print?
1.int low1(long n) {
2. return n ^ (n & (n - 1));
3.}

Exemplu:

110110002 = n
110101112 = n - 1
110100002 = n & (n - 1)
110110002 = n
000010002 = n ^ (n & (n - 1))

Această funcţie este foarte importantă pentru structura de date arbori indexaţi binar prezentată într-un un articol
mai vechi din GInfo.

Aplicaţia #4
Să se determine cel mai semnificativ bit cu valoarea 1 din reprezentarea binară a lui n.

Rezolvare
Putem aplica şi aici ideile prezentate mai sus: cea naivă şi cea cu eliminarea biţilor, dar putem găsi şi ceva mai
bun.

O abordare ar fi cea a căutării binare (aplicabilă şi în problema anterioară). Verificăm dacă partea superioară a lui
n este 0. Dacă nu este 0, atunci căutăm bitul cel mai semnificativ din ea, iar dacă este, ne ocupăm de partea
inferioară, deci reducem la fiecare pas problema la jumătate.

neformatat
print?
01.int high1(long n) {
02. long num = 0;
03. if (!n) return -1;
04. if (0xFFFF0000 & n) {
05. n = (0xFFFF0000 & n)>>16;
06. num += 16;
07. }
08. if (0xFF00 & n) {
09. n = (0xFF00 & n) >> 8;
10. num += 8;
11. }
12. if (0xF0 & n) {
13. n = (0xF0 & n) >> 4;
14. num += 4;
15. }
16. if (12 & n) {
17. n = (12 & n) >> 2;
18. num += 2;
19. }
20. if (2 & n) {
21. n = (2 & n) >> 1;
22. num += 1;
23. }
24. return 1 << num;
25.}

Aplicaţia #5
Să se determine indexul celui mai semnificativ bit de 1 din reprezentarea binară a lui n.

Rezolvare
Metodele prezentate la rezolvarea problemei 4 pot fi folosite şi aici, dar vom prezenta o nouă rezolvare. Să luăm
următorul şir de operaţii pentru un exemplu:

n = 100000002
n = n | (n >> 1)
n = 110000002
n = n | (n >> 2)
n = 111100002
n = n | (n >> 4)
n = 111111112

Se observă că aplicând o secvenţă asemănătoare de instrucţiuni cu cea de mai sus putem face ca un număr n să
se transforme în alt număr care are un număr de biţi de 1 egal cu 1 plus indexul celui mai semnificativ bit cu
valoarea 1 din n. De aici algoritmul este următorul...

neformatat

print?
1.int indexHigh1(long n) {
2. n = n | (n >> 1);
3. n = n | (n >> 2);
4. n = n | (n >> 4);
5. n = n | (n >> 8);
6. n = n | (n >> 16);
7. return count(n) - 1;
8.}

Pentru ca această metodă să fie eficientă ne trebuie o metodă count eficientă.


Aplicaţia #6
Să se determine dacă n este o putere a lui 2 (întrebare interviu Microsoft).

Rezolvare
neformatat

print?
1.int isTwoPower(long n) {
2. return ( n & (n - 1) ) == 0 ;
3.}

O altă abordare: preprocesarea


Operaţiile prezentate mai sus sunt implementate în limbajele de asamblare, dar câteodată se folosesc algoritmi
naivi şi atunci o implementare inteligentă ajunge să fie mai rapidă decât instrucţiunea din limbajul de asamblare.
Pentru unele supercalculatoare aceste instrucţiuni sunt instrucţiuni ale procesorului.

Se pot găsi alţi algoritmi mai rapizi decât cei de mai sus, dar de obicei găsirea unui algoritm mai inteligent este
mai grea decât folosirea unei abordări banale care aduce un câştig foarte mare: preprocesarea.

În continuare vom rezolva problemele prezentate anterior folosindu-ne de preprocesarea datelor.

Rezolvarea aplicaţiei #1
Determinăm, pentru numerele de la 0 la 216 - 1, un tablou num, în care num[i] este egal cu numărul de biţi de 1
din reprezentarea binară a lui i. De aici obţinem soluţia...

neformatat

print?
1.int count(long n) {
2. return num[n & 0xFFFF] + num[n >> 16];
3.}

Rezolvarea aplicaţiei #2
Determinăm, pentru numerele de la 0 la 216 - 1, un tablou par, în care par[i] este egal cu paritatea numărului
de biţi de 1 din reprezentarea binară a lui i. De aici obţinem soluţia...

neformatat

print?
1.int parity(long n) {
2. return par[n & 0xFFFF] ^ par[n >> 16];
3.}

Rezolvarea aplicaţiei #3
Determinăm, pentru numerele de la 0 la 216 - 1, un tablou l, în care l[i] este egal cu cel mai nesemnificativ bit
de 1 din reprezentarea binară a lui i. De aici obţinem soluţia...

neformatat

print?
1.int low(long n) {
2. if (l[n & 0xFFFF]) return l[n & 0xFFFF];
3. else return l[n >> 16] << 16;
4.}

Rezolvarea aplicaţiei #4
Determinăm, pentru numerele de la 0 la 216 - 1, un tablou h, în care h[i] este egal cu cel mai semnificativ bit de
1 din reprezentarea binară a lui i. De aici obţinem soluţia...

neformatat

print?
1.int high(long n) {
2. if (h[n >> 16]) return h[n >> 16] << 16;
3. else return h[n & 0xFFFF];
4.}

Rezolvarea aplicaţiei #5
Determinăm, pentru numerele de la 0 la 216 - 1, un tablou iH, în care iH[i] este egal cu indexul celui mai
semnificativ bit de 1 din reprezentarea binară a lui i şi este egal cu -1 pentru 0. De aici obţinem soluţia...

neformatat

print?
1.int indexHigh(long n) {
2. if (iH[n >> 16] != -1) return iH[n >> 16] + 16;
3. else return iH[n & 0xFFFF];
4.}

Algoritmi mai serioşi


Deşi ceea ce am prezentat mai sus sunt mai degrabă trucuri de implementare decât algoritmi serioşi şi în general
nu se acordă aşa mare importanţă detaliilor de genul acesta, câteodată asemenea trucuri se pot dovedi importante
mai ales în timpul concursurilor.

Un exemplu ar fi problema rundei 12 de la concursul de programare Bursele Agora. Acolo se cerea găsirea
numărului de dreptunghiuri conţinute de o matrice binară M care au în colţuri 1. Un algoritm de complexitate
O(n2m) ar fi fost următorul: se consideră fiecare pereche formată din două linii şi se numără câte perechi (Mi,k,
Mj,k) pentru care Mi,k = 1 şi Mj,k = 1 există. Fie acest număr x. Atunci numărul de dreptunghiuri care au colţurile
pe aceste două linii va fi x(x - 1) / 2.

Un asemenea algoritm ar fi luat numai 64 de puncte deoarece restricţiile date erau mari (n ≤ 200, m ≤ 2500).
Putem rescrie condiţia M[i][k] == 1 && M[j][k] == 1 ca şi M[i][k] ^ M[j][k] == 1, deci putem modifica
rezolvarea anterioară în modul următor...

neformatat

print?
1.pentru fiecare i
2. pentru fiecare j > i
3. L = M[i] ^ M[j];
4. sfârşit pentru
5.sfârşit pentru
6. numaraBitiUnu(L)

Dacă în loc să păstrăm în M[i][j] un element al matricei binare, păstrăm 16 elemente, atunci în linia L = M[i] ^
M[j] se efectuează cel mult 2500/16 calcule în loc de 2500 de calcule. Acum, ce a rămas de optimizat este metoda
numaraBitiUnu. Dacă folosim preprocesarea, atunci această metodă va fi compusă şi ea din 2500/16 calcule. Se
observă că în acest fel am mărit viteza de execuţie a algoritmului cu un factor de 16.

Alt exemplu ar fi problema rundei 17. În acea problemă se cere determinarea numărului de sume distincte care se
pot obţine folosind nişte obiecte cu valori date folosind fiecare obiect pentru o sumă cel mult o dată. Din nou o
rezolvare directă, folosind marcarea pe un şir de valori logice n-ar fi obţinut punctaj maxim. Pentru obţinerea
punctajului maxim următoarea idee ar fi putut fi folosită cu succes: şirul de valori logice se condensează într-un şir
de întregi reprezentaţi pe 16 biţi, şi acum actualizarea se face asupra a 16 valori deodată. Deci şi aici se câştiga un
factor de viteză egal cu 8 (datorită detaliilor de implementare).

Un al treilea exemplu ar fi problema Viteza de la a 9-a rundă a concursului organizat de .campion anul acesta.
Acolo se cerea determinarea celui mai mare divizor comun a două numere binare de cel mult 10 000 de cifre.
După cum se ştie, complexitatea algoritmului de determinare a celui mai mare divizor comun a două numere este
logaritmică, acest fapt este adevărat atunci când operaţiile efectuate sunt constante, dar în rezolvarea noastră
apar numere mari, deci o estimare a numărului de calcule ar fi maxn * maxn * log maxn (împărţirea are în medie
costul maxn * maxn dacă nu implementăm metode mai laborioase). Acest număr este prea mare pentru timpul de
rezolvare care ne este cerut. Pentru a evita împărţirea, care este operaţia cea mai costisitoare, putem folosi
algoritmul de determinare a celui mai mare divizor comun binar care se foloseşte de următoarele proprietăţi...

1. cmmdc(a, b) = 2 * cmmdc(a / 2, b / 2), dacă a şi b sunt numere pare;

2. cmmdc(a, b) = cmmdc(a, b / 2), dacă a este impar;

3. cmmdc(a, b) = cmmdc(a - b, b), dacã a este impar, b este impar şi a > b.

Aplicând acest algoritm numărul de calcule s-a redus la maxn * log maxn, dar nici acest număr nu este suficient
de mic, şi deci
pentru accelerarea algoritmului folosim ideea utilizată în cele două probleme de mai sus. Împachetăm numărul în
întregi, în pachete de câte 30 de biţi, şi atunci deplasarea la stânga (adică împărţirea la 2) şi scăderea vor fi de 30
de ori mai rapide.

Să încercăm acum rezolvarea unei probleme în care eficienţa este cea mai importantă, mai importantă decât
lizibilitatea codului obţinut. Problema are următorul enunţ...

Se consideră un număr n. Să se determine numerele prime mai mici sau egale cu n, unde n ≤ 10 000 000.

Practic această problemă ne va fi utilă în testarea rapidă a primalităţii numerelor mai mici sau egale cu n.

Putem încerca mai multe rezolvări, dar în practică cea mai rapidă se dovedeşte de obicei cea numită ciurul lui
Eratostene. Implementarea banală a algoritmului ar fi următoarea:

neformatat

print?
01.define maxsize 10000000
02.
03.char at[maxsize]; // vector de valori logice în care numerele prime vor fi marcate
cu 0
04.int n;
05.
06.int isPrime(int x) {
07. return (!at[x]) ;
08.}
09.
10.void sieve() {
11. int i, j;
12. memset(at,0,sizeof(at)) ;
13. for (i = 2; i <= maxsize; i++) if (!at[i])
14. for (j = i + i; j <= maxsize; j += i)
15. at[j] = 1; // marcăm toţi multipli lui i ca fiind numere prime
16.}

Observăm că jumătate din timpul folosit de noi la preprocesare este pierdut cu numerele pare. Marcându-le de la
început vom putea ignora numerele pare în preprocesare...

neformatat

print?
01.#define maxsize 10000000
02.
03.char at[maxsize];
04.int n;
05.
06.int isPrime(int x) {
07. return (!at[x]) ;
08.}
09.
10.void sieve() {
11. int i, j;
12. memset(at,0,sizeof(at));
13. for (i = 4; i <= maxsize; i += 2)
14. at[i] = 1;
15. for (i = 3; i <= maxsize; i += 2) if (!at[i])
16. for (j = i + i + i; j <= maxsize; j += i + i)
17. at[j] = 1;
18.}

La marcarea multiplilor numărului prim i toate numerele până la i * i au fost deja marcate deoarece i * i este
cel mai mic număr care nu este divizibil cu numere naturale mai mici sau egale cu i - 1. Deci, avem o a treia
versiune a programului...

neformatat

print?
01.void sieve() {
02. int i, j;
03. memset(at,0,sizeof(at));
04. for (i = 4; i <= maxsize; i += 2)
05. at[i] = 1;
06. for (i = 3; i <= maxsize; i += 2) if (!at[i])
07. for (j = i * i; j <= maxsize; j += i + i)
08. at[j] = 1;
09.}

Ultima optimizare este optimizarea spaţiului necesar. Într-un tip de date char putem împacheta opt valori logice şi
punând un test suplimentar în metoda isPrime putem elimina şi numerele pare, astfel vom avea nevoie de un
spaţiu de 16 ori mai mic.

neformatat

print?
01.#define maxsize 10000000
02.
03.char at[maxsize / 16];
04.int n;
05.
06.int isPrime(int x) {
07. if (!(x & 1))
08. if (x == 2) return 0 ;
09. else return 1 ;
10. else return (at[((x - 3) >> 1) >> 8] & (1 << (((x - 3) >> 1) & 7))) ;
11.}
12.
13.void sieve() {
14. int i, j;
15. memset(at, 0, sizeof(at)) ;
16. for (i = 3; i <= maxsize; i += 2)
17. if (!at[((i - 3) >> 1) >> 8] & (1 << (((i - 3) >> 1) & 7)))
18. for (j = i * i ; j <= maxsize ; j += i + i)
19. at[((i - 3) >> 1) >> 8] |= (1 << (((i - 3) >> 1) & 7))) ;
20.}

Concluzie
Asemenea optimizări ca şi cele prezentate în cadrul acestui articol se pot dovedi foarte utile în unele cazuri, dar ele
fac programul ilizibil, tocmai din acest motiv, asemenea trucuri trebuie aplicate numai în locurile critice ale
codurilor sursă pentru a duce la o creştere semnificativă de viteză a aplicaţiilor şi trebuie documentate foarte bine,
mai ales dacă se doreşte sau este necesar ca alt programator să poată lucra cu codul respectiv mai târziu.

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