dp[mask] or dp[mask][last] state for a new problem, implement it correctly, and recognize when submask enumeration should be replaced by SOS DP.
1. Introduction: State as a Bitmask
Many combinatorial problems have a natural brute-force solution that tries every ordering, every subset, or every assignment of a small collection of elements. When the number of elements $n$ is small — roughly $n \le 20$ — a different technique becomes available: encode which subset of elements has already been used, visited, or assigned as a single integer, and let dynamic programming operate directly over that integer.
This is bitmask DP. Instead of a DP state built from an index or a length, the state includes a mask $\in [0, 2^n)$ whose bit $i$ records whether element $i$ has been consumed. Because there are only $2^n$ possible masks, an exponential search over orderings (which can be $n!$ possibilities) collapses into a polynomial-in-$2^n$ table of subproblems, each solved once and reused.
The same bitmask-as-state idea also powers a second, related technique covered later in this lesson: Sum-Over-Subsets (SOS) DP, which computes, for every mask simultaneously, an aggregate over all of that mask's submasks — in $O(n \cdot 2^n)$ instead of the $O(3^n)$ that naive per-mask submask enumeration would cost.
2. Prerequisites
This lesson assumes comfort with two things this site already covers in depth:
- Basic bitmask mechanics: representing a subset of $n$ elements as an $n$-bit integer and enumerating all $2^n$ subsets by counting a mask from
0to(1<<n)-1. This mechanic is covered in the Bitmasks for Subsets section of the Bit Manipulation lesson and is not re-derived here — iffor mask in range(1<<n)and(mask >> i) & 1are unfamiliar, read that section first. - General DP methodology: identifying overlapping subproblems, defining state precisely, deriving a recurrence, and choosing memoization versus tabulation, as built up in Dynamic Programming Foundations. Bitmask DP is not a separate discipline from that methodology — it is ordinary DP where the state happens to be a bitmask instead of an index.
3. A Concrete Example: TSP by Hand
Four cities, one shortest tour
Four cities are numbered 0–3, with a symmetric travel cost between every pair:
| City 0 | City 1 | City 2 | City 3 | |
|---|---|---|---|---|
| City 0 | 0 | 10 | 15 | 20 |
| City 1 | 10 | 0 | 35 | 25 |
| City 2 | 15 | 35 | 0 | 30 |
| City 3 | 20 | 25 | 30 | 0 |
The tour must start and end at city 0 and visit every other city exactly once. Define dp[mask][last] as the minimum cost of a path that has visited exactly the cities in mask (which always includes city 0) and currently sits at city last. A few entries, built up mask by mask:
| mask (binary) | Cities visited | last | dp[mask][last] | How it was reached |
|---|---|---|---|---|
| 0001 | {0} | 0 | 0 | base case: start at city 0 |
| 0011 | {0,1} | 1 | 10 | dp[0001][0] + cost[0][1] = 0+10 |
| 0101 | {0,2} | 2 | 15 | dp[0001][0] + cost[0][2] = 0+15 |
| 1001 | {0,3} | 3 | 20 | dp[0001][0] + cost[0][3] = 0+20 |
| 1111 | {0,1,2,3} | 1 | 70 | best of two ways to reach all four cities ending at 1 |
| 1111 | {0,1,2,3} | 2 | 65 | best of two ways to reach all four cities ending at 2 |
| 1111 | {0,1,2,3} | 3 | 75 | best of two ways to reach all four cities ending at 3 |
Closing the tour back to city 0 from each full-mask entry gives 70+10=80, 65+15=80, and 75+20=95. The minimum is 80, achieved either as 0→1→3→2→0 or 0→2→3→1→0.
Notice what made this tractable: instead of trying all $3! = 6$ orderings of the non-start cities individually (which would not scale), the DP shares work across orderings that visit the same set of cities and end at the same city — regardless of the order those cities were visited in. That sharing is exactly what the bitmask state buys.
4. Subset States: Traveling Salesman Problem
dp[mask][last] = the minimum cost of a path that starts at city 0, visits exactly the cities in mask (city 0 ∈ mask always), and ends at city last ∈ mask. The recurrence extends every such path by one more unvisited city: dp[mask | (1<<next)][next] = min(dp[mask][last] + cost[last][next]) over every last ∈ mask and every next ∉ mask. The answer is min over last of dp[full][last] + cost[last][0], where full = (1<<n)-1.Processing masks in increasing numeric order guarantees correctness without recursion: every transition goes from mask to mask | (1<<next), which is numerically larger, so by the time a mask is read as a source it has already received every update from its predecessors.
All three panels implement the same recurrence verified by hand above and print 80. The DP has $2^n \cdot n$ states, each with an $O(n)$ transition, so it explores the search space that would otherwise require $O(n!)$ orderings in $O(2^n \cdot n^2)$ time.
5. Assignment DP and Set Partition DP
dp[mask] = the minimum cost of assigning the tasks in mask to the first popcount(mask) workers. Because exactly one worker is assigned per task and workers are assigned in a fixed order, the identity of "which worker is next" is not extra state — it is popcount(mask), the count of tasks already given out. The recurrence tries every unassigned task for that next worker: dp[mask | (1<<task)] = min(dp[mask] + cost[popcount(mask)][task]).This is a strictly smaller state than the TSP DP above — dp[mask] instead of dp[mask][last] — because the "current position" (which worker is being assigned) is a deterministic function of the mask rather than independent information that must be tracked alongside it.
Checking by brute force: of the $3! = 6$ ways to assign 3 tasks to 3 workers, worker0→task1 (2), worker1→task0 (6), worker2→task2 (1) totals 9, and no other permutation does better. The DP explores $O(2^n)$ masks with $O(n)$ transitions each, so it finds this minimum in $O(n \cdot 2^n)$ instead of $O(n!)$.
Set Partition DP
A related state, dp[mask] = the minimum number of valid subsets needed to partition exactly the elements in mask, where "valid" means some condition holds for a subset (here, its sum does not exceed a limit). Instead of trying one next element, the recurrence tries every valid first subset as a submask of mask: dp[mask] = min over valid submask sub of mask of dp[mask ^ sub] + 1, with dp[0] = 0. This uses the submask-enumeration trick from the next section applied inside a DP transition, rather than to a standalone aggregate.
With values = [4, 5, 6, 3] and limit = 9, the total sum is 18, so a single subset cannot hold everything. But {4, 5} and {6, 3} each sum to exactly 9, so two partitions suffice — matching the printed result.
6. Submask Enumeration
for (sub = mask; sub > 0; sub = (sub - 1) & mask) visits every nonzero submask of mask exactly once, from mask itself down to its lowest single set bit. Subtracting 1 from sub borrows from the lowest set bit of sub, flipping a run of low bits; ANDing with mask then discards every flipped bit that mask does not itself have set, producing the next-smaller submask of mask in one step.Run this loop for every mask from 0 to (1<<n)-1 and the total number of (mask, submask) iterations across the whole outer loop is $O(3^n)$ — not the $O(4^n)$ a first, incorrect mental model might suggest from "for each of $2^n$ masks, for each of $2^n$ possible submasks."
Why $3^n$: fix one pair (mask, submask) with submask ⊆ mask, and look at bit position $i$ independently across all $n$ positions. Bit $i$ can be in exactly one of three states relative to that pair: absent from mask (and therefore absent from sub); present in mask but absent from sub; or present in both. Three independent choices per bit, across $n$ bits, gives $3^n$ total (mask, submask) pairs when the empty submask is included — and $3^n - 2^n$ when the loop skips it, since every one of the $2^n$ masks loses exactly its one empty-submask pairing. Both quantities are $\Theta(3^n)$.
7. Sum Over Subsets (SOS) DP
f[mask] for every mask in $[0, 2^n)$, define g[mask] = the sum of f[sub] over every submask sub of mask (including 0 and mask itself). Computing g for one mask by submask enumeration costs up to $O(2^n)$, and doing that for every mask costs $O(3^n)$ total. SOS DP instead computes every g[mask] simultaneously in $O(n \cdot 2^n)$ by processing one bit position at a time.Think of a conceptual layer dp[i][mask] = the sum of f[sub] over every submask sub of mask that agrees with mask on every bit above position $i$ — that is, only bits $0..i$ are allowed to differ between sub and mask. At layer $i=-1$, no bit is allowed to differ, so dp[-1][mask] = f[mask]. Moving from layer $i-1$ to layer $i$ additionally allows bit $i$ to be cleared: if bit $i$ of mask is set, the newly-allowed submasks are exactly the old ones with bit $i$ cleared, so dp[i][mask] = dp[i-1][mask] + dp[i-1][mask \oplus (1<<i)]; if bit $i$ of mask is already 0, nothing new becomes allowed and dp[i][mask] = dp[i-1][mask]. After all $n$ layers, dp[n-1][mask] = g[mask].
Because layer $i$ only ever reads layer $i-1$ at the same mask and at mask with bit $i$ cleared — and both of those already hold their final layer-$(i-1)$ value before layer $i$ begins — the layer dimension can be dropped and the update done in place, one bit at a time, over a single array:
The trace below follows f = [1, 2, 3, 4, 5, 6, 7, 8] (indexed by 3-bit mask) through each bit pass, matching the printed output exactly:
| mask (binary) | f[mask] | after bit 0 | after bit 1 | after bit 2 (= g[mask]) |
|---|---|---|---|---|
| 000 | 1 | 1 | 1 | 1 |
| 001 | 2 | 3 | 3 | 3 |
| 010 | 3 | 3 | 4 | 4 |
| 011 | 4 | 7 | 10 | 10 |
| 100 | 5 | 5 | 5 | 6 |
| 101 | 6 | 11 | 11 | 14 |
| 110 | 7 | 7 | 12 | 16 |
| 111 | 8 | 15 | 26 | 36 |
Spot-check mask = 111: its submasks are all eight masks 000–111, so g[111] must equal the sum of every f value, $1+2+\dots+8=36$ — which is exactly what both the bit-by-bit trace and the code print.
Zeta Transform, Möbius Transform, and OR/AND/XOR Connections
The transform SOS DP computes is called the zeta transform: g = ζ(f), where g[mask] sums f over all subsets of mask. It is the building block of OR-convolution: the array $h$ with $h[\text{mask}] = \sum_{a \mathbin{|} b = \text{mask}} f[a]\,g[b]$ can be computed by taking the zeta transform of $f$ and of $g$, multiplying the two transformed arrays pointwise, and inverse-transforming the result.
That inverse is the Möbius transform: it recovers f from g using inclusion–exclusion, by running the identical bit-by-bit loop with subtraction instead of addition:
# Given g (the zeta transform of f), recover f in place:
for i in range(n):
for mask in range(1 << n):
if mask & (1 << i):
g[mask] -= g[mask ^ (1 << i)]
# g now holds the original f again (this is the Moebius / inverse transform)
Running this on the g = [1, 3, 4, 10, 6, 14, 16, 36] computed above peels bit 0, then bit 1, then bit 2, and returns exactly [1, 2, 3, 4, 5, 6, 7, 8] — the original f.
The same divide-and-conquer-over-bits pattern generalizes in two more directions. Summing over supermasks instead of submasks (mirror the trick: for each bit, propagate from the mask with the bit set into the mask without it) computes the analogous transform underlying AND-convolution. And processing bits with a different pairwise combine — add/subtract on the two halves rather than one-sided propagation — generalizes to the Fast Walsh–Hadamard Transform, which underlies XOR-convolution. FWHT is a more advanced technique left to a later point in the curriculum; it is named here only so the family of bit-by-bit transforms is recognizable when it appears.
8. Complexity Analysis
- Basic subset-state DP (
dp[mask], one value per mask, $O(1)$ transition per mask such as Assignment DP): $O(2^n)$ time and space. - TSP-style DP (
dp[mask][last], $O(n)$ transitions per state): $O(2^n \cdot n)$ states $\times$ $O(n)$ work $= O(2^n \cdot n^2)$ time, $O(2^n \cdot n)$ space. - Naive submask enumeration across all masks (e.g. Set Partition DP as written): $\Theta(3^n)$ time.
- Sum-Over-Subsets DP: $O(n \cdot 2^n)$ time and $O(2^n)$ space — an exponential improvement over the $\Theta(3^n)$ that per-mask submask enumeration would cost for the same aggregate.
Every one of these bounds still has an exponential factor of $2^n$ or worse, so bitmask DP is only practical while $n$ stays small. As a rule of thumb, $2^{20} \approx 10^6$ is comfortable, $2^{22}$–$2^{24}$ is often the practical ceiling depending on the constant work per state, and beyond roughly $n = 25$ the table itself becomes too large to hold in memory regardless of how fast each transition is. This is a hard qualitative cutoff, not a tuning parameter: past it, the technique needs to be replaced — with meet-in-the-middle, a smaller or approximate state, or an entirely different algorithm — rather than optimized further.
9. Applications and Alternatives
Where bitmask state shows up in practice
- Route and scheduling optimization on small fleets: exact TSP and vehicle-routing subproblems over a handful of stops, used as an inner routine inside a larger heuristic solver.
- Task and resource assignment: the Assignment DP pattern generalizes to any one-to-one matching with a small side (workers-to-machines, shifts-to-slots) where the Hungarian algorithm's $O(n^3)$ is either unnecessary or the extra bitmask-DP structure (partial assignments, side constraints) does not fit that algorithm's model.
- Competitive programming set-cover and partition variants: the Set Partition DP shape recurs constantly under different validity conditions on the "next subset."
- Feature/interaction analysis and subset-sum style aggregation: SOS DP appears whenever an answer needs "for every subset, an aggregate over its sub-subsets" — frequency tables over bitmask-encoded categorical features are a common source.
The chief alternative to bitmask DP is not a different DP shape but a different exponent: when $n$ is too large for $2^n$, meet-in-the-middle splits the elements into two halves, enumerates $2^{n/2}$ possibilities for each half, and combines them, trading a squared-root improvement in the exponent for extra combination logic. When the state genuinely needs to track more than one number per subset, a plain array-indexed DP (as in the general Dynamic Programming Foundations lesson) is usually simpler and should be preferred unless the state really is "which elements are used."
10. Exercises and Mastery
Easy
- Five cities 0–4 have cost matrix
cost[i][j]of your choosing (symmetric, cost[i][i]=0). Tracedp[mask][last]for at least three intermediate masks by hand, then run the TSP code from Section 4 and confirm the minimum tour cost matches your trace.
Medium
- Build a 4×4 cost matrix for the Assignment Problem where the optimal assignment is not the one obtained by greedily picking each worker's cheapest remaining task. Run the Assignment DP from Section 5 and verify it finds the true optimum, then show by brute force over all 24 permutations that no cheaper assignment exists.
Hard
- A common competitive-programming pattern: given
f[mask]for all $2^n$ masks, compute, for every mask, the maximum value off[sub]over all submaskssubofmask(instead of the sum). Adapt the SOS DP transition in Section 7 — replaceg[mask] += g[mask ^ (1<<i)]with a max update — and argue why the same $O(n \cdot 2^n)$ bound and the same bit-by-bit correctness argument still apply.
Review rhythm: tomorrow, rederive the TSP and Assignment DP recurrences from memory and explain in one sentence each why their state sizes differ; next week, implement SOS DP without looking it up and verify it against brute-force submask enumeration on a random small array; several weeks later, recognize a "bitmask as state" opportunity in an unrelated problem, and separately recognize a "sum/max over submasks for every mask" opportunity that calls for SOS DP instead of enumeration. Finally, teach someone the $3^n$ submask-enumeration argument using the three-choices-per-bit reasoning, not just the formula.
11. Summary Cheat Sheet
- Bitmask DP encodes "which elements have been used" as an integer state when $n$ is small (roughly $n \le 20$–22).
- TSP state:
dp[mask][last], minimum cost to have visited exactlymaskending atlast; $O(2^n \cdot n^2)$ time. - Assignment state:
dp[mask], next worker ispopcount(mask); $O(n \cdot 2^n)$ time. - Set Partition state:
dp[mask], minimum valid subsets to exactly covermask, trying every valid submask as the first piece. - Submask enumeration:
for (sub = mask; sub > 0; sub = (sub-1) & mask)visits every nonzero submask ofmask; total work across all masks is $\Theta(3^n)$, from three independent choices per bit. - SOS DP computes
g[mask] = sum of f[sub] over all submasks, for every mask at once, in $O(n \cdot 2^n)$ by processing one bit at a time. - SOS DP is the zeta transform; subtracting instead of adding gives the inverse Möbius transform; the same bit-by-bit idea generalizes to AND-convolution (supermasks) and, in more advanced form, to XOR-convolution via the Fast Walsh–Hadamard Transform.
- Past roughly $n=20$–22, replace the technique — meet-in-the-middle, a smaller state, or a different algorithm — rather than trying to optimize the constant factor.