Bitmask DP
State is a bitmask encoding which of n ≤ ~20 elements are used, plus optionally the last element; transitions add one bit.
Overview
When the "history" that matters is which elements have been used and n is small (≤ 20–22), encode that set as an integer bitmask: bit i set means element i is used. There are 2^n masks, so tables of size 2^n or 2^n · n fit in memory (2^20 · 20 ≈ 2·10^7). Transitions add one bit (mask | (1 << i)), so the natural order is increasing mask value — every predecessor is numerically smaller.
The flagship is Held-Karp for the Travelling Salesman Problem: dp[mask][v] = shortest path that visits exactly the cities in mask and ends at v, O(2^n · n²) — for n = 16 that is 16.7 million transitions, versus 15! ≈ 1.3·10^12 permutations. Others: assignment problems (dp[mask] = min cost assigning the first popcount(mask) workers to the jobs in mask), Shortest Path Visiting All Nodes (BFS over (mask, node)), partition into k equal-sum subsets, counting Hamiltonian paths, and sum-over-subsets (SOS) DP for aggregating over all submasks in O(2^n · n).
Bit tricks used constantly: mask & (1 << i) test, mask ^ (1 << i) remove, mask & -mask lowest set bit, sub = (sub - 1) & mask to enumerate submasks (total 3^n over all masks). See Bit Masks and Subset Generation with Bitmasks.
Intuition
A mental model before the formal terms.
A row of n light switches records which tasks are done. The table has one cell per switch pattern (and, for path problems, per "where I am standing"). To fill a cell you look at patterns with exactly one fewer light on — the pattern you were in just before flipping the last switch. Since flipping a switch on always increases the binary number, filling cells in numeric order is safe.
The exponential is unavoidable — you are genuinely tracking all subsets — but 2^n subsets is enormously smaller than n! orderings, and that gap is what makes n = 20 feasible.
How it works
- State:
dp[mask]ordp[mask][last].mask= set of used elements;last= the element that will constrain the next step (current city, last job) when the transition cost depends on it. - Transition: for each unset bit
jofmask:dp[mask | 1<<j][j] = best(dp[mask][last] + cost(last, j)). Or, pulling:dp[mask][j] = best over last in mask\{j} of dp[mask ^ 1<<j][last] + cost(last, j). - Base cases:
dp[1 << start][start] = 0; everything elseINF(or0for counting). For assignment,dp[0] = 0. - Order: increasing
mask(transitions only add bits). Table: flat arrays of size2^nor2^n × n. Answer:dp[full][*]combined as required (e.g. plus return edge for TSP tour). - Optimization: when the transition depends only on
popcount(mask)(assignment), drop the second index; useint32arrays; prune masks that are unreachable.
Why it works
Two partial tours that visited the same set of cities and end in the same city have identical futures — the remaining cities and the current position are all that matters. So (mask, last) is a complete state, and only the best value per state needs to be kept.
Since mask | bit > mask, increasing numeric order is a topological order of the state DAG, and each of the 2^n · n states does O(n) work.
Recognition
How to tell a problem wants this.
n ≤ 20(occasionally 22–24) with a problem that is otherwise about permutations, subsets, or assignments — the constraint is the tell.- "Visit every node", "assign each task to a distinct worker", "partition into groups", "count subsets with property".
- A brute force would enumerate orderings (
n!) but only the set of used items matters for the future.
Interactive visualization
Play, step, change the input. ← → and space work too.
| end 0 | end 1 | end 2 | end 3 | |
|---|---|---|---|---|
| 0000 | ∞ | ∞ | ∞ | ∞ |
| 0001 | 0 | ∞ | ∞ | ∞ |
| 0010 | ∞ | ∞ | ∞ | ∞ |
| 0011 | ∞ | ∞ | ∞ | ∞ |
| 0100 | ∞ | ∞ | ∞ | ∞ |
| 0101 | ∞ | ∞ | ∞ | ∞ |
| 0110 | ∞ | ∞ | ∞ | ∞ |
| 0111 | ∞ | ∞ | ∞ | ∞ |
| 1000 | ∞ | ∞ | ∞ | ∞ |
| 1001 | ∞ | ∞ | ∞ | ∞ |
| 1010 | ∞ | ∞ | ∞ | ∞ |
| 1011 | ∞ | ∞ | ∞ | ∞ |
| 1100 | ∞ | ∞ | ∞ | ∞ |
| 1101 | ∞ | ∞ | ∞ | ∞ |
| 1110 | ∞ | ∞ | ∞ | ∞ |
| 1111 | ∞ | ∞ | ∞ | ∞ |
1dp[1][0] = 0 // visited {0}, ending at 02for mask in increasing order, for u in mask with dp[mask][u] < ∞:3 for v not in mask:4 dp[mask | 1<<v][v] = min(dp[mask | 1<<v][v], dp[mask][u] + d[u][v])5answer = min over u of dp[FULL][u] + d[u][0]Pseudocode
1# TSP (Held-Karp): dp[mask][v] = min cost path visiting mask, ending at v2dp[1 << 0][0] = 0; all else INF3for mask in 1..2^n - 1:4 for v in mask:5 if dp[mask][v] == INF: continue6 for u not in mask:7 dp[mask | 1<<u][u] = min(dp[mask | 1<<u][u], dp[mask][v] + dist[v][u])8return min over v of dp[full][v] + dist[v][0]Implementations
1import math2 3# Bitmask DP: when the state is "which subset of a small set have I used",4# encode that subset as the bits of an integer. n up to about 20 is the5# practical ceiling (2^20 states). Representative example: assignment problem6# (n tasks to n workers at minimum cost), plus Held-Karp for the TSP.7 8 91 · dp[mask] = min cost to assign the first popcount(mask) workers to the10# tasks in mask. The worker index is implied by how many bits are set.11def min_assignment_cost(cost: list[list[int]]) -> float:12 n = len(cost)13 full = 1 << n14 dp = [math.inf] * full15 dp[0] = 016 17 for mask in range(full):18 if dp[mask] == math.inf:19 continue20 worker = bin(mask).count("1") # next worker to assign (int.bit_count in 3.10+)21 if worker == n:22 continue232 · Try every still-free task for this worker24 row = cost[worker]25 for task in range(n):26 if mask & (1 << task):27 continue # already taken28 nxt = mask | (1 << task)29 if dp[mask] + row[task] < dp[nxt]:30 dp[nxt] = dp[mask] + row[task]31 return dp[full - 1]32 33 343 · Held-Karp: dp[mask][last] = shortest path visiting mask, ending at last35def tsp(dist: list[list[int]]) -> float:36 n = len(dist)37 if n == 0:38 return 039 full = 1 << n40 dp = [[math.inf] * n for _ in range(full)]41 dp[1][0] = 0 # start at city 042 43 for mask in range(1, full):44 if not mask & 1:45 continue # every tour includes city 046 row = dp[mask]47 for last in range(n):48 if not mask & (1 << last) or row[last] == math.inf:49 continue504 · Extend the path to any unvisited city51 base = row[last]52 dist_last = dist[last]53 for nxt in range(n):54 if mask & (1 << nxt):55 continue56 nm = mask | (1 << nxt)57 if base + dist_last[nxt] < dp[nm][nxt]:58 dp[nm][nxt] = base + dist_last[nxt]59 605 · Close the tour by returning to city 061 if n == 1:62 return 063 return min(64 (dp[full - 1][last] + dist[last][0] for last in range(1, n) if dp[full - 1][last] < math.inf),65 default=math.inf,66 )bin(mask).count("1")is the portable population count;int.bit_count()is the fast built-in from Python 3.10 and is noted in the comment.- Python integers are unbounded, so
1 << nis exact for any n — the only limit is memory for 2^n states, not the word size. row = cost[worker],base = row[last]anddist_last = dist[last]hoist repeated lookups out of the innermost loop, which matters a lot in CPython.- The explicit
if base + d < dp[nm][nxt]comparison instead ofmin(...)avoids a function call in the hot loop. min(generator, default=math.inf)handles the case where no tour completes, without a separate emptiness check.
Python is roughly 50x slower than C++ here, so the practical ceiling drops to about n = 15 rather than n = 20.
int.bit_count()(3.10+) is a single fast call;bin(x).count("1")builds a string and is much slower in a hot loop.- Python integers are arbitrary precision, so bit masks never overflow — the one language here with no shift-width limit.
min(gen, default=...)is the clean way to handle a possibly-empty minimisation.scipy.optimize.linear_sum_assignmentsolves the assignment problem in O(n^3) via the Hungarian algorithm and should be used for anything but a demonstration.
- Using
bin(mask).count("1")inside the innermost loop rather than once per mask. - Calling
min()in the hot loop instead of an explicit comparison, which adds a Python-level call per candidate. - Reaching for bitmask DP on the assignment problem at all, when
scipysolves it in polynomial time.
- Population count: C++ has
__builtin_popcount/std::popcount, Python hasint.bit_count()(3.10+), and JS/TS have nothing — the Kernighan loop is written out there. - Shift width: Python integers are unbounded, C++
intshifts are undefined behaviour at 31+, and JS/TS bitwise operators silently coerce to signed int32 — the same expression has three different failure modes. - The unreachable sentinel:
Infinityandmath.infsaturate safely under addition, while C++ needsINT_MAX / 4headroom to avoid overflowing before the guard runs. - Only Python has a library escape hatch (
scipy.optimize.linear_sum_assignment) that makes the assignment problem polynomial rather than exponential.
Complexity
n = 20 → 2^20 ≈ 10^6 masks; n = 16 with n² transitions ≈ 1.7·10^7 operations.
Compare growth rates in the Complexity Explorer →When to use — and when not to
n ≤ 20and the future depends only on the set of used elements (plus possibly the last one).- Hamiltonian path/cycle, assignment, set partition, "visit all" problems.
- Aggregating a function over all subsets or all submasks (SOS DP).
n > 25—2^nis infeasible; look for structure (greedy, matching, flow, meet-in-the-middle forn ≤ 40).- Only the count of used elements matters, not which — a plain 1D/2D DP suffices.
- The problem is an assignment with a bipartite structure and large
n— use the Hungarian algorithm or min-cost flow instead.
Alternatives
Common mistakes
- Iterating masks in an order that is not topological for the transition used (e.g. removing bits while iterating upward).
- Allocating
dp[2^n][n]with 64-bit values forn = 20(160 MB) — use 32-bit or drop a dimension. - Off-by-one on
1 << nvs(1 << n) - 1; forgetting the start-city bit in the initial mask. - Using
INFsums that overflow 32-bit ints when adding costs; useINF = 1e9, notINT_MAX. - Mixing up "in mask" tests:
(mask >> i) & 1vsmask & i.
Interview patterns
- Shortest Path Visiting All Nodes (BFS on
(mask, node)). - Assignment / "Minimum Cost to Assign Jobs", Campus Bikes II, Number of Ways to Wear Different Hats.
- Partition to K Equal Sum Subsets, Matchsticks to Square (
dp[mask]with running remainder). - Count Hamiltonian paths; Parallel Courses II (submask enumeration).
- Coin ChangeIntermediate