Sunteți pe pagina 1din 37

OPERATORI PE BITI

Operațiile pe biți sunt operații foarte eficiente, deoarece ele lucrează direct cu biții din
reprezentările în memorie ale operanzilor. Înțelegerea lor presupune înțelegerea reprezentării în
memorie a datelor întregi.

Reprezentarea în memorie a valorilor întregi

Valorile întregi se reprezintă în memorie ca o secvență de biți (cifre binare, 0 și 1). Acestă
secvență poate avea 8, 16, 32 sau 64 de biți.

Reprezentarea în memorie a datelor de tip întreg se face în mod similar pentru toate tipurile cu
semn (char, short int, int, long long int) și similar pentru toate tipurile fără semn
(unsigned char, unsigned short int, unsigned int, unsigned long long
int).

În exemplele care urmează vom folosi tipurile reprezentate pe 16 biți: unsigned short
int, respectiv short int.

Reprezentarea în memorie a valorilor de tip unsigned short int

Tipul unsigned short int memorează valori mai mari sau egale cu 0. Acestea se
reprezintă în memorie astfel:

 se transformă numărul în baza 2 și se memorează, adăugând la început cifre de 0


nesemnificative, atâtea câte sunt necesare până la completarea celor 16 biți.
 dacă reprezentarea în baza 2 a numărului are mai mult de 16 cifre, se vor memora numai
ultimele 16 cifre – numărul se va trunchia.

Astfel, valorile fără semn care se pot reprezenta pe 16 biți sunt cuprinse între 0 și 216-1, adică
0 și 65535.

 0 se reprezintă 0000000000000000
 65535 se reprezintă 1111111111111111
 5 se reprezintă 0000000000000101
 133 se reprezintă 0000000010000101

Reprezentarea în memorie a valorilor de tip short int

Tipul short int memorează atât valori pozitive, cât și valori negative. Astfel, dintre cei 16
biți disponibili, cel mai din dreapta (numit bit de semn) stabilește semnul numărului. Dacă acest
bit este 0, numărul este pozitiv, dacă acest bit este 1, numărul este negativ. Astfel, se pot
memora 32768 valori negative, de la -32768 la -1, și 32768 pozitive sau zero, de la 0 la
32767.

Modalitatea de reprezentarea în memorie a întregilor se numește cod complementar.

Reprezentarea numerelor pozitive se face exact ca mai sus: se transformă numărul în baza 2 și se
completează cu zerouri nesemnificative. Nu la fel se face reprezentarea numerelor întregi
negative. Această reprezentare se face conform pașilor următori:

1. se determină reprezentarea în memorie a numărului ce reprezintă valoarea absolută a


numărului inițial. Aceasta are bitul de semn 0.
2. se determină complementul față de 1 a reprezentării de la pasul anterior – fiecare bit 1
devine 0 și fiecare bit 0 devine 1.
3. se adună 1 la valoarea obținută

De exemplu, pentru reprezentarea în memorie a numărului -133 (considerat de tip short


int) se procedează astfel:

1. se determină reprezentarea în memorie a lui 133 și se obține:


0000000010000101
2. se obține complementul față de 1:
1111111101111010
3. se adună 1 și se obține:
1111111101111011
Mecanismul de memorare numerelor este același pentru toate tipurile întregi. Diferă numai
numărul de biți folosiți pentru reprezentare și implicit intervalul din care fac parte valorile
reprezentate.

Operatori pe biți

Operațiile pe biți se aplică numai datelor de tip întreg, și presupun manipularea directă a biților
din reprezentarea în memorie a operanzilor.

Operatorul de negație ~

Este un operator unar care are ca rezultat numărul obținut prin complementarea față de 1 a biților
din reprezentarea numărului inițial (biții 0 devin 1, biții 1 devin 0).

Exemplu:

~ 133 == -134

Reprezentarea lui 133 este 0000000010000101. Prin complementare se obține


1111111101111010. Aceasta este reprezentarea în memorie a lui -134.

Pentru a verifica, îl reprezentăm conform celor de mai sus pe -134:

