Table of contents
Open Table of contents
1. Bit compression and storing a set
Suppose we need to operate on subsets of a set with elements , where . We store the subset in a bit string: bit is if element belongs to the set, and otherwise. Since , a single 64-bit unsigned integer holds it: unsigned long long x; // a set with at most 64 elements.
Given two sets and , we can express the set operations through integer operations:
| Set operation | Bitwise operation | |
|---|---|---|
| bitwise AND | ||
| bitwise OR | ||
| complement of | bitwise NOT | |
| add element to the set | left shift | |
| remove element from the set |
Here 1ull is the value 1 of type unsigned long long.
The harder part is counting the elements of a set, that is counting the bits of the binary representation. In C++ the built-in functions __builtin_popcount and __builtin_popcountll do this in , where is the machine word length (usually or , depending on the hardware). For small sets, say at most 16 or 32 elements, we can count the bits in by precomputing a table of size bytes:
char cnt[1 << 16];
void precompute() {
for (int i = 0; i < (1 << 16); ++i) {
cnt[i] = (i & 1) + cnt[i >> 1];
}
}
int bit_count_16(unsigned int x) { return cnt[x]; }
int bit_count_32(unsigned int x) { return cnt[x >> 16] + cnt[x & 65535]; }
For larger sets we can do the following:
const int N = (int) 5e7; // maximum number of elements in the set
const int N1 = N / 32 + 1;
unsigned int a[N1];
int get(int i) { return (a[i >> 5] >> (i & 31)) & 1; }
void set_0(int i) { a[i >> 5] &= ~(1u << (i & 31)); }
void set_1(int i) { a[i >> 5] |= 1u << (i & 31); }
int count() {
int sum = 0;
for (int i = 0; i < N1; ++i) {
sum += bit_count_32(a[i]);
}
return sum;
}
In C++, bitset is a similar structure for these operations:
a[3] = 1; // set bit index 3 to 1
a[3] = 0; // set bit index 3 to 0
int x = a[3];
printf("%d\n", (int) a[3]); // cast bitset<100>::reference to int
a = a | b; a |= b; // union of two sets
a = a & b; a &= b; // intersection of two sets
a = b >> 10; b = a << 10; // shift bits right and left
a = a & ~b; // difference of two sets
int c = (int) a.count(); // count the 1 bits in a
a.reset(3); b.reset(); // unset bits
Accessing and assigning element costs ; the other bitset<n> operations cost , where is the machine word length.
A bitset behaves like a plain array, so we can walk it with a pointer:
const int N = 40;
bitset<N> a;
uint8_t *ptr = (uint8_t*) &a;
ptr[0] = 10; // set the first 8 bits to 00001010
ptr[1] = 132; // set the next 8 bits to 10000100
cout << a << '\n'; // 00000000000000001000010000001010
In the rest of the article we look at examples of using bitset. For brevity, arrays default to zero unless stated otherwise.