Documente Academic
Documente Profesional
Documente Cultură
Obiective
În urma parcurgerii acestui laborator, studentul va fi capabil:
În exemplul anterior performanța se referă la timp (dacă executăm mai puține instrucțiuni într-un program,
ne așteptăm să se termine mai repede). Acest aspect va fi abordat pe larg la materiile AA și PA.
În acest laborator vom vorbi despre altă metrică de măsurare a performanței unui program, mai exact
despre memoria folosită de un program.
De ce este important și acest aspect? Dacă din punct de vedere al timpului de execuție, sunt situații în care
putem aștepta mai mult timp pentru a se termina programul, din punctul de vedere al memoriei folosite avem
o limitare exactă. Un exemplu simplu este calculatorul nostru, care are 4GB/8GB/16GB. Dacă mașina noastră
are X GB RAM, dintre care o parte importantă o ocupă sistemul de operare, asta înseamnă că într-un anumit
program nu putem folosi o cantitate nelimitată de RAM (mai multe detalii la CN2, SO). Pentru simplitate,
momentan presupunem că programul nostru nu poate rula pe o mașină cu X GB, dacă are nevoie de mai mult
de X GB.
Dacă ajungem într-o astfel de situație în mod evident trebuie să schimbăm ceva, însă de multe ori putem păstra
algoritmul și să facem câteva modificări în implementare, care exploatează anumite abilități ale limbajului C
(ex. operații pe biți).
#include <stdio.h>
int main() {
// afiseaza dimensiunea tipurilor si a unor variabile de un anumit tip
char xc;
printf("sizeof(unsigned char) = %ld B\n", sizeof(unsigned char));
printf("sizeof(char) = %ld B\n", sizeof(char));
printf("sizeof(xc) = %ld B\n", sizeof(xc));
int xi;
printf("sizeof(unsigned int) = %ld B\n", sizeof(unsigned int));
printf("sizeof(int) = %ld B\n", sizeof(int));
printf("sizeof(xi) = %ld B\n", sizeof(xi));
return 0;
}
În urma executării acestui program pe o arhitectură de 32 biți (ceea ce folosim la PC) vom vedea următorul
rezultat.
sizeof(unsigned char) = 1 B
sizeof(char) = 1 B
sizeof(xc) = 1 B
sizeof(short int) = 2 B
sizeof(xs) = 2 B
sizeof(unsigned int) = 4 B
sizeof(int) = 4 B
sizeof(xi) = 4 B
sizeof(vc) = 100 B
sizeof(vs) = 200 B
sizeof(vi) = 400 B
Putem afla dimensiunea unui tip de date / unei variabile de un anumit tip la compile time folosind operatorul
sizeof care returnează dimensiunea în bytes a parametrului dat.
sizeof poate fi folosit și pentru măsurarea dimesiunii unui vector / matrice alocat(a) static.
Memoria totală folosită de un program poate fi calculată ca suma tuturor dimensiunilor ocupate de variabilele
din program.
De obicei, ne interesează să știm ordinul de mărime al spațiului de memorie alocat, astfel, de cele mai multe
ori, putem contoriza doar tablourile.
Un caz special îl poate reprezenta recursivitatea! Punerea parametrilor pe stivă de un număr foarte mare de ori,
este echivalent cu declararea unui tablou de valori pe stivă. Aceste variabile nu pot fi neglijate în calculul
memoriei!
Operatori pe biți în C
Operatorii limbajului C pot fi unari, binari sau ternari, fiecare având o precedenţă şi o asociativitate bine definite
(vezi lab02).
În tabelul următor reamintim operatorii limbajului C care sunt folosiți la nivel de bit.
Dacă nu sunteți sigur de precendența unui operator, folosiți o pereche de paranteze rotunde în plus în expresia
voastră! Nu exagerați cu parantezele, codul poate deveni ilizibil.
Bitwise NOT
Semnificație
Bitwise AND
Semnificație
Bitwise OR
Semnificație
Bitwise XOR
Semnificație
Bit LOGICAL SHIFT
În C sunt definite doar shiftări logice. Acestea pot fi la stânga (<<) sau la dreapta (>>), reprezentând deplasarea
in binar a cifrelor și completarea pozițiilor “golite” cu zerouri.
LEFT SHIFT
Efectul unei deplasări la stânga cu un rang binar este echivalent cu înmulțirea cu 2 a numărului din baza 10.
Dacă rezultatul nu are loc pe tipul de date folosit, atunci se pot pierde din biți!
x=3 0 1 1
x«1=6 1 1 0
x«2=8 1 0 0
RIGHT SHIFT
Efectul unei deplasări la dreapta cu un rang binar este echivalent cu împărțirea întreagă la 2 a numărului din
baza 10.
x=3 0 1 1
x»1=1 0 0 1
x»2=0 0 0 0
Lucrul cu măști
Având la dispoziție operațiile prezentate mai sus, putem răspunde la următoarele întrebări.
n * … * ? * … *
mask 0 … 0 1 0 … 0 op
x 0 … 0 ? 0 … 0
// is_set
// byte - byte de intrare pentru care vreau sa verific un bit
// i - indexul bitului din byte
// @return - 1, daca bitul este 1
// 0, daca bitul este 0
int is_set(char byte, int i) {
int mask = (1 << i);
return (byte & mask) != 0;
}
...
if (is_set(mybyte, i)) {
printf("bitul %d din byteul %d este setat!\n", i, mybyte);
} else {
printf("bitul %d din byteul %d NU este setat!\n", i, mybyte);
}
...
Explicație pas cu pas
Această întrebare ne oferă valoarea bitului i.
Dacă “valoarea este 1”, atunci vom spune că “bitul este setat”.
Dacă “valoarea este 0”, atunci vom spune că “bitul nu este “setat”.
Pentru a verifica valoarea bitului i din numărul n, practic noi ar trebui să privim numărul astfel:
b7b7 b6b6 … bibi … b1b1 b0b0
n * * … ? … * *
unde * înseamnă don't care (de la PL), iar ? este valoarea pe care o cautăm.
Deci am vrea să facem, după cum am zis mai sus, o operație de tipul “scoate” doar bitul i din număr, iar în rest
lasă 0 (pentru a evidenția bitul nostru).
n * … * ? * … *
n op mask 0 … 0 ? 0 … 0
op este o operație, iar mask un număr. Să analizăm cine pot fi op și biții din mască (mimi).
Dorim ca:
1 este elementul neutru pentru ȘI, ceea ce verifică ? & 1 = ? , oricare are fi ? un bit
0 este elementul care poate “șterge” un bit prin Și, ceea ce verifică * & 0 = 0 , oricare ar fi * un bit
Cum setăm (valoarea devine 1) bitul i dintr-un număr n?
Setarea bitului:
mask 00 … 00 11 00 … 00 op
explicație:
orice valoare are avea bitul ∗∗ va fi suprascris cu 1
ceilalti biți vor fi copiați
Implementare C
// set
// byte - byte de intrare pentru care vreau sa setez un bit
// i - indexul bitului din byte
// @return - noul byte
char set(char byte, int i) {
int mask = (1 << i);
return (byte | mask);
}
...
mybyte = set(mybyte, i);
...
op este o operație, iar mask un număr. Să analizăm cine pot fi op și biții din mască (mimi).
Dorim ca:
∗ op mi=1∗ op mi=1, adică operația op aplicată pe ∗∗ (orice) și mimi, va avea mereu ca rezultat pe 11
nj op mj=njnj op mj=nj (unde i != j), adică operația opop aplicată pe njnj și mjmj, va da njnj
Observăm că:
1 este elementul care poate “umple” un bit prin SAU, ceea ce verifică ∗|1=1∗|1=1, oricare ar fi * un bit
0 este elementul neutru pentru SAU, ceea ce verifică nj|0=njnj|0=nj, oricare are fi njnj un bit
Cum resetăm (valoarea devine 0) bitul i dintr-un număr n?
Resetarea bitului:
pas 1 / 1: se aplică următoarea operația n = n & mask , unde mask = ~(1 « i)
b7 … bi+1 bi bi−1 … b0
n n7 … ni+1 * ni−1 … n0
mask 1 … 1 0 1 … 1 op
explicație:
orice valoare are avea bitul ∗∗ va fi suprascris cu 0
ceilalti biți vor fi copiați
Implementare C
// reset
// byte - byte de intrare pentru care vreau sa resetez un bit
// i - indexul bitului din byte
// @return - noul byte
char reset(char byte, int i) {
int mask = ~(1 << i);
return (byte & mask);
}
...
mybyte = reset(mybyte, i);
...
op este o operație, iar mask un număr. Să analizăm cine pot fi op și biții din mască (mimi).
Dorim ca:
∗ op mi=0∗ op mi=0, adică operația op aplicată pe ∗∗ (orice) și mimi, va avea mereu ca rezultat pe 00
nj op mj=njnj op mj=nj (unde i != j), adică operația opop aplicată pe njnj și mjmj, va da njnj
Observăm că:
0 este elementul care poate “șterge” un bit prin ȘI, ceea ce verifică * & 0 = 0 * & 0 = 0, oricare ar fi *
un bit
1 este elementul neutru pentru ȘI, ceea ce verifică nj|1=njnj|1=nj, oricare are fi njnj un bit
Exerciții
Checker laborator 7 CB\CD Teste Problema 1 Teste Problema 2 Teste Problema 3 Teste Problema 4 Teste
Problema 5 Teste Problema 6
Precizari CB\CD
Rezolvați împreună cu asistentul pe tablă, exercițiile 0-4, apoi rezolvați invidual exercițiul 5.
Să se scrie o funcție care să afișeze toți biții unui număr întreg pe 32 biți.
Să se scrie o funcție care verifică dacă un număr întreg n pe 32 biți este puterea a lui 2. Funcția va returna 1
dacă n este putere a lui 2, 0 altfel.
O mulțime de numere întregi poate fi reprezentată astfel: spunem că un număr i aparține unei mulțimi S dacă
bit-ul al i-lea din vectorul S are valoarea 1.
Pentru eficientă, vectorul S va conține date de tipul unsigned char (reamintim ca sizeof(unsigned char) ==
1 byte adică 8 biți).
Pentru a folosi cu ușurință același cod facând schimbări minime (de exemplu schimbăm dimensiunea maximă a
unei mulțimi), putem să ne definim propriul tip astfel:
5.1
Implementați următoarele funcții. Realizați un program în C prin care să demonstrați că funcțiile implementate
funcționează.
În final va trebui sa creați o funcție main și să faceți un program care rezolvă cerința folosind funcțiile
implementate.
B1. Optimizations
O să învățați pe viitor că nu toate înstrucțiunile sunt la fel din punct de vedere al timpului de execuției. Ca un
punct de start, gândiți-vă că dacă ați face pe foaie o adunare sau o înmultire, durează mai mult să înmulțiți
decât să adunați. Această dificulate o are și procesorul din laptopul vostru!
Pentru a-l ajuta și a face programul mai rapid, putem înlocui operații costisitoare ( * , / ) cu altele mai puțin
costistoare ( + , - ). Cele mai rapide instrucțiuni sunt cele care lucrează direct pe biți (deoarece numerele sunt
stocate în memorie în binar, deci este modul natural de lucru pentru calculator).
Acest exercițiu vă propune să aplicați aceste optimizări pe codul vostru!
Hint
Pentru a completa acest bonus, NU aveți voie să folosiți operatorii /, *, % ! Încercați să folosiți operații pe biți!
B2. MSB/LSB
Să se scrie o câte o funcție pentru aflarea MSB(Most significant bit), respectiv LSB(Least significant bit), pentru
un număr n pe 32 biți.
Atenție! Problemele din această categorie au un nivel de dificultate ridicat, peste cel cerut la cursul de PC.
Recomandăm totuși rezolvarea acestor probleme pentru cei care doresc să aprofundeze lucrul cu operații pe
biți.
Swap bits
Se dă un număr n natural pe 32 de biți. Se cere să se calculeze numărul obținut prin interschimbarea biților de
rang par cu cel de rang impar.
n = 5 și sirul [1, 4, 4, 1, 5]
Follow-up 1: Șirul are 2∗n+(2∗p+1)2∗n+(2∗p+1) numere. Se sție că un singur număr apare de un număr impar
de ori (2p + 1), iar celelalte apar de un număr par de ori. Cum găsiți numărul care apare de un număr impar
de ori?
Exemplu:
n = 5 și sirul [1, 1, 4, 4, 4, 4, 5, 5, 5]
Răspunsul este 5.
Follow-up 2: Șirul are 2n+22n+2 numere, n numere apar de câte 2 ori, iar 2 numere sunt unice. Cum găsiți cele
2 numer unice?
Exemplu:
n = 5 și sirul [1, 4, 4, 1, 5, 6]
TODO: sursă
Follow up: Puteți găsi alt algoritm care să nu se bazeze pe împarțirea vectorului în două și compararea
elementului din mijloc cu cel cautat?
pune pe cei n sclavi aleși la masă cu câte un pahar în mână, inițial gol
poruncește bucătarului să toarne vin în pahare, alegând cui și ce să toarne
unui sclav i poate turna în pahat vin din oricâte sticle (chiar și simultan)
după ce s-a turnat în toate cele n pahare, ei vor bea toți simultan
la finalul celor 24h Gigel trebuie să își dea seama care este sticla otrăvită, altfel se va oftica și va pune
100 de sclavi să guste fiecare din câte o sticlă, iar pe cei care rămân în viață îi va împușca oricum pentru
că e nervos
P.S. Apreciați faptul că Gigel încearcă să nu omoare foarte mulți oameni degeaba. Ajutați-l să
găsească numărulminimndesclavinumărulminimndesclavi care trebuie să guste din vin.
Follow-up: Aceeași problemă, dar pentru n sticle.
TODO: sursă
Jocul NIM
Se dau n grămezi, fiecare conţinând un anumit număr de pietre. Doi jucători vor începe să ia alternativ din
pietre, astfel: la fiecare pas, jucătorul aflat la mutare trebuie să îndepărteze un număr nenul de pietre dintr-o
singură grămadă. Câştigătorul este cel care ia ultima piatră. Să se determine dacă jucătorul care ia primele
pietre are strategie sigură de câştig.
Exemple
Hint: nim
Sushi
Enunt: sushi
Optimizarea programelor folosind operaţii pe biţi
(Categoria Algoritmi, Autor Cosmin Negruşeri)
Conţinut:
Aplicaţia 1
Aplicaţia 2
Aplicaţia 3
Aplicaţia 4
Aplicaţia 5
Aplicaţia 6
O altă abordare: preprocesarea
Rezolvarea aplicaţiei 1
Rezolvarea aplicaţiei 2
Rezolvarea aplicaţiei 3
Rezolvarea aplicaţiei 4
Rezolvarea aplicaţiei 5
Algoritmi mai serioşi
Ciurul lui Eratostene
Bibliografie
Î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++)
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.
int count(long n) {
int num = 0;
if (n)
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;
return num;
}
int parity(long n) {
int num = 0;
if (n)
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) {
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 (0xFFFF0000 & n) {
num += 16;
if (0xFF00 & n) {
num += 8;
if (0xF0 & n) {
if (12 & n) {
num += 2;
if (2 & n) {
n = (2 & n) >> 1;
num += 1;
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 ;
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.
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) {
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) {
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) {
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) {
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) {
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
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;
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:
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)) ;
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...
char at[maxsize];
int n;
int isPrime(int x) {
return (!at[x]) ;
void sieve() {
int i, j;
memset(at,0,sizeof(at));
at[i] = 1;
for (i = 3; i <= maxsize; i += 2) if (!at[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));
at[i] = 1;
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.
int n;
int isPrime(int x) {
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)) ;
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.
Bibliografie
1. James A. Storer, An Introduction to Data Structures and Algorithms, Springer Verlag, 2001;
2. T. H. Cormen, C. E. Leiserson, R. R. Rivest, Introducere în algoritmi, Editura Computer Libris Agora, 2000;
4. TopCoder
Lucrul pe biti in C++
In primul rand, numerele intregi sunt reprezentate in memorie in baza 2, iar lungimea lor poate
avea 8,16 sau 32 de biti. Felul in care sunt memorate aceste numere se numeste cod
complementar. Sa zicem ca vrem sa prezentam numarul 5 in cod complementar. 5 scris in
baza 2 este 101 (se citeste unu zero unu si nu o suta unu). Reprezentarea lui pe 8 biti ar arata
cam asa:
Dupa cate puteti observa, ceilalti biti s-au umplut cu 0 pana la ultimul bit.
Operatiile pe biti
1.Operatorul de conjunctie
Se noteaza cu & si acesta returneaza numarul intreg a carui reprezentare se obtine prin
conjunctia bitilor din reprezentarea operanzilor in cod complementar.Ca sa puteti intelege o sa
fac un tabel cu valorile care rezulta din aplicarea acestui operator si un exemplu:
1 0 0
0 1 0
1 1 1
0 0 0
Exemplu:Reprezentarea lui 5 este 00000101 iar a lui 3 este 00000011.Cand aplicam operatorul
& rezultatul va arata astfel conform tabelului:
Evident rezultatul final este 1.
2.Operatorul de disjunctie
Se noteaza cu | si este un operator binar. Returneaza numarul intreg a carui reprezentare se
obtine prin disjunctia bitilor din reprezentarea operanzilor. Din nou o sa va arat clasicul tabel.
Vom aplica operatorul | asupra numerelor 15 si 3(reprezentarea lui 15 este 00001111):
1 0 1
0 1 1
1 1 1
0 0 0
1 0 1
0 1 1
1 1 0
0 0 0
Sa luam exemplu numerele 4 si 2.Reprezentarea lui 4 este 00000100. Prin deplasare la stanga se
obtine 00010000, care este de fapt numarul 16. Prin deplasare la dreapta se obtine 00000001,
care este numarul 1.
Operatorul shift right poate fi asociat cu impartirea intreaga la 2. Operatorul shift left poate fi
folosit si pentru a calcula mai rapid numarul 2 la puterea n prin aplicarea lui astfel 1<<n.
Sper ca am fost cat de cat de ajutor. In urmatorul articol o sa vorbim despre operatii la nivel de
bit (asta daca mai am timp). Operatorii pe biti sunt folositi deseori in optimizarea programelor
deoarece sunt operatii care sunt folosite direct de catre microprocesor. Asa ca puteti da copy
paste la codul de mai jos si sa va jucati cu valori.
#include <iostream>
int main()
{ int n,m;
cin>>n>>m;
cout<<(n & m)<<endl;
cout<<(n | m)<<endl;
cout<<(n ^ m)<<endl;
cout<<(n<<m)<<endl;
cout<<(n>>m);
return 0;}