1. reprezentarea lui 134 este 0000000010000110


2. prin complementare se obține 1111111101111001
3. adunăm 1 și obținem 1111111101111010

Operatorul de conjuncție biți &

Este un operator binar care are ca rezultat numărul obținut prin conjuncția fiecărei perechi de biți
ce apar în reprezentare în memorie a operanzilor:

0 & 0 == 0
0 & 1 == 0
1 & 0 == 0
1 & 1 == 1

Exemplu:

Să calculăm 13 & 151.

Reprezentarea lui 13 este 0000000000001101. Reprezentarea lui 151 este


0000000010010111:

0000000000001101 &
0000000010010111

Se obține:

0000000000000101, adică 5

Deci: 13 & 151 == 5

Operatorul de disjuncție pe biți |

Este un operator binar care are ca rezultat numărul obținut prin disjuncția fiecărei perechi de biți
ce apar în reprezentare în memorie a operanzilor:

0 | 0 == 0
0 | 1 == 1
1 | 0 == 1
1 | 1 == 1

Exemplu:

Să calculăm 13 | 151.

Reprezentarea lui 13 este 0000000000001101. Reprezentarea lui 151 este


0000000010010111:
0000000000001101 |
0000000010010111

Se obține:

0000000010011111, adică 159

Deci: 13 | 151 == 159

Operatorul de disjuncție exclusivă ^

Este un operator binar care are ca rezultat numărul obținut prin disjuncția exclusivă fiecărei
perechi de biți ce apar în reprezentare în memorie a operanzilor:

0 ^ 0 == 0
0 ^ 1 == 1
1 ^ 0 == 1
1 ^ 1 == 0

Exemplu:

Să calculăm 13 ^ 151.

Reprezentarea lui 13 este 0000000000001101. Reprezentarea lui 151 este


0000000010010111:

0000000000001101 ^
0000000010010111

Se obține:

0000000010011010, adică 159 2 8 16 128

Deci: 13 | 151 == 154


Operatorul de deplasare spre stânga – shift left <<

Este un operator binar care are ca rezultat numărul obținut prin deplasare spre stânga a biților din
reprezentarea în memorie a primului operand cu un număr de poziții egal cu al doilea operand.

Să calculăm 13 << 3.

Reprezentarea lui 13 este 0000000000001101. Deplasând toți biții spre stânga cu 3 poziții se
obține: 0000000001101000, adică 104.

Să observăm că 104 este egal cu 13 * 23. În general n << k este n * 2k.

Pentru a calcula 2n putem folosi operația 1 << n.

Operatorul de deplasare spre dreapta – shift left >>

Este un operator binar care are ca rezultat numărul obținut prin deplasare spre dreapta a biților
din reprezentarea în memorie a primului operand cu un număr de poziții egal cu al doilea
operand.

Să calculăm 133 >> 3.

Reprezentarea lui 133 este 0000000010000101. Deplasând toți biții spre dreapta cu 3 poziții
se obține: 0000000000010000 adică 16.

Să observăm că 16 este egal cu 133 / 23. În general n >> k este n / 2k.

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. Generalizearea se obţine imediat, înlocuind 23 cu 2k.

cin >> n ;
cout << "Catul este : " << (n >> 3) ;
cout << "Restul este : " << (n & 7) ;

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:

