1. Introduction and Motivation
Many dynamic programming tables store nothing but a yes/no answer per state: is this sum reachable, is this position matched, is this configuration possible. A table like that is a row of booleans, and a row of booleans is exactly what a machine word already is. Instead of storing reachable[0], reachable[1], ..., reachable[m] as separate array cells and looping over them one at a time, you can store the entire row as the bits of a single integer, where bit $i$ means "state $i$ is reachable."
Once the row is a bitset, a transition that touches every state at once — such as "state $s$ becomes reachable if state $s - x$ was reachable" for every $s$ — collapses into one shifted OR: row = row | (row << x). A CPU executes that shift and that OR on all 32 or 64 bits of a word in a single instruction each, so an entire inner loop over the DP row is replaced by a small constant number of word-level operations. This is not a change in asymptotic complexity class in the strict sense — it is a division by the machine word width $w$ (typically 64) — but that constant factor is large enough to turn a borderline-too-slow DP into a fast one.
2. Prerequisites
This lesson assumes you are comfortable with basic bitwise operators. Bit Manipulation: Core Tricks & Interview Patterns covers &, |, ^, and shifts in detail and is the right place to build that fluency first; this lesson does not re-derive how a single shift or AND works; it only assumes you already read one as "move every bit left by $x$ positions" or "keep a bit only where both operands have it set."
The other prerequisite is the ordinary, one-state-at-a-time subset sum dynamic program. Knapsack & Coin Change Variants develops that DP and its 0/1 versus unbounded distinction in full, and it links back to this page for the bitset-acceleration technique applied to its Subset Sum variant. Read that lesson's Subset Sum section first if the recurrence reachable[s] = reachable[s] OR reachable[s - item] is not already familiar, since this lesson starts from that recurrence rather than deriving it from scratch.
3. A Concrete Example
Bitset Subset Sum on items [3, 4, 5]
Represent the set of reachable sums as one integer, reachable, where bit $i$ is 1 exactly when sum $i$ can be formed from the items processed so far. Start with only sum 0 reachable (the empty subset), then fold in each item with reachable = reachable | (reachable << item).
| Step | Operation | Bits (index 12 down to 0) | Reachable sums |
|---|---|---|---|
| Start | — | 0000000000001 | {0} |
| +3 | reachable |= reachable << 3 | 0000000001001 | {0, 3} |
| +4 | reachable |= reachable << 4 | 0000010011001 | {0, 3, 4, 7} |
| +5 | reachable |= reachable << 5 | 1001110111001 | {0, 3, 4, 5, 7, 8, 9, 12} |
Each row of the table is exactly the subset sums achievable from the items folded in so far: after all three items, the reachable set matches every subset sum of {3, 4, 5} (the empty set sums to 0, single items give 3, 4, 5, pairs give 7, 8, 9, and the full set gives 12). Bit 9 is set, so 9 is reachable via {4, 5}; bit 2 is never set at any step, so 2 is not a reachable sum from this item set.
4. Core Insight: Fast Boolean State Transitions
reachable[s] = reachable[s] OR reachable[s - item] evaluated for every $s$, is exactly what reachable = reachable | (reachable << item) computes in one step: shifting the whole row left by item moves the value that used to live at index $s - \text{item}$ up to index $s$, and ORing it back into the original row updates every $s$ simultaneously.
The naive DP visits every sum $s$ from high to low and asks "was $s - \text{item}$ already reachable." The bitset version asks the same question for every $s$ at once, because a left shift by item is precisely the operation "move whatever was true at $s - \text{item}$ to position $s$," for all positions simultaneously. Nothing about the recurrence changes; only the unit of work changes, from one bit to one machine word.
5. Bitset Subset Sum
Each panel below tests whether a target sum is reachable from a list of positive items, using the shift-and-OR recurrence from the previous section. Python's arbitrary-precision int grows to fit any sum, so no upper bound needs to be declared in advance. C++'s std::bitset<N> is fixed-size and must be declared with a compile-time capacity N large enough for the largest sum you will ever query; in exchange it is a flat array of machine words with no allocation per operation, which is faster than a dynamically sized big-integer when the maximum sum is known ahead of time. Java has no bitwise-shiftable fixed-size bitset built in, so BigInteger is the idiomatic choice — it behaves like Python's int, arbitrary precision with native shiftLeft/or/testBit methods.
The naive Subset Sum DP fills a boolean array of size $\text{sum}+1$ once per item, giving $O(n \times \text{sum})$ time. The bitset version performs the same number of OR-with-shifted-row operations, one per item, but each operation processes $w$ bits (a full machine word, typically 64) per underlying instruction instead of one bit per loop iteration. That gives $O(n \times \text{sum} / w)$, a constant-factor speedup of roughly $w$ that is easy to underestimate until you measure it: on large sums this routinely turns a multi-second naive DP into a sub-second bitset DP.
6. String DP Acceleration
state where bit $i$ means "the pattern's first $i+1$ characters match the text ending at the current position." Scanning one text character updates every bit of state in one shift-and-mask: state = ((state << 1) | 1) & charMask[currentChar], where charMask[c] is a precomputed bitset marking every pattern position that holds character c.
Precompute, for each character $c$ that appears in the pattern, a mask with bit $i$ set wherever pattern[i] == c. Scanning the text one character at a time, shift state left by one (every partial match advances by one matched character), OR in a 1 at bit 0 (a match can always restart at the current position), then AND with the current character's mask (drop every partial match whose next required character does not equal the current text character). If the highest bit of state — bit $m-1$ for a pattern of length $m$ — becomes set, the whole pattern just matched ending at the current text position.
Tracing pattern "ab" against text "cab": charMask['a'] = 01, charMask['b'] = 10, charMask['c'] = 00, matchMask = 10 (bit 1). Start state = 00. Reading c: ((00<<1)|1) & 00 = 00. Reading a: ((00<<1)|1) & 01 = 01 (the "a" alone has matched). Reading b: ((01<<1)|1) & 10 = 11 & 10 = 10, and bit 1 is set, so "ab" matches ending at this position — the match starts at index 1, since a match of length 2 ending at text index 2 starts at $2-2+1=1$.
This is the same acceleration as bitset Subset Sum applied to a different DP: the underlying recurrence for exact string matching is a boolean transition over "how many pattern characters matched so far," and packing that dimension into one word lets a single shift-and-mask update the entire match-progress row per text character, instead of a nested loop over pattern positions.
7. Complexity Analysis
For bitset Subset Sum, $n$ is the number of items and $m$ is the target sum, so naive $O(n \times \text{sum})$ becomes $O(n \times \text{sum} / w)$. For Shift-And string matching, $n$ is the text length and $m$ is the pattern length, so the naive automaton simulation's $O(n \times m)$ (or a naive character-by-character comparison's worst case $O(n \times m)$) becomes $O(n \times m / w)$ whenever the pattern fits in one machine word; longer patterns need $\lceil m / w \rceil$ words and correspondingly $\lceil m/w \rceil$ times the per-character work, still far fewer operations than one comparison per pattern position.
8. Applications and Alternatives
Where bitset acceleration is worth reaching for
- Competitive programming Subset Sum feasibility: "can this multiset reach this target" problems with sums in the tens of thousands are a routine
std::bitsetor Python big-integer speedup over a boolean DP array. - Bioinformatics string search: Shift-And and its edit-distance relative Shift-Or scan DNA/protein sequences against short motifs far faster than a naive scan, especially over a small alphabet where the per-character mask table stays tiny.
- Approximate and multi-pattern matching: the same bitset state can be extended to tolerate a bounded number of mismatches (bit-parallel approximate matching) or to track several patterns' automata in parallel words.
- When it is the wrong tool: if the DP needs to answer range queries or point updates over a mutable, evolving structure rather than a fixed left-to-right fold, a Fenwick tree or segment tree is usually the better fit; see Fenwick Tree Variants: Range Updates & 2D BIT for that alternative rather than trying to force updates into a bitset transition.
9. Exercises and Mastery
Easy
- Using the bitset Subset Sum code, check whether the set
[2, 6, 9]can reach the target 15. Trace the bitset by hand after each item, the way the worked example did for[3, 4, 5]. - Extend the same code to also report the smallest reachable sum that is greater than or equal to a given target, using
reachableafter all items are folded in.
Medium
- Extend bitset Subset Sum to reconstruct which items were chosen for a reachable target. A single final bitset only tells you a sum is reachable, not by which items, because the OR has merged every path into one word. The standard workaround is to keep one bitset snapshot
reachable_iafter processing each of the first $i$ items (an array of $n+1$ bitsets), then walk backward from $i = n$: if bit $s$ is set inreachable_ibut not inreachable_{i-1}, item $i$ was necessarily used to reach $s$, so record it and continue the search fromreachable_{i-1}at $s - \text{item}_i$; otherwise item $i$ was not needed for this particular reconstruction, and you continue fromreachable_{i-1}at the same $s$. This costs $O(n)$ extra bitsets of memory in exchange for full traceability. - Adapt the Shift-And code to count, rather than list, how many text positions the pattern matches, without storing the match list.
Hard
- Use bitset acceleration to speed up counting the number of distinct subsequences of a text that equal a given pattern. The naive DP is $O(n \times m)$ over a boolean-or-count table indexed by (text position, pattern position); when only reachability of "pattern fully consumed" matters rather than the exact count, the same per-character bitset used in Shift-And can determine in $O(n \times m / w)$ whether at least one occurrence exists, though recovering an exact count still requires care since counts, unlike booleans, do not compress into single bits without additional bookkeeping (e.g., splitting the count DP across several bitsets to represent low-order bits, similar to how a binary adder is built from single-bit gates).
Review rhythm: tomorrow, rederive reachable = reachable | (reachable << item) from the plain recurrence without looking it up; next week, implement the Shift-And trace by hand on a pattern you pick yourself; several weeks later, look for a third DP in your own work whose transition is a boolean OR or AND over a large dimension, and pack it into a bitset the same way.
10. Summary Cheat Sheet
- A boolean DP row is a bitset waiting to happen: bit $i$ of one integer replaces a whole array cell
reachable[i]. - Subset Sum's
reachable[s] = reachable[s] OR reachable[s - item]becomesreachable = reachable | (reachable << item)as a single word-parallel step. - Naive $O(n \times \text{sum})$ becomes $O(n \times \text{sum} / w)$; naive $O(n \times m)$ string matching becomes $O(n \times m / w)$, where $w$ is the machine word size, typically 64.
- Python's arbitrary-precision
int, C++'sstd::bitset<N>, and Java'sBigIntegerare the three idiomatic carriers for this technique, trading fixed capacity for allocation-free speed or vice versa. - Shift-And packs "how much of the pattern has matched" into one word and updates it per text character with
state = ((state << 1) | 1) & charMask[c]. - The technique loses per-item or per-position bookkeeping; reconstructing which choices were made requires extra bitset snapshots or an auxiliary structure, not just the final bitset.
- Reach for a Fenwick tree or segment tree instead when the workload needs point updates or range queries over time, rather than a single left-to-right fold.