Bitmask Dynamic Programming
When the state you need to remember is "which subset of a small set of items has been used so far," a bitmask stores that subset as one integer, and dynamic programming over it turns exponential brute force into a tractable table.
Prerequisites: Generating Permutations and Subsets, Knapsack Problems
The traveling salesman problem — visit every city exactly once and return home as cheaply as possible — looks like it needs brute force over every ordering. But you don't actually need the full order, only two facts at each step: which cities have already been visited, and which one you're currently standing at. Bitmask DP encodes "which subset has been visited" as a single integer, where bit is 1 if city has been visited, turning that exponential-looking state into something a table can hold.
The idea: state = (subset visited, current position)
Define as the minimum cost to have visited exactly the set of cities in mask, ending at city . The transition tries extending to every unvisited city :
In words: if you can reach city having visited exactly mask, then you can reach city having visited mask plus , at a cost of getting to plus one more hop. mask | (1 << j) sets bit — "add city to the visited set" — and mask & (1 << j) checks whether it's already set. There are possible masks and possible current cities, so the table has entries, and each transition is , giving total — astronomically better than for around 15-20 ( versus ), though still only viable for small , which is the honest limit of this technique.
Worked example: 3 cities, tiny TSP
Cities 0, 1, 2, symmetric costs , , , start and must return to city 0. Base case: (mask = 001). From there, extend to city 1: (mask 011). Extend to city 2: (mask 101). From \{0,1\} at city 1, extend to city 2: (mask 111). From \{0,2\} at city 2, extend to city 1: . Full mask 111 reached at city 2 with cost 30, or city 1 with cost 35; add the return trip home: , or — both routes tie at 45, which is the answer.
def tsp(cost, n): # O(2^n * n^2)
dp = [[float('inf')] * n for _ in range(1 << n)]
dp[1][0] = 0 # start at city 0, only city 0 visited
for mask in range(1 << n):
for i in range(n):
if dp[mask][i] == float('inf') or not (mask & (1 << i)):
continue
for j in range(n):
if mask & (1 << j):
continue # already visited
nmask = mask | (1 << j)
dp[nmask][j] = min(dp[nmask][j], dp[mask][i] + cost[i][j])
full = (1 << n) - 1
return min(dp[full][i] + cost[i][0] for i in range(n))
Bitmask DP works whenever the natural state is "which subset of a small item set has been used" — encode it as one integer, and mask | (1<<j) / mask & (1<<j) become the entire add/check vocabulary. It's only practical for up to roughly 20, since the table itself has rows.
The technique's ceiling is real, not a rule of thumb to relax under pressure: already exceeds 33 million rows before multiplying by transitions each. If is much above 20, bitmask DP is the wrong tool regardless of how naturally the state fits the pattern.
Where it shows up
Interview staples: traveling salesman itself, "shortest path visiting all nodes" (LeetCode 847), and assignment-style problems where each of workers must be matched to a distinct task. On a desk, small-scale routing problems — sequencing a handful of trade legs to minimize total market impact, or ordering a short list of rebalancing trades to minimize slippage — fit this pattern exactly when the leg count is small enough for to be tractable.
Practice in interviews
Further reading
- Cormen, Leiserson, Rivest & Stein, Introduction to Algorithms (ch. 15, traveling salesman variant)