A machine word holds booleans, and one instruction processes all of them. Any DP or set operation whose inner loop is a boolean OR / AND / XOR / shift over an array can run faster: , . With , is still too slow, but style bounds (about simple word operations) pass in a few seconds. The basics of std::bitset are in Bitset.
| Problem | Plain | Word-parallel |
|---|---|---|
| subset sum up to with items | ||
| LCS of strings of length | ||
| transitive closure | ||
| count triangles | or | |
| boolean matrix product | (Four Russians: ) | |
| Gaussian elimination over GF(2) | ||
| “is there a pair/triple with property” over sets | AND of two bitsets, then any() |
Subset sums, closure, triangles
template<int N> bitset<N> subsetSums(const vector<int>& a) { // bit s set iff some subset sums to s (s < N)
bitset<N> b; b[0] = 1;
for (int x : a) b |= b << x;
return b;
}
template<int N> vector<bitset<N>> transitiveClosure(vector<bitset<N>> g, int n) { // O(n^3 / 64)
for (int k = 0; k < n; k++)
for (int i = 0; i < n; i++) if (g[i][k]) g[i] |= g[k];
return g;
}
template<int N> long long countTriangles(const vector<bitset<N>>& adj, int n) {
long long c = 0;
for (int i = 0; i < n; i++) for (int j = i + 1; j < n; j++) if (adj[i][j]) c += (adj[i] & adj[j]).count();
return c / 3;
}Proof: the shifted-OR recurrences
Subset sums. After processing items , bit is set iff is a subset sum of them: true for ; and is a subset sum of the first items iff it is one of the first (bit already set) or is (bit set by
b << a_i). Closure. This is Warshall’s algorithm: after processing intermediate vertex , row contains every vertex reachable from using intermediates in ; if reaches , everything reaches is OR-ed in. Triangles. For each edge with , the AND of the neighbourhoods counts common neighbours; each triangle is counted once per edge, so three times.
LCS in O(nm/64)
Encode a DP row by its differences: bit of is where the row value increases. Each character of updates all columns at once with one addition, one subtraction and a few logic operations, carried across words:
int lcsBitParallel(const string& s, const string& t) { // O(|s| * |t| / 64): V <- (V + U) | (V - U), U = V & match
int m = sz(t), W = (m + 63) / 64;
vector<vector<ull>> match(256, vector<ull>(W, 0));
for (int j = 0; j < m; j++) match[(unsigned char)t[j]][j >> 6] |= 1ULL << (j & 63);
vector<ull> V(W, ~0ULL); // zero bits of V (among the first m) = LCS length
for (char c : s) {
const auto& M = match[(unsigned char)c];
ull carry = 0, borrow = 0;
for (int k = 0; k < W; k++) {
ull u = V[k] & M[k];
ull s1 = V[k] + u, c1 = s1 < u, s2 = s1 + carry, c2 = s2 < s1;
ull d1 = V[k] - u, b1 = V[k] < u, d2 = d1 - borrow, b2 = d1 < borrow;
V[k] = s2 | d2; carry = c1 | c2; borrow = b1 | b2;
}
}
int ones = 0;
for (int k = 0; k < W; k++) {
ull w = V[k];
if (k == W - 1 && m % 64) w &= (1ULL << (m % 64)) - 1;
ones += __builtin_popcountll(w);
}
return m - ones;
}Proof sketch: the bit-vector LCS update
Let be the LCS of and ; consecutive values differ by or , so row is encoded by the 0/1 vector of increments ( stores the complement, = no increment). In row , within each segment of ending at an increment, the new increment moves to the leftmost match position of the segment, if any. Adding to makes the carry run from each matched position to the end of its run of ones, which clears exactly those leftmost matches and sets the right bits; OR-ing with restores the bits that the carry wrongly cleared. This is the Allison–Dix / Hyyrö recurrence; the number of zero bits is . Tested against the DP on random strings over alphabets of size .
Rules of thumb
std::bitset<N>needs a compile-time ; for run-time sizes use avector<ull>with manual shifts, or a template recursion over sizes () to pick the smallest bitset that fits.- Shifts by cost each; OR-ing shifted copies is .
_Find_first()and_Find_next(i)iterate set bits in .- Popcount:
__builtin_popcountll, orbitset::count().
Related: Bitset, Knapsack, Sequence DP.