bitset — tablou compact de biți

Mediu~15 min15 pași

De ce contează?

Imaginează-ți un panou lung cu becuri, fiecare aprins (1) sau stins (0). Vrei să le comanzi pe toate odată: „stinge tot rândul”, „aprinde becurile care sunt aprinse și la panoul vecin”. Dacă apeși un buton pentru fiecare bec, durează o veșnicie. bitset îți dă un singur buton care acționează zeci de becuri în aceeași clipă.

Ce este un bitset și de ce îl folosești

Un bitset este un tablou de biți (valori 0 sau 1) cu dimensiune fixată la compilare: scrii bitset<N>, iar N nu se mai poate schimba la rulare. Pare o limitare, dar tocmai asta îl face rapid.

Sunt două motive pentru care îl alegi:

  • Compactare. Un bool ocupă de obicei 1 octet (8 biți) ca să țină o singură valoare 0/1. Un bitset împachetează 64 de biți într-un singur cuvânt de memorie, deci e de aproximativ 8x mai compact decât un șir de bool și mult mai compact decât folosirea de întregi.
  • Operații în masă. Când faci a & b, procesorul prelucrează 64 de biți dintr-o singură instrucțiune. Pentru N biți, costul devine de ordinul O(N/64) în loc de O(N) — de aici accelerarea de ~64x folosită la ciur sau la DP pe seturi de biți.

Exemplu pas cu pas

Pornim cu un bitset<8> plin de zerouri și îl modificăm:

  • b.set(2) aprinde bitul 2 → 00000100
  • b.set(5) aprinde bitul 5 → 00100100
  • b.set(0) aprinde bitul 0 → 00100101
  • b.count() numără biții pe 1 → 3
  • b.reset(5) stinge bitul 5 → 00000101
  • b.flip() inversează toți biții → 11111010

Atenție la ordine: bitul 0 este cel mai din dreapta (cel mai puțin semnificativ). Când scriem un bitset, primul bit (index 0) apare ultimul.

Implementare C++

#include <iostream>
#include <bitset>
using namespace std;

int main() {
    bitset<8> b;                 // 00000000 (toti biti pe 0)

    b.set(2);                    // 00000100
    b.set(5);                    // 00100100
    b[0] = 1;                    // 00100101 (indexare cu [])

    cout << b << "\n";           // afiseaza 00100101
    cout << b.count() << "\n";   // 3 (biti pe 1)
    cout << b.size() << "\n";    // 8 (dimensiunea N)
    cout << b.test(2) << "\n";   // 1 (bitul 2 e setat)
    cout << b.any() << "\n";     // 1 (exista cel putin un bit pe 1)
    cout << b.none() << "\n";    // 0 (nu sunt toti pe 0)

    b.reset(5);                  // 00000101 (stinge bitul 5)
    b.flip();                    // 11111010 (inverseaza toti bitii)
    cout << b << "\n";           // 11111010

    // operatii pe doua bitset-uri de aceeasi dimensiune
    bitset<8> x(string("11001010"));   // construit dintr-un sir
    bitset<8> y(string("10101100"));
    cout << (x & y) << "\n";     // 10001000 (SI pe biti)
    cout << (x | y) << "\n";     // 11101110 (SAU pe biti)
    cout << (x ^ y) << "\n";     // 01100110 (XOR pe biti)
    cout << (~x)    << "\n";     // 00110101 (negare)
    cout << (x << 2) << "\n";    // 00101000 (deplasare stanga cu 2)
    cout << (x >> 1) << "\n";    // 01100101 (deplasare dreapta cu 1)
    return 0;
}

Complexitate

Fie N numărul de biți. Operațiile în masă tratează câte 64 de biți deodată.

OperațieTimp
set(i) / reset(i) / flip(i) / test(i) / b[i]O(1)
count() / any() / none()O(N/64)
& | ^ ~ << >> (pe tot bitset-ul)O(N/64)
afișarea cu cout << bO(N)
Observația-cheie

Acel factor /64 nu schimbă clasa asimptotică (rămâne liniar în N), dar în practică taie timpul de ~64 de ori. La un ciur al lui Eratostene sau la un DP de tip „subset sum”, diferența dintre O(N) și O(N/64) e exact ce te ține în limita de timp.

Greșeli frecvente

Greșeli frecvente de concurs:

  • Dimensiune variabilă. bitset<n> cu n citit de la tastatură NU compilează — N trebuie să fie o constantă cunoscută la compilare. Pune o margine fixă, de ex. bitset<1000000> b;.
  • Inversezi indexarea. Bitul 0 este cel mai puțin semnificativ (cel din dreapta când afișezi). b[0] nu e primul caracter scris, ci ultimul.
  • test() vs [] pe poziție invalidă. b.test(i) aruncă excepție dacă i iese din N; b[i] NU verifică limita și dă comportament nedefinit. Folosește test() când nu ești sigur de index.
  • Operații între dimensiuni diferite. bitset<8> și bitset<16> nu se pot combina cu &, |, ^. Ambii operanzi trebuie să aibă același N.

Dacă vizualizezi un bitset, citește-l de la dreapta la stânga — indicele 0 e în capătul din dreapta:

bit
1
0
1
1
0
0
1
0
7
6
5
4
3
2
1
0
Un bitset<8>. count() = 4 (biții pe 1). Indicele 0 e celula din dreapta.

Recapitulare

  • bitset<N> are dimensiune fixă la compilare; e de ~8x mai compact ca bool[].
  • Operațiile în masă (&, |, ^, ~, <<, >>) lucrează pe 64 de biți deodată, deci O(N/64) — folosite la ciur și DP pe seturi de biți.
  • Controlul individual: set/reset/flip/test/count/any/none și indexarea cu [], unde bitul 0 e cel mai puțin semnificativ.

Întrebarea 1 / 3

Cum se stabilește dimensiunea unui bitset?