Documente Academic
Documente Profesional
Documente Cultură
focus
Dac ne uitm cu atenie i analizm rezultatul operaiei n & (n - 1) putem obine o soluie mai bun. S lum un exemplu:
n = (11011101010000)2 n-1 = (11011101001111)2 n & (n - 1) = (11011101000000)2
Se vede clar de aici c efectul operaiei 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) while (n &= n - 1) num++; return num; }
Problema 1
S se determine numrul de bii 1 din reprezentarea binar a numrului n. Rezolvarea naiv a acestei probleme ar consta n parcurgerea secvenial a biilor lui n.
32
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 numr de pai egali cu numrul de bii cu valoarea 1 din numr, deci n medie jumtate din numrul de pai efectuai de prima metod.
Problema 2
S se determine paritatea numrului de bii de 1 din reprezentarea binar a unui numr n. 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) while (n &= n - 1) num ^= 1; return num; }
Dei constanta multiplicativ este mai mare, numrul de operaii are ordin logaritmic fa de numrul de bii ai unui cuvnt al calculatorului.
Problema 3
S se determine cel mai puin semnificativ bit de 1 din reprezentarea binar a lui n. Rezolvarea naiv se comport bine n medie, deoarece numrul de cicluri pn la gsirea unui bit cu valoarea 1 este de obicei mic, dar din cele discutate mai sus putem gsi ceva mai bun. Aa cum am artat n & (n - 1) are ca rezultat numrul n din care s-a sczut cel mai puin semnificativ bit. Folosind aceast idee obinem urmtoarea funcie:
int low1(long n){ return n ^ (n & (n - 1)); }
focus
Putem obine o a treia rezolvare fcnd cteva observaii pe un exemplu: Considerm n = (11011011)2. Rezultatul cutat este dat de valoarea 1 ^ 1 ^ 0 ^ 1 ^ 1 ^ 0 ^ 1 ^ 1. mprim pe n n partea lui superioar i partea lui inferioar:
1 1 0 1 ^ 1 0 1 1 = 0 1 1 0
Exemplu:
n = (11011000)2 n-1 = (11010111)2 n & (n - 1) = (11010000)2 n = (11011000)2 n ^ (n & (n - 1)) = (00001000)2
Aceast funcie este foarte important pentru structura de date Arbori Indexai Binar prezentat ntr-un un articol mai vechi din GInfo.
Problema 4
Aplicm asupra rezultatului acelai procedeu:
0 1 ^ 1 0 = 0 1
S se determine cel mai semnificativ bit cu valoarea 1 din reprezentarea binar a lui n. Putem aplica i aici ideile prezentate mai sus: cea naiv i cea cu eliminarea biilor, dar putem gsi i ceva mai bun. O abordare ar fi cea a cutrii binare (aplicabil i n problema anterioar). Verificm dac partea superioar a lui n este 0. Dac nu este 0, atunci cutm bitul cel mai semnificativ din ea, iar dac este, ne ocupm de partea inferioar, deci reducem la fiecare pas problema la jumtate.
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;
i
1 ^ 1 = 0
S scriem algoritmul care reiese din acest exemplu, lund n considerare faptul c numrul n este reprezentat pe 32 de bii:
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; }
33
focus
} if (12 & n) { n = (12 & n) >> 2; num += 2; } if (2 & n) { n = (2 & n) >> 1; num += 1; } return 1 << num; }
tru unele supercalculatoare aceste instruciuni sunt instruciuni ale procesorului. Se pot gsi ali algoritmi mai rapizi dect cei de mai sus, dar de obicei gsirea unui algoritm mai inteligent este mai grea dect folosirea unei abordri banale care aduce un ctig foarte mare: preprocesarea. n continuare vom rezolva problemele prezentate anterior folosindu-ne de preprocesarea datelor. Problema 1 Determinm, pentru numerele de la 0 la 216 - 1, un tablou num, n care num[i] este egal cu numrul de bii de 1 din reprezentarea binar a lui i. De aici obinem soluia:
int count(long n){ return num[n & 0xFFFF] + num[n >> 16];
Problema 5
S se determine indexul celui mai semnificativ bit de 1 din reprezentarea binar a lui n. Metodele prezentate la rezolvarea problemei 4 pot fi folosite i aici, dar vom prezenta o nou rezolvare. S lum urmtorul ir de operaii pentru un exemplu:
n = (10000000)2 n = n | (n >> 1) n = (11000000)2 n = n | (n >> 2) n = (11110000)2 n = n | (n >> 4) n = (11111111)2
} Problema 2 Determinm, pentru numerele de la 0 la 216 - 1, un tablou par, n care par[i] este egal cu paritatea numrului de bii de 1 din reprezentarea binar a lui i. De aici obinem soluia:
int parity(long n){ return par[n & 0xFFFF] ^ par[n >> 16];
Se observ c aplicnd o secven asemntoare de instruciuni cu cea de mai sus putem face ca un numr n s se transforme n alt numr care are un numr de bii de 1 egal cu 1 + indexul celui mai semnificativ bit cu valoarea 1 din n. De aici algoritmul este urmtorul:
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;
} Problema 3 Determinm, 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 obinem soluia:
int low(long n){ if (l[n & 0xFFFF]) return l[n & 0xFFFF]; else return l[n >> 16] << 16; }
Problema 6
S se determine dac n este o putere a lui 2 (ntrebare interviu Microsoft).
int isTwoPower(long n){ return (n & (n-1) == 0); }
Problema 4 Determinm, 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 obinem soluia:
int high(long n){ if (h[n >> 16]) return h[n >> 16] << 16; else return h[n & 0xFFFF]; }
34
Problema 5 Determinm, 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.
face asupra a 16 valori deodat. Deci i aici se ctig 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 mic divizor comun a dou numere binare de cel mult 10000 de cifre. Dup cum se tie, complexitatea algoritmului de determinare a celui mai mare divizor comun a dou numere este logaritmic, acest fapt este adevrat atunci cnd operaiile efectuate sunt constante, dar n rezolvarea noastr apar numere mari, deci o estimare a numrului de calcule ar fi maxn maxn log maxn (mprirea are n medie costul maxn maxn dac nu implementm metode mai laborioase). Acest numr este prea mare pentru timpul de rezolvare care ne este cerut. Pentru a evita mprirea, care este operaia cea mai costisitoare, putem folosi algoritmul de determinare a celui mai mare divizor comun binar care se folosete de urmtoarele proprieti: cmmdc(a, b) = 2 * cmmdc(a / 2, b / 2), dac a i b sunt numere pare; cmmdc(a, b) = cmmdc(a, b / 2), dac a este impar; cmmdc(a, b) = cmmdc(a - b, b), dac a este impar, b este impar i a > b. Aplicnd acest algoritm numrul de calcule s-a redus la maxn log maxn. Dar nici acest numr nu este suficient de mic, i deci pentru accelerarea algoritmului folosim ideea utilizat n cele dou probleme de mai sus.mpachetm numrul n ntregi n pachete de cte 30 de bii, i atunci deplasarea la stnga (adic mprirea la 2) i scderea vor fi de 30 de ori mai rapide. S ncercm acum rezolvarea unei probleme n care eficiena este cea mai important, mai important dect lizibilitatea codului obinut. Problema are urmtorul enun: Se consider un numr 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 primalitii numerelor mai mici sau egale cu n .
focus
Dac n loc s pstrm n a[i][j] un element al matricei binare, pstram 16 elemente, atunci n linia b = a[i] ^ a[j] se efectueaz cel mult 2500 / 16 calcule n loc de 2500 de calcule. Acum ce a rmas de optimizat este metoda numaraBitiUnu. Dac folosim preprocesarea, atunci aceast metod va fi compus i ea 2500/16 calcule. Se observ c n acest fel am mrit viteza de execuie a algoritmului cu un factor de 16. Alt exemplu ar fi problema rundei 17. n acea problem se cere determinarea numrului de sume distincte care se pot obine folosind nite 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 obinut punctaj maxim. Pentru obinerea punctajului maxim urmtoarea idee ar fi putut fi folosit cu succes: irul de valori logice se condenseaz ntr-un ir de ntregi reprezentai pe 16 bii, i acum actualizarea se
Putem ncerca mai multe rezolvri, dar n practic cea mai rapid se dovedete de obicei cea numit ciurul lui Eratostene. Ea se nva n coli nc din clasa a 6-a, deci nu o voi mai explica. Implementarea banal a algoritmului ar fi urmtoarea:
#define maxsize 10000000 char at[maxsize]; //vector de valori logice n care nume//rele prime vor fi marcate cu 0 int n; int isPrime(int x){ return (!at[x]) ; }
35
focus
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; //marcm toi multipli lui i ca fiind //numere prime }
Ultima optimizare este optimizarea spaiului necesar. ntr-un tip de date char putem mpacheta opt valori logice i, punnd un test suplimentar n metoda isPrime putem elimina i numerele pare, astfel vom avea nevoie de un spaiu 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))) ; }
Observm c jumtate din timpul folosit de noi la preprocesare este pierdut cu numerele pare. Marcndu-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; }
Concluzie
Asemenea optimizri 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 cretere semnificativ de vitez a aplicaiilor i trebuie documentate foarte bine, mai ales dac se dorete sau este necesar ca alt programator s poat lucra cu codul respectiv mai trziu.
La marcarea multiplilor numrului prim i toate numerele pn la i i au fost deja marcate deoarece i i este cel mai mic numr care nu este divizibil cu numere naturale mai mici sau egale cu i - 1. Deci, avem o a treia versiune a programului:
#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; j <= maxsize; j += i + i) at[j] = 1; }
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 3. ***, colecia GInfo 4. http://www.liis.ro/~campion 5. http://www.aggregate.org/MAGIC/ 6. http://www.topcoder.com RoundTables
Cosmin-Silvestru Negrueri este student n anul III la Universitatea Babe-Bolyai din Cluj-Napoca. El poate fi contactat prin e-mail la adresa kosmin_2000@yahoo.com.
36