Atenţie! Aceasta este o versiune veche a paginii, scrisă la 2009-12-28 10:46:27.
Revizia anterioară   Revizia următoare  

Operaţii pe biţi

Acest articol a fost adăugat de GheorgheMihaiMihai Gheorghe GheorgheMihai
Intră aici dacă doreşti să scrii articole sau află cum te poţi implica în celelalte proiecte infoarena!

(Categoria Algoritmi, Autor Cosmin Negruşeri)

Î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). Mie personal mi s-a întâmplat ca la implementarea soluţiei unei probleme care în engleză se numeşte "Bottleneck Minimal Spanning Tree" să observ 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:
n  = 110111010100002
n - 1  = 110111010011112
n & (n - 1) = 110111010000002

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)
      while (n &= n - 1) num++;
   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)
      while (n &= n - 1) num ^= 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:
n  = 110110002
n-1  = 110101112
n & (n - 1)  = 110100002
n  = 110110002
n ^ (n & (n - 1)) = 000010002

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