Pack 64 booleans into a machine word and operate on all of them at once. That divides by and turns simple operations into . With AVX2 pragmas the compiler processes 256 bits per instruction.
std::bitset
bitset<N> b; // N must be a compile-time constant
b.set(i); b.reset(i); b.flip(i); b[i];
b.count(); b.any(); b.none();
b |= c; b &= c; b ^= c; b <<= k; b >>= k; // O(N / 64)
for (int i = b._Find_first(); i < N; i = b._Find_next(i)) {} // iterate set bits (GCC)For a runtime size, either pick the next power of two from a template list (template<int L> void solve() dispatched on ), or write a vector<ull> bitset by hand. Shifts across words need a carry from the neighbouring word.
Classic speedups
| Problem | Bitset form | Cost |
|---|---|---|
| subset sum / 0-1 knapsack feasibility | dp |= dp << w | |
| subset sum with bounded counts | binary splitting of multiplicities, then shifts | when values (at most distinct values) |
Proof:
dp |= dp << wand the distinct-value boundThe shift encodes the subset-sum transition. Represent “sums reachable so far” as a bitset where bit is set iff is reachable. Before considering item , let be the reachable set; after optionally taking , the new reachable set is (either skip the item, keeping , or take it, shifting every reachable sum up by ) — and as a bitset is exactly
dp << w(shifting every set bit to position ), so is exactlydp | (dp << w). Applying this once per item, by induction the bitset after processing items holds exactly the subset sums achievable using a subset of the first items, with always reachable (the empty subset) since bit of the initialdpis set and no shift ever clears it. bound. If the multiset of item values has distinct values each appearing at least once, and the total sum is , then since appearing at all requires contributing at least (as the values are distinct positive integers, the -th smallest is at least ), , so . Binary-splitting each value’s multiplicity into power-of-two-sized chunks and shifting by each chunk (rather than one shift per original item) therefore costs shifts rather than , each , dominating when .Tested
dp |= dp << wagainst an explicit subset-sum enumeration for up to 8, random positive weights, confirming the bitset after all items marks exactly the achievable sums.
| transitive closure |if (r[i][k]) r[i] \|= r[k]| |
| DAG reachability counts for every vertex | reverse topological order,reach[v] \|= reach[u]| ; process in vertex chunks of 64·k to save memory |
| boolean matrix product | row of = OR of rows of with | |
| triangles in a dense graph |(adj[u] & adj[v]).count()over edges | |
| bipartite matching / BFS on dense graphs | unvisited neighbours,_Find_first| per BFS |
| maximum clique (Bron-Kerbosch) | in one AND | practical up to (Maximum Clique) |
| LCS | bit-parallel row update (Crochemore-Iliopoulos-Pinzon) | |
| pattern matching with wildcards / small alphabets | Shift-And / Bitap:D = ((D << 1) \| 1) & mask[c]| |
| all occurrences of a pattern in a string with updates |ans = all ones; ans &= (pos[p[j]] >> j)for each | per query |
| Gaussian elimination over | rows as bitsets, XOR row operations | |
| counting pairs with Hamming distance | popcount of XOR of rows | |
| 3D/4D dominance (offline) | one bitset per (dimension, threshold), AND them | |
Patterns
- Subset sum with reconstruction: keep a bitset per item (memory bytes) or use divide and conquer on items.
- Knapsack where the answer is “which sums are possible”: bitsets beat the DP by 64x; with a count limit per value, group equal values and split into powers of two.
- Sliding-window or dynamic bitsets: maintain one bitset per value (positions where ); “positions such that for all ” is an AND of shifted bitsets.
- Bitset over the value domain for “does a pair with exist”: convolution would be FFT, but a bitset gives an alternative for moderate .
- Offline queries in chunks: if bitsets of size don’t fit in memory ( gives 1.25 GB), process vertices or queries in blocks of 64 or 4096 and reuse the memory.
When it pays
The inner loop is a simple AND/OR/XOR/shift over independent boolean entries, with no data-dependent branching. Values that aren’t boolean can sometimes be split by bit or by threshold. vector<bool> is also bit-packed, but it has no bulk operations, so it doesn’t count.
Practical: #pragma GCC target("avx2") gives a further 2-4x on bitset loops. Memory is bytes per bitset. Iterating set bits with _Find_next costs , not — the win that matters for a sparse bitset.
Bitsets are the accessible form of word-level parallelism: the same “process 64 independent booleans per instruction” idea behind fusion trees (Theoretical Algorithms Catalog), Bitap/shift-or string matching, and the four-Russians technique. Whenever an inner loop does the same simple thing to many independent booleans, it can probably be done 64 at a time.