Bitmask DP
Bitmask DP is the family of dynamic-programming algorithms whose state space is the powerset of a small ground set — that is, states correspond one-to-one with subsets
S ⊆ {0, 1, ..., n-1}, encoded asn-bit integers0 ≤ mask < 2^n. Treating subsets as integers turns set-membership tests into bit operations ((mask >> i) & 1) and set construction into bit twiddling (mask | (1 << i)), exposing an extremely cache-friendly, branch-free implementation that is dramatically faster than any pointer-based set representation. Bitmask DP is the standard tool when the optimal substructure decomposes naturally over which subset of items has already been used / visited / assigned, and the problem size is small enough to make2^ntractable: roughlyn ≤ 20for2^nenumeration, andn ≤ ~22forn · 2^n(the Travelling Salesman DP regime). Beyond this, the exponential explosion is fatal. The headline canonical applications are the Held-Karp algorithm for the Travelling Salesman Problem (O(n² · 2^n)exact DP), the assignment problem inO(n · 2^n)time, and set cover by full subset enumeration. A non-obvious technical gem associated with bitmask DP is the submask-iteration trick —for sub = mask; sub > 0; sub = (sub - 1) & mask— which enumerates exactly the non-empty subsets ofmaskinO(2^popcount(mask))per mask, leading to a total complexity ofO(3^n)rather than the naiveO(4^n)over all(mask, submask)pairs. The3^nbound, derivable in two lines from the binomial theorem, is one of the most beautiful counting arguments in algorithm design and shows up everywhere from set-cover DP to the SOS (Sum Over Subsets) DP technique used in Codeforces problems.
1. Intuition — Subsets Are Just Integers
The classical bottleneck in algorithms over subsets is representing the subsets. A Set<int> in Java costs ~50 bytes per element, supports O(1) membership only amortized over hashing overhead, and is heap-allocated — terrible for cache and for being a DP key. But for n ≤ 64, a subset S ⊆ {0, ..., n-1} fits in a single machine word: bit i is set iff element i is in S. The Set<int>’s slow operations become single CPU instructions:
| Set operation | Bit operation | CPU cost |
|---|---|---|
| Empty set | 0 | 0 cycles |
Universe {0..n-1} | (1 << n) - 1 | 1 cycle |
Add element i | `mask | (1 << i)` |
Remove element i | mask & ~(1 << i) | 2 cycles |
Test element i | (mask >> i) & 1 | 2 cycles |
| Union | `a | b` |
| Intersection | a & b | 1 cycle |
| Symmetric difference | a ^ b | 1 cycle |
| Cardinality | popcount(mask) | 1–3 cycles (hardware support) |
| Iterate elements | loop over set bits | O(popcount) |
Using subsets as DP keys turns the DP table into an array of size 2^n, indexed directly by the integer mask. Lookup is O(1) (one array access), comparison is one integer compare, and storage is 2^n · sizeof(state) bytes — very compact. For n = 20, that’s 2^20 ≈ 10^6 entries, easily fitting in cache.
A real-world analogy: imagine a hotel with 20 rooms and you want to enumerate every possible booking pattern (which subset of rooms is occupied). Instead of writing each pattern as a list ([3, 7, 12, 19]) you write it as a 20-character string of zeros and ones (00010001000010000001), then interpret that string as a number (0x...). Same information, vastly more compact, and every “is room 7 booked?” query becomes a one-instruction test. The whole hotel-state space — over a million configurations — fits easily in memory, and you can iterate over it in a single for loop.
The intellectual leap from “DP over subsets” to “Bitmask DP” is just committing to the integer encoding. Once committed, problems whose state was naturally “which items have I used so far” become trivially indexable.
2. The Submask-Iteration Trick and the O(3^n) Identity
The most distinctive technical move in bitmask DP is iterating over the submasks of a given mask. Suppose mask has k = popcount(mask) bits set. We want to iterate over all 2^k subsets of mask (without iterating over the full 2^n integers and filtering). The idiom:
sub := mask
while sub > 0:
process(sub)
sub := (sub - 1) & mask
process(0) # don't forget the empty subset, if needed
Why this works. The expression (sub - 1) & mask decrements within the bits of mask. Subtracting 1 from sub flips its lowest set bit and sets all lower bits; the & mask zeroes out everything not in mask, so the result is the largest submask of mask strictly less than sub. Iterating from sub = mask downward visits every non-zero submask exactly once, in lexicographically descending order, and terminates at sub = 0 (which the loop condition exits before processing — handle separately if needed).
The total work to enumerate all (mask, submask) pairs over all mask ∈ [0, 2^n) is
T = Σ_{mask=0}^{2^n − 1} 2^popcount(mask)
= Σ_{k=0}^{n} C(n, k) · 2^k [grouping masks by popcount]
= (1 + 2)^n [binomial theorem with x = 2, y = 1]
= 3^n.
Symbol-by-symbol unpacking:
popcount(mask)is the number of set bits inmask— its cardinality as a subset.2^popcount(mask)is the number of submasks ofmask.C(n, k) = n! / (k! · (n-k)!)is the number ofn-bit masks with exactlykbits set (choose whichkpositions are set).- The binomial identity
Σ_k C(n, k) · x^k = (1 + x)^n(the binomial theorem) collapses the sum to(1 + 2)^n = 3^n.
The 3^n bound is the reason bitmask DPs that iterate over submasks are strictly faster than O(4^n) (the naive double loop over mask and submask ⊆ {0..n-1}). For n = 20, 3^20 ≈ 3.5 × 10^9 (one second of tight assembly per ~10^9 ops) — borderline; 4^20 ≈ 10^12 is hopeless. For n = 16, 3^16 ≈ 4.3 × 10^7 — fast. Many bitmask DPs sit at this 3^n complexity (set cover by full enumeration, optimal partitioning, etc.).
A combinatorial restatement: each pair (mask, submask) with submask ⊆ mask ⊆ {0..n-1} corresponds to a function f : {0..n-1} → {out, in_submask, in_mask_not_submask} — i.e., a 3-coloring. There are 3^n 3-colorings; the equivalence is bijective.
3. Tiny Worked Example — Subset Sum (LC 416 Variant)
To anchor the bitmask-DP mechanics in a concrete problem, consider Subset Sum: given nums = [3, 1, 4, 2] (n = 4), determine whether some subset sums to a target T = 6. Bitmask DP is not the optimal algorithm for this (1D DP over the sum value is O(n · T)), but it is pedagogically useful because every subset is a state.
Define dp[mask] = True iff the subset of items indicated by mask sums to exactly T. Brute force: enumerate all 2^4 = 16 masks and check.
| mask (binary) | items | sum | dp[mask] |
|---|---|---|---|
| 0000 | {} | 0 | False |
| 0001 | {0} = {3} | 3 | False |
| 0010 | {1} = {1} | 1 | False |
| 0011 | {0,1} = {3,1} | 4 | False |
| 0100 | {2} = {4} | 4 | False |
| 0101 | {0,2} = {3,4} | 7 | False |
| 0110 | {1,2} = {1,4} | 5 | False |
| 0111 | {0,1,2} = {3,1,4} | 8 | False |
| 1000 | {3} = {2} | 2 | False |
| 1001 | {0,3} = {3,2} | 5 | False |
| 1010 | {1,3} = {1,2} | 3 | False |
| 1011 | {0,1,3} = {3,1,2} | 6 | True ✓ |
| 1100 | {2,3} = {4,2} | 6 | True ✓ |
| 1101 | {0,2,3} = {3,4,2} | 9 | False |
| 1110 | {1,2,3} = {1,4,2} | 7 | False |
| 1111 | {0,1,2,3} = {3,1,4,2} | 10 | False |
There are 2 valid subsets summing to 6: {3, 1, 2} (mask 1011) and {4, 2} (mask 1100).
State transition view (what makes this DP, not just enumeration): define dp[mask] = sum of items in mask. Then dp[mask] = dp[mask ^ low_bit] + nums[index_of_low_bit], where low_bit = mask & (-mask) extracts the lowest set bit. Filling the table in order of increasing mask automatically respects the dependency (lower-popcount masks come first because each one removes one bit). Once dp is filled, dp[mask] == T is the membership check.
For more interesting bitmask DPs, the state is not a simple sum; it is “the best value achievable by using some subset and ending at a specific item” (TSP) or “the minimum cost to cover all elements in mask with some subfamily of sets” (set cover). The shape of the recurrence varies, but the index space {0, 1, ..., 2^n − 1} is the same.
4. Worked Example — Minimum Cost to Assign N Workers to N Jobs (Assignment Problem)
The Assignment Problem: given an n × n cost matrix c[i][j] representing the cost of assigning worker i to job j, find the minimum-cost perfect matching between workers and jobs (every worker gets exactly one job, every job exactly one worker).
This problem is solvable in O(n³) by the [Hungarian Algorithm], but the bitmask DP gives a cleaner O(n · 2^n) solution that is good enough for n ≤ 20.
Define dp[mask] = minimum cost to assign workers 0, 1, ..., popcount(mask) - 1 to the jobs in mask. The interpretation: we process workers in fixed order 0, 1, 2, ...; the bits of mask indicate which jobs have already been taken. Worker popcount(mask) is the next worker to assign.
Recurrence. Let i = popcount(mask). To compute dp[mask], consider the choice of which job worker i was assigned. If worker i took job j (where j ∈ mask), then before worker i’s assignment, mask was mask ^ (1 << j) (job j not yet taken), and dp of that state plus c[i][j] is the candidate.
dp[mask] = min over j in mask of: dp[mask ^ (1 << j)] + c[popcount(mask) - 1][j]
Symbol-by-symbol unpacking:
mask= bitmask of jobs assigned so far (bitjset ↔ jobjtaken).popcount(mask)= number of workers assigned so far. The “current” worker (the one whose assignment we are reverse-engineering) is workerpopcount(mask) - 1(zero-indexed).j ∈ maskranges over jobs that workerpopcount(mask) - 1could have taken; for each, recurse onmaskwith that bit removed.c[i][j]is the cost of assigning workerito jobj, looked up from the input matrix.
Base: dp[0] = 0 (no workers assigned, no cost).
Answer: dp[(1 << n) - 1] (all jobs assigned).
4.1 Tiny Numerical Example
n = 3, costs:
job 0 job 1 job 2
w 0 3 4 6
w 1 2 5 8
w 2 7 9 1
Compute dp[mask] for mask = 0, 1, 2, ..., 7:
dp[000] = 0.dp[001]: 1 bit, worker 0 took job 0. Costc[0][0] = 3.dp = 0 + 3 = 3.dp[010]: worker 0 took job 1.c[0][1] = 4.dp = 4.dp[100]: worker 0 took job 2.c[0][2] = 6.dp = 6.dp[011]: 2 bits, worker 1’s choice. Either worker 1 took job 0 (then before, mask = 010, anddp[010] + c[1][0] = 4 + 2 = 6) or job 1 (dp[001] + c[1][1] = 3 + 5 = 8). Min:6.dp[101]: worker 1 took job 0 (dp[100] + c[1][0] = 6 + 2 = 8) or job 2 (dp[001] + c[1][2] = 3 + 8 = 11). Min:8.dp[110]: worker 1 took job 1 (dp[100] + c[1][1] = 6 + 5 = 11) or job 2 (dp[010] + c[1][2] = 4 + 8 = 12). Min:11.dp[111]: 3 bits, worker 2’s choice. Either job 0 (dp[110] + c[2][0] = 11 + 7 = 18) or job 1 (dp[101] + c[2][1] = 8 + 9 = 17) or job 2 (dp[011] + c[2][2] = 6 + 1 = 7). Min:7.
Answer: minimum cost is 7, achieved by worker 0 → job 0 (3), worker 1 → job 1 (5)? No wait — let me recompute. The optimal answer per dp[111] = 7 is: worker 2 took job 2 (cost 1), and the remaining dp[011] = 6 came from worker 0→job 1 (4) and worker 1→job 0 (2). So: w0→j1 (4) + w1→j0 (2) + w2→j2 (1) = 7. ✓
The trace is canonical bitmask DP: process workers in order, for each subset of jobs choose which job the next-to-assign worker took.
5. Pseudocode
BitmaskDP_Assignment(c, n):
INF := infinity
dp := array of size 2^n, all set to INF
dp[0] := 0
for mask := 0 to (2^n) - 1:
if dp[mask] == INF: continue
i := popcount(mask)
if i == n: continue # full assignment, done
for j := 0 to n - 1:
if (mask >> j) & 1 == 0: # job j not yet taken
new_mask := mask | (1 << j)
cost := dp[mask] + c[i][j]
if cost < dp[new_mask]:
dp[new_mask] := cost
return dp[(2^n) - 1]
Note the forward-DP style (push from each mask to its supersets) versus the backward-DP style (pull from each mask from its subsets shown in §4). Both produce the same result; forward is sometimes cleaner for sparse-state problems.
6. Python Implementations
6.1 Assignment Problem
def min_assignment_cost(c: list[list[int]]) -> int:
n = len(c)
INF = float('inf')
dp = [INF] * (1 << n)
dp[0] = 0
for mask in range((1 << n)):
if dp[mask] == INF:
continue
i = bin(mask).count('1') # popcount
if i == n:
continue
for j in range(n):
if not (mask >> j) & 1:
new_mask = mask | (1 << j)
if dp[mask] + c[i][j] < dp[new_mask]:
dp[new_mask] = dp[mask] + c[i][j]
return dp[(1 << n) - 1]
# Test
c = [[3, 4, 6], [2, 5, 8], [7, 9, 1]]
print(min_assignment_cost(c)) # 76.2 Subset Cover (Set Cover by Full Enumeration)
Given n elements (numbered 0..n-1) and m sets (each a bitmask), find the minimum number of sets whose union is the full universe.
def min_set_cover(sets: list[int], n: int) -> int:
full = (1 << n) - 1
INF = float('inf')
dp = [INF] * (1 << n)
dp[0] = 0
for mask in range((1 << n)):
if dp[mask] == INF:
continue
for s in sets:
new_mask = mask | s
if dp[mask] + 1 < dp[new_mask]:
dp[new_mask] = dp[mask] + 1
return dp[full] if dp[full] != INF else -1This is O(2^n · m). For exact set-cover from “best partition into rejected subsets” with submask iteration (e.g., LC 1125), the inner loop over sets becomes a submask enumeration, giving the O(3^n) complexity discussed in §2.
6.3 SOS (Sum Over Subsets) DP — The Submask-Iteration Workhorse
A classic bitmask-DP idiom: given f[mask] for every mask, compute g[mask] = Σ_{sub ⊆ mask} f[sub]. Naive: O(3^n). SOS DP: O(n · 2^n).
def sum_over_subsets(f: list[int], n: int) -> list[int]:
g = f[:]
for i in range(n):
for mask in range(1 << n):
if mask & (1 << i):
g[mask] += g[mask ^ (1 << i)]
return gThe trick: process bit i left-to-right, in-place. After processing bits {0, ..., i-1}, g[mask] holds the sum over all submasks differing from mask only in bits {0, ..., i-1}. After processing all n bits, g[mask] is the full sum-over-subsets. Bit-by-bit Möbius transform on the boolean lattice. Worth memorizing for advanced problems.
7. Complexity
Naive submask enumeration over all masks: O(3^n) — the identity derived in §2.
Layered bitmask DPs (assignment problem, TSP): O(n · 2^n) to O(n² · 2^n). Each subset is paired with O(n) extra state (the “current position” in TSP, or the “next worker” in assignment), and each transition is O(n) work to consider all extensions.
Practical limits.
| n | 2^n | n · 2^n | n² · 2^n | 3^n |
|---|---|---|---|---|
| 10 | 1024 | 10⁴ | 10⁵ | 6 · 10⁴ |
| 15 | 3·10⁴ | 5·10⁵ | 7·10⁶ | 1.4·10⁷ |
| 20 | 10⁶ | 2·10⁷ | 4·10⁸ | 3.5·10⁹ |
| 22 | 4·10⁶ | 9·10⁷ | 2·10⁹ | 3·10¹⁰ |
| 25 | 3·10⁷ | 8·10⁸ | 2·10¹⁰ | 8·10¹¹ |
A modern CPU sustains ~10⁹ simple operations/second. So:
2^nenumeration:n ≤ 25is fine (a second or two).n · 2^n:n ≤ 22comfortable.n² · 2^n(TSP-style):n ≤ 20borderline;n ≤ 18fast.3^n(subset/submask):n ≤ 18comfortable;n = 20is ~3.5 seconds.
Space: O(2^n) for the DP table. For n = 20, that’s 4 MB for 32-bit integers, 8 MB for 64-bit. Memory is rarely the binding constraint; CPU time is.
8. Diagram — The Subset Lattice
flowchart TD M111["111<br/>{0,1,2}"] M110["110"] M101["101"] M011["011"] M100["100"] M010["010"] M001["001"] M000["000<br/>{}"] M111 --> M110 & M101 & M011 M110 --> M100 & M010 M101 --> M100 & M001 M011 --> M010 & M001 M100 --> M000 M010 --> M000 M001 --> M000
What this diagram shows. The subset lattice of {0, 1, 2} (i.e., n = 3), drawn with the full universe 111 at the top and the empty set 000 at the bottom. Each node is a 3-bit mask, labeled by both its binary representation and the set it represents. Edges go from a mask to its immediate submasks (those differing by one bit removed). The lattice has 2^n = 8 nodes, and the total number of (mask, submask) ordered pairs (the count of distinct paths from any node down to any of its descendants, including itself) is 3^n = 27. The submask-iteration trick traverses, for each starting node, the set of nodes below or equal to it; summed over all starts, this is the 3^n count. The bitmask DP fills the lattice in topological order: for forward DPs, lower-popcount masks first; for SOS DP, bit-by-bit. The DP table is essentially this lattice with a value per node, which is why DP problems with “subset” state shape map so naturally onto bitmask DP.
9. Common Interview / Competitive Problems
| Problem | Source | Pattern |
|---|---|---|
| Travelling Salesman | classical / Held-Karp 1962 | dp[mask][i], O(n² · 2^n). See Travelling Salesman DP. |
| Assignment Problem | classical | dp[mask], O(n · 2^n) |
| Smallest Sufficient Team | LC 1125 | Set cover with skill-bitmask, O(2^n · m) |
| Maximum Compatibility Score Sum | LC 1947 | Bitmask matching, O(2^m · m) |
| Number of Ways to Wear Different Hats | LC 1434 | Bitmask over people, iterate hats |
| Minimum Cost to Connect Sticks | LC 1167 | Greedy, not bitmask (included as anti-pattern) |
| Maximum Students Taking Exam | LC 1349 | Row-by-row bitmask + valid configurations |
| Partition to K Equal Sum Subsets | LC 698 | dp[mask] boolean, partition correctness |
| Find Minimum Time to Finish All Jobs | LC 1723 | Worker × bitmask DP |
| Minimum Number of Refueling Stops | LC 871 | Heap, not bitmask (anti-pattern) |
| Shortest Path Visiting All Nodes | LC 847 | TSP-shaped, dp[mask][node] |
| Beautiful Arrangement | LC 526 | Bitmask permutation, O(n · 2^n) |
| Maximum AND Sum of Array | LC 2172 | Bitmask of slot occupancy, O(n · 3^k) |
| Subset Sum problem | classical | 1D DP over sum, not bitmask (anti-pattern) |
| Set Cover (general) | classical | NP-hard; bitmask DP is exact for small n |
The recognition signal: the problem has a small set of items (n ≤ 20) and asks for an optimal allocation/visit/ordering whose cost depends on which items are used. If you can encode “subset of items used so far” as a bitmask and the recurrence respects subset inclusion, you have a bitmask DP.
10. Variants and Optimizations
10.1 Permutations via Bitmask DP
Counting or optimizing over permutations of a small n-element set: state = (mask, current_position), transition = choose next element from ~mask. Reduces an O(n!) brute force to O(n² · 2^n) — the Held-Karp regime. See Travelling Salesman DP.
10.2 Two-Bit-Per-Position States
Some problems need a small auxiliary state per element (e.g., “for each element, is it included, excluded, or reserved?”). Use 2n bits — a state 0 to 4^n − 1 — with the same machinery. Costs another factor of 2 in memory but is straightforward.
10.3 Profile DP (Broken-Profile DP)
For problems on a small grid (e.g., domino tiling on m × n with m ≤ ~10), the bitmask represents the profile of one row’s occupancy, and the DP transitions row-by-row. State: dp[row][mask]. Time: O(rows · 2^cols · transitions). Used in classical tilings, including Markov-chain models on small grids. See cp-algorithms profile-DP article.
10.4 Sum-Over-Supersets (Dual SOS)
Symmetrically to SOS, you can compute g[mask] = Σ_{sup ⊇ mask} f[sup] in O(n · 2^n) by processing bits left-to-right and adding from supermasks. Same idea, reversed inequality.
10.5 Subset Sum Convolution
A more advanced operation: given f, g, compute h[mask] = Σ_{a ⊕ b = mask, a ∩ b = 0} f[a] · g[b] (the subset-sum convolution) in O(n² · 2^n). Used in advanced problems like counting Hamiltonian paths in subsets. The naive computation is O(3^n); the optimized version uses Möbius transforms.
10.6 Bitset DP
When n is too big for a single integer (n > 64) but states factor over small chunks, store the DP table as a bitset (array of 64-bit integers), and use vectorized bit operations (std::bitset in C++, numpy.packbits in Python). Often gives a constant-factor 8–32× speedup; not asymptotically better.
11. Pitfalls
11.1 Forgetting to Initialize dp[0]
The empty-subset base case (dp[0] = 0 typically) must be set before the loop. Forgetting it leaves dp[0] = INF, and the entire DP returns INF.
11.2 Incorrect popcount in Forward DP
When iterating mask in increasing order in the forward style (§5), popcount(mask) indicates “how many items already used” → “the next item to consider”. An off-by-one (popcount(mask) - 1 vs popcount(mask)) is the classic bug.
11.3 Iterating Wrong Range
for mask in range(1 << n) is the standard. range(2 ** n) works in Python but allocates a giant range object in some languages. range(0, (1 << n)) with an explicit start is safe everywhere.
11.4 Submask Loop Skips Empty Subset
The idiomatic while sub > 0; sub = (sub - 1) & mask exits before processing sub = 0. If your DP needs the empty subset (e.g., partitioning into non-empty pieces with cost-of-empty = 0), handle sub = 0 explicitly after the loop.
11.5 Confusing “iterate submasks” with “iterate single bits of mask”
for j in range(n): if (mask >> j) & 1: ... iterates single bits — O(popcount(mask)) per mask, O(n · 2^n) total. The submask iteration (for sub = mask; sub > 0; sub = (sub - 1) & mask) iterates all subsets of the bits, which is O(2^popcount(mask)) per mask, O(3^n) total. These are different operations; mixing them up changes the asymptotics.
11.6 Memory Blow-Up for n > 24
2^25 = 33M cells × 4 bytes = 128 MB; usually too much. Either reduce n, use a hash map for sparse states, or rethink the algorithm. The exponential is the binding constraint; don’t try to “optimize the constant” past n = 25.
11.7 Python-Specific: bin(mask).count('1') is Slow
For very tight loops (n = 20 with O(n · 2^n) inner loops), bin(mask).count('1') is ~10x slower than C’s __builtin_popcount. Use mask.bit_count() (Python 3.10+) for a 5–10× speedup, or precompute popcount[0..2^n] array if you call popcount many times per mask.
11.8 Confusing Held-Karp Indexing
In TSP DP, dp[mask][i] requires i ∈ mask (you must end at a city you have visited). Not enforcing this in code allows invalid states to leak into the recurrence and produces wrong answers. See Travelling Salesman DP §11.
11.9 Forgetting Symmetry-Breaking for Counting Problems
Some counting bitmask DPs over-count by assigning items in different orders. Process items in fixed order (item i corresponds to bit popcount(mask) typically) to break the symmetry.
11.10 Misunderstanding “popcount” in Multi-Worker DPs
When state encodes “subset of jobs taken” and workers are processed in fixed order, the next-worker-index is popcount(mask). Some problems instead encode “subset of workers used” (then next-job-index is popcount(mask)). Be explicit about which dimension is the subset and which is the implicit order.
12. Open Questions
- Is the
3^nsubmask-iteration bound tight? Yes, by the colorings argument: there are exactly3^nordered pairs(mask, submask)withsubmask ⊆ mask ⊆ {0..n-1}. Any algorithm enumerating them all isΩ(3^n). - When can we beat
2^non bitmask-DP-shaped problems? Subset Sum hasO(n · T)(knapsack) for smallT, beating2^nifT << 2^n. TSP has no known sub-2^nexact algorithm. Somen ≤ 30problems benefit from meet-in-the-middle (O(2^{n/2})). - What’s the right cutoff for “bitmask DP” vs “MILP / SAT / general optimization”? In practice, bitmask DP is competitive only for
n ≤ ~22. Beyond, modern SAT/MILP solvers with branch-and-bound dominate, even though they have worse worst-case bounds. - Can quantum computers do TSP faster than
O(2^n · poly)? Not yet known. Grover’s algorithm givesO(√(n!))for brute force search but no known speedup for the DP version.
12.1 TSP Exact and Approximate — As-of 2026-05
For exact metric TSP, Held-Karp’s Θ(n² · 2^n) time and Θ(n · 2^n) space remain the textbook standard (per Wikipedia: Held–Karp algorithm); no algorithm with worst-case complexity beating 2^n · poly(n) is currently known for general metric TSP. For approximation, Christofides–Serdyukov (1976) gave the long-standing 3/2-approximation for metric TSP. In 2020, Karlin, Klein, and Oveis Gharan introduced a randomized (3/2 − ε)-approximation with ε ≈ 10^{-36}, the first improvement over Christofides in more than 40 years; the paper won a best-paper award at STOC 2021 (per Wikipedia: Christofides algorithm and the STOC 2021 publication; see Quanta coverage). The improvement is mathematically dramatic but practically negligible — 10^{-36} is far below any realistic floating-point precision, so Christofides remains the engineering baseline.
13. See Also
- Travelling Salesman DP — the canonical
n² · 2^nbitmask DP - Subset Enumeration with Bitmasks — the underlying enumeration primitive (submask trick, single-bit iteration,
3^nidentity); this note layers DP semantics on top of those primitives - 01 Knapsack — DP with subset state, but the items kept are summarized by total weight (1D), not encoded as a bitmask. Useful contrast.
- Bit Manipulation Tricks — the underlying bit-twiddling vocabulary
- XOR Properties — a different bit-level technique
- Memoization vs Tabulation — bitmask DPs are typically tabulation-style
- DP State Identification — recognizing “subset of items” as a state
- Combinations — counting subsets, dual to bitmask enumeration
- Subsets — enumeration of subsets via backtracking; comparison with the bitmask enumeration
- Master Theorem — does not apply to bitmask DP (no subdivision into equal halves)
- Big-O Notation
- SWE Interview Preparation MOC