int count(long n) {

int num = 0;

for (int i = 0; i < 32; i++)


if (n & (1 << i)) num++;

return num;

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:

int count(long n) {

int num = 0;

if (n)

do num++; while (n &= n - 1);

return num;

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:

int parity(long n) {

int num = 0;

for (int i = 0; i < 32; i++)

if (n & (1 << i)) num ^= 1;

return num;

int parity(long n) {

int num = 0;

if (n)

do num ^= 1; while (n &= n - 1);

return num;

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:

int parity(long n) {

n = ((0xFFFF0000 & n) >> 16) ^ (n & 0xFFFF);

n = ((0xFF00 & n) >> 8) ^ (n & 0xFF);

n = ((0xF0 & n) >> 4) ^ (n & 0xF);

n = ((12 & n) >> 2) ^ (n & 3);

n = ((2 & n) >> 1) ^ (n & 1);

return n;

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:

int low1(long n) {

return n ^ (n & (n - 1));

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.
int high1(long n) {

long num = 0;

if (!n) return -1;

if (0xFFFF0000 & n) {

n = (0xFFFF0000 & n)>>16;

num += 16;

if (0xFF00 & n) {

n = (0xFF00 & n) >> 8;

num += 8;

if (0xF0 & n) {

n = (0xF0 & n) >> 4;

num += 4;

if (12 & n) {

n = (12 & n) >> 2;

num += 2;
}

if (2 & n) {

n = (2 & n) >> 1;

num += 1;

return 1 << num;

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...
int indexHigh1(long n) {

n = n | (n >> 1);

n = n | (n >> 2);

n = n | (n >> 4);

n = n | (n >> 8);

n = n | (n >> 16);

return count(n) - 1;

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
int isTwoPower(long n) {

return ( n & (n - 1) ) == 0 ;

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...

int count(long n) {

return num[n & 0xFFFF] + num[n >> 16];

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...

int parity(long n) {

return par[n & 0xFFFF] ^ par[n >> 16];

}
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...

int low(long n) {

if (l[n & 0xFFFF]) return l[n & 0xFFFF];

else return l[n >> 16] << 16;

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...

int high(long n) {

if (h[n >> 16]) return h[n >> 16] << 16;

else return h[n & 0xFFFF];

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...

int indexHigh(long n) {
if (iH[n >> 16] != -1) return iH[n >> 16] + 16;

else return iH[n & 0xFFFF];

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...

pentru fiecare i

pentru fiecare j > i

L = M[i] ^ M[j];

sfârşit pentru

sfârşit pentru

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:

define maxsize 10000000

char at[maxsize]; // vector de valori logice în care numerele prime vor fi marcate cu 0

int n;

int isPrime(int x) {

return (!at[x]) ;

void sieve() {

int i, j;

memset(at,0,sizeof(at)) ;

for (i = 2; i <= maxsize; i++) if (!at[i])


for (j = i + i; j <= maxsize; j += i)

at[j] = 1; // marcăm toţi multipli lui i ca fiind numere prime

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...

#define maxsize 10000000

char at[maxsize];

int n;

int isPrime(int x) {

return (!at[x]) ;

void sieve() {

int i, j;

memset(at,0,sizeof(at));

for (i = 4; i <= maxsize; i += 2)

at[i] = 1;

for (i = 3; i <= maxsize; i += 2) if (!at[i])

for (j = i + i + i; j <= maxsize; j += i + i)


at[j] = 1;

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...

void sieve() {

int i, j;

memset(at,0,sizeof(at));

for (i = 4; i <= maxsize; i += 2)

at[i] = 1;

for (i = 3; i <= maxsize; i += 2) if (!at[i])

for (j = i * i; j <= maxsize; j += i + i)

at[j] = 1;

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.

#define maxsize 10000000

char at[maxsize / 16];


int n;

int isPrime(int x) {

if (!(x & 1))

if (x == 2) return 0 ;

else return 1 ;

else return (at[((x - 3) >> 1) >> 8] & (1 << (((x - 3) >> 1) & 7))) ;

void sieve() {

int i, j;

memset(at, 0, sizeof(at)) ;

for (i = 3; i <= maxsize; i += 2)

if (!at[((i - 3) >> 1) >> 8] & (1 << (((i - 3) >> 1) & 7)))

for (j = i * i ; j <= maxsize ; j += i + i)

at[((i - 3) >> 1) >> 8] |= (1 << (((i - 3) >> 1) & 7))) ;

COMENTURI DE PE INFOARENA

La algoritmul care numără biții unui număr cred că trebuie să faci un do-while sau să începi
numărătoarea de la 1, pentru că va returna numărul de biți-1. De exemplu, pentru 2, el când face 2&1 =
0, iese și nu mai incrementează rezultatul.

sau returnezi num + 1


N-ar strica prezentarea caluclarii minimului si maximului folosind operatii pe bits.

Cod:

inline int min( int x, int y )


{
return y^( (x^y) & - ( x<y) );
}
inline int max( int x, int )
{
return x^( (x^y) & -(x<y) );
}
Sa luam y^( (x^y) & -(x<y) );
Daca x < y atunci y^( x^y ) & -0 = y^x^y=x.
Altfel y^( ( x^y) & 0 )=y.

Si de ce ai folosi functiile alea de minim / maxim daca sunt mai lente decat cele normale?
Unde e optimizarea?

Cod:
#include <cstdio>
#include <ctime>
#include <cstdlib>

inline int min(int x, int y)


{
return y ^ (( x ^ y) & - (x < y) );
}

inline int min2(int x, int y)


{
if(x < y) return x;
return y;
}

int main()
{
srand(time(0));

double start, end;

start = clock();

int i, n = 20000, j,s = 0;


for(i = 1; i <= n; ++i)
for(j = 1; j <= n; ++j)
s += min(i, j);
printf("%d\n", s);

end = clock();

printf("Time minim: %lf\n", (end - start) /(double)CLOCKS_PER_SEC);

start = clock();

s = 0;

for(i = 1; i <= n; ++i)


for(j = 1; j <= n; ++j)
s += min2(i, j);
printf("%d\n", s);

end = clock();

printf("Time minim normal: %lf\n", (end - start) / (double)


CLOCKS_PER_SEC);
return 0;
}
Ianuarie 29, 2010, 17:08:01 alexandru •alexandru92

La mine is mai rapizi decat algoritmul obisnuit

Ianuarie 29, 2010, 17:09:09 Mircea Dima •blasterz

Pai probabil ca nu stii sa testezi calumea


Testeaza cu sursa mea de mai sus.

Ianuarie 29, 2010, 17:18:43 alexandru •alexandru92

Citat din mesajul lui: Mircea Dima din Ianuarie 29, 2010, 17:09:09

Pai probabil ca nu stii sa testezi calumea


Testeaza cu sursa mea de mai sus.

Am verificat sub linux. Folosind time din bash

Ianuarie 29, 2010, 17:19:14 Mircea Dima •blasterz

Si? (PS. si eu tot pe linux)

Posteaza codul folosit la testare...

Ianuarie 29, 2010, 17:32:28 Andrei Misarca •Mishu91


Acuma văzui că pot modifica și eu.
Totuși, această facilitate cred că ar trebui eliminată, ca să nu modifice fiecare cum îl taie capul,
indiferent dacă este corect sau nu.

alexandru •alexandru92

Citat din mesajul lui: Mircea Dima din Ianuarie 29, 2010, 17:19:14

Si? (PS. si eu tot pe linux)


Posteaza codul folosit la testare...

Cod:

#include <cstdio>

/*
*
*/
using namespace std;
inline unsigned int min( unsigned int x, unsigned int y )
{
if( x < y )
return x;
return y;
}
int main()
{
unsigned int i, j, n=2000000, m=0;
for( i=0, j=n; i <= n; ++i, --j )
m+=min( i, j );
printf( "%u", m );
return 0;
}
Ianuarie 29, 2010, 18:08:53 Mircea Dima •blasterz

Pai am marit n-ul la 200 000 000 si se simte diferenta... metoda normala ia 0.328 sec iar metoda pe biti
0.578.
:-" Ti-am zis ca nu stii sa testezi.

Si chiar si cu n = 2 000 000


metoda ta ia 0.013 iar cea clasica 0.008.

Testat cu time pe linux


Pai tocmai asta e ideea la wiki, sa poata modifica oricine, orice. Daca se observa ca a modificat prost, se
poate reveni la orice versiune anterioara a paginii.

Februarie 11, 2010, 08:10:25 nash mit •nash


Uite aici se vede optimizarea Mircea, tu nu faci testarea cum trebuie... rezultatele pe care le ai tu acolo
sunt datorate modului in care face compilatorul optimizarile... practic tu ai o secventa constanta...
procesorul stie sa optimizeze secventele astea... si rezultatul este acela pe care il vezi... eu am adaugat
un vector de elemente pentru minimuri... facut radom... rezultatele sunt :

The Debugger has exited with status 0.


[Session started at 2010-02-11 08:40:33 +0200.]
-1367706278
Time minim: 4.436630
-1367706278
Time minim normal: 5.624118

Practic ar merge mai repede sa faci min/max si daca le-ai face prin adunari si scaderi (ex: min(a,b) = (a+b
- abs(a-b) )/2 ) pentru ca if-ul spage intr-un fel secventa de cod... si face niste sarituri la nivel de
asamblare in contrast cu metodele aratate ( pe biti, ce am zis eu etc. ) care se sparg in secvente de
instructiuni succesive fara salturi... asta e si motivul pentru care merge mai repede ce face acolo pe biti...

Cod:
#include <cstdio>
#include <ctime>
#include <cstdlib>

inline int min(int x, int y)


{
return y ^ (( x ^ y) & - (x < y) );
}

inline int min2(int x, int y)


{
if(x < y) return x;
return y;
}

int vec[20001];

int main()
{
srand(time(0));

double start, end;

int i, n = 20000, j,s = 0;

for(int i = 1 ; i <= n ; i++ )


vec[i] = rand();
start = clock();

for(i = 1; i <= n; ++i)


for(j = 1; j <= n; ++j)
s += min(vec[i], vec[j]);

printf("%d\n", s);

end = clock();

printf("Time minim: %lf\n", (end - start) /(double)CLOCKS_PER_SEC);

start = clock();

s = 0;

for(i = 1; i <= n; ++i)


for(j = 1; j <= n; ++j)
s += min2(vec[i], vec[j]);
printf("%d\n", s);

end = clock();

printf("Time minim normal: %lf\n", (end - start) / (double)


CLOCKS_PER_SEC);
return 0;
}
Februarie 11, 2010, 09:15:35 Mircea Dima •blasterz

Si la tine e sir constant (practic accesezi aceeasi indici in aceeasi ordine). Si in plus nu spargi cache-ul...de
unde stii ca, compilatorul nu optimizeaza mai bine operatiile pe biti cand nu se sparge cache-ul ?

Uite, eu am facut asa: am declarat un vector int vec[100 000 001]; adica 400 MB. si ii dau min(a[rand() %
nr + 1], a[rand() % nr +1]) . Nu este nici sir constant (ii dau srand(time(0)).

Diferite rulari:

-1368030597
Time minim: 3.600000
-282026249
Time minim normal: 3.570000

163207845
Time minim: 3.570000
-1876227552
Time minim normal: 3.380000
1921398278
Time minim: 3.500000
-1700755686
Time minim normal: 3.800000

605368241
Time minim: 4.180000
842137942
Time minim normal: 4.080000

Cod:
#include <cstdio>
#include <ctime>
#include <cstdlib>

inline int min(int x, int y)


{
return y ^ (( x ^ y) & - (x < y) );
}

inline int min2(int x, int y)


{
if(x < y) return x;
return y;
}

int vec[100000001];

int main()
{
srand(time(0));

double start, end;

int i, n = 5000, j,s = 0, nr = 100000000;

for(i = 1; i <= nr; ++i)


vec[i] = rand();

start = clock();
for(i = 1; i <= 10000000 ; ++i)
s += min( vec[rand() % nr + 1], vec[rand() % nr + 1]);

printf("%d\n", s);
end = clock();

printf("Time minim: %lf\n", (end - start) /(double)CLOCKS_PER_SEC);

start = clock();

s = 0;

for(i = 1; i <= 10000000 ; ++i)


s += min2( vec[rand() % nr + 1], vec[ rand() % nr + 1]);

printf("%d\n", s);

end = clock();

printf("Time minim normal: %lf\n", (end - start) / (double)


CLOCKS_PER_SEC);
return 0;
}
Februarie 12, 2010, 02:08:47 nash mit •nash

Nu, la mine nu este sir constant pentru ca secventa de operatii nu va fi mereu aceasi cand repornesti
programul pentru ca automat nu sunt aceleasi numere cum erau la tine fiind indici de doua for-uri...
Toata secventa aia era constnta. ( secventa de instructiuni era constanta ). La mine la fiecare pornire de
program era alta si atunci compilatorul nu facea nici o optimizare pentru ca vedea ca nu se poate si ai
vazut diferentele .
In al doilea rand eu nu vb de cache... ci de o chestie mult mai simpla... if-ul este implementat ca o
secventa de "goto" la nivel de asamblare ( nu ma intreba de unde stiu asta... citesti/verifici/te informezi
si vezi si tu daca nu sti asamblare ) si face operatie de salt. Practic cand se translateaza apoi in cod
masina in secventa de 0010101... o sa apara o saritura care face sa mai treaca un timp.
Sincer nu stiu ce sa zic despre datele tale... tind sa cred ca esti carcotasi

The Debugger has exited with status 0.


[Session started at 2010-02-12 01:58:14 +0200.]
-1905502094
Time minim: 2.019822
-607096570
Time minim normal: 2.179637

The Debugger has exited with status 0.


[Session started at 2010-02-12 01:58:27 +0200.]
183067316
Time minim: 1.998376
-546390122
Time minim normal: 2.137789
The Debugger has exited with status 0.
[Session started at 2010-02-12 01:58:41 +0200.]
-601500509
Time minim: 1.992697
1901982635
Time minim normal: 2.135167

The Debugger has exited with status 0.


[Session started at 2010-02-12 01:59:04 +0200.]
1385362508
Time minim: 1.999421
-1248609482
Time minim normal: 2.137639

The Debugger has exited with status 0.


[Session started at 2010-02-12 01:59:18 +0200.]
920421738
Time minim: 1.987323
856807656
Time minim normal: 2.125442

The Debugger has exited with status 0.

( testele sunt facute exact pe sursa malale de mai sus ... daca vrei sa evidentieti diferentele si mai
bine maresc numarul de teste )

( pentru dublu numar de calcul de minimuri )

The Debugger has exited with status 0.


[Session started at 2010-02-12 02:10:36 +0200.]
-2006889069
Time minim: 4.066287
849834287
Time minim normal: 4.247599

The Debugger has exited with status 0.


[Session started at 2010-02-12 02:10:58 +0200.]
812755062
Time minim: 3.995999
1018319365
Time minim normal: 4.268772

The Debugger has exited with status 0.


[Session started at 2010-02-12 02:11:10 +0200.]
1922056890
Time minim: 3.992492
1852173117
Time minim normal: 4.265673

The Debugger has exited with status 0.


[Session started at 2010-02-12 02:11:25 +0200.]
548556780
Time minim: 4.034963
1186260194
Time minim normal: 4.262402

The Debugger has exited with status 0.


[Session started at 2010-02-12 02:11:38 +0200.]
1705988173
Time minim: 3.987084
552722841
Time minim normal: 4.251289

The Debugger has exited with status 0.

Februarie 12, 2010, 09:27:44 Mircea Dima •blasterz

De ce folosesti Debugger-ul ? "The Debugger has exited with status 0."

Am dublat si eu numarul la 20000000. De data asta am testat pe un computer sub Windows XP pe


MingW. (data trecuta pe laptop in Ubuntu):

-335291704
Time minim: 2.906000
-271732366
Time minim normal: 2.579000

Nu stiu... ma gandesc ca depinde si de platforma, si de procesor... (stiu ca tu ai mac).

Deci pe bune...asa imi arata...

pe linux am incercat sa compilez in 4 moduri distincte, si da acelasi rezultat (adica minim normal mai
mic)

g++ -O2 -o x sursa.cpp //g++ 4.3


g++ -o x sursa.cpp
g++-4.2 -O2 -o x sursa.cpp
g++-4.2 -lm -static -Wall -fopenmp -pthread -O2 -o x sursa.cpp

Cel mai mare impact il are cand compilez cu g++ -O2 -lm -static -o x sursa.cpp (diferente mai mari de o
secunda intre ele)

Spune-mi cum compilezi tu...


Iar tu cand faci for-urile alea in sursa ta,
for i = 1 la n
for j = 1 la n
tu practic calculezi min( a[ i ], a[ j ] ) si min( a[ j ], a[ i ] )... la asta ma refeream ca e constant...
Eu nu cred ca 5 operatii pe biti sunt mai rapide decat un jump...

Deci... in alta ordine de idei: (da stiu... nu e bine sa postez consecutiv...dar am obosit sa tot dau
"modifica")

Pe noi ne intereseaza daca una din cele 2 functii scrise in C++ (nu in asm sau altceva) optimizeaza cu
ceva, in special la olimpiade si concursuri...
Raspuns: NU ... nu conteaza pe care le folositi... depinde cum optimizeaza compilatorul, cum se
compileaza (daca se compileaza cu -lm -static, cum e la olimpiade si concursuri, se simte diferenta...in
anumite cazuri).
Pentru programatori in general... conteaza atunci cand se doreste optimizarea la sange... si cred ca e
important sa studiezi care e mai buna din variante... nu cred ca e vreuna din ele strict mai buna.
Eu m-am plictisit de topicul asta...care oricum nu are prea multa importanta.
buna! va scrie un pasionat de informatica din R.Moldova.
M-am apucat de informatica la 27 de ani si acum sunt student (am 29 de ani) la Universitatea Tehnica
din Moldova. Referitor la articolul "operatii pe biti... ciurul lui Eratostene"- care de ltfel e un articol
destul de interesant! Daca nu gresesc asi vrea sa fac unele corectii ale codului, si anume:
#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.}
in sectiunea 17 in loc de if (!at[((i - 3) >> 1) >> 8] & (1 << (((i - 3) >> 1) & 7)))
ar trebui de scris if (!(at[((i - 3) >> 1) >> 3] & (1 << (((i - 3) >> 1) & 7))))
sau chiar if 17.if (!(at[i >> 4] & (1 << ((i >> 1) & 7))))
astfel evitându-se scăderea repetată (x-3) dar cu prețul pierderii primilor trei biți din primul octet
al char-ului at[], astfel fiind necesar de a rezerva cu un octet mai multă memorie decât este necesar.
A doua observație ar fi ca în secțiunea 19.at[((i - 3) >> 1) >> 8] |= (1 << (((i - 3) >> 1) & 7))) ;
la fel aceiași observație , plus că în loc de i trebuie să fie j, adică:
19.at[((j-3) >> 1) >> 3] |= (1 << (((j - 3) >> 1) & 7))) ;
sau cum am mai spus mai sus :19.if (!(at[j >> 4] & (1 << ((j >> 1) & 7))))
O a treia observație ar fi cea din funcția :
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.}
și anume :
int isPrime(int x) {
07.if (!(x & 1))
08.if (x == 2) return 1 ;
09.else return 0 ;
10.else return ( ! (at[((x - 3) >> 1) >> 8] & (1 << (((x - 3) >> 1) & 7))) );
11.}

Codul final ar fi:


#include <stdio.h>
#include <memory.h>
#include <conio.h>
#define maxsize 30000
char sita[maxsize / 16 + 1];
int n;
int isPrime(int );
void sieve();

int main()
{
clrscr();
sieve();
printf("\n n= "); scanf("%d", &n);
for (int i=3; i <= n; i+=2)
if (isPrime(i)) printf("%d este prim\n", i);
else printf("%d nu este prim\n", i);
getch();
return 0;
}
int isPrime(int x)
{
if (x <= 1) return 0;
if (!(x & 1))
if (x == 2) return 1;
else return 0;
else return ( !(sita[x >> 4] & (1 << ((x >> 1) & 7))) );
}
void sieve()
{
int i, j;
memset(sita, 0, sizeof(sita));
for (i=3; i * i <= maxsize; i+=2)
if ( !(sita[i >> 4] & (1 << ((i >> 1) & 7))) )
for (j=i * i; j <= maxsize; j+=i + i)
sita[j >> 4] |= (1 << ((j >> 1) & 7));
}

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