Bitmask DP
Encode "which items are already used" as the bits of an integer so that DP can range over every subset, turning factorial brute force into roughly 2^n work.
Instinct n is tiny (about 20 or less) and you must use, visit or assign every item - make the set of used items a bitmask and DP over all 2^n subsets.
Memory hack Small n, big choice - let the bits keep score: each item is a light switch and the mask is the whole switch panel.
At a glance
When to use it
- n is tiny, roughly 20 or less, and every item must be used, visited or assigned
- Only which items are done matters, plus maybe one fact like the current node
- You must split items into groups, sessions or buckets and optimise the result
- Brute force over all orders would be n! and too slow
Key idea
Encode the set of used items as the bits of an integer and DP over all 2^n masks (plus a last-item dimension if needed), merging every ordering that used the same set and cutting n! work to about 2^n x n.
Typical complexity
O(2^n x n) time (O(3^n) with submask loops), O(2^n) space
| # | Common problems | Level | Our solution | Practice |
|---|---|---|---|---|
| 1 | Fair Distribution of Cookies | Medium | Read solution | LeetCode ↗ |
| 2 | Minimum Number of Work Sessions to Finish the Tasks | Medium | Read solution | LeetCode ↗ |
| 3 | Shortest Path Visiting All Nodes | Hard | Read solution | LeetCode ↗ |
The instinct
Brute force over orders is n!; brute force over subsets is 2^n. When the problem only cares about which items have been handled (plus maybe one extra fact such as "where am I now"), not the exact order, collapse all orders that used the same set into one state. An int with n bits is a perfect, hashable, array-indexable set. With n ≤ 20, 2^20 ≈ 10^6 states fit easily.
Recognise it
| Signal in the problem | Why it points here |
|---|---|
Constraints like n ≤ 12, n ≤ 16, n ≤ 20 |
Exponential in n is intended; 2^n fits, n! does not |
| "Visit every node / city at least once, shortest route" (Shortest Path Visiting All Nodes) | State = (current node, visited set) |
| "Assign each task / job to one worker / session" (Minimum Number of Work Sessions to Finish the Tasks) | Partition the item set into groups |
| "Distribute bags to k children, minimise the maximum" (Fair Distribution of Cookies) | Choose a submask per child |
| "Match each element of A with a unique element of B" | dp over mask of used B items; next A index = popcount(mask) |
| "All items used exactly once", "permutation with minimum cost" | Order matters only through the last item, so mask + last |
| Words like "subset", "group", "pair up", "select a team covering all skills" | Sets of items are the natural state |
Not this pattern if...
nis 40+ (consider meet-in-the-middle) or 10^3+ (it is knapsack, greedy or graph search).- Items are interchangeable and only the count or sum matters - plain knapsack is enough.
- You only need to list subsets, not optimise over them - that is backtracking.
Memory hack
Small n, big choice - let the bits keep score. Picture n light switches. Bit i on means item i is done. Flipping one more switch on is a transition; all switches on is the goal.
Analogy: a festival wristband with a row of stamps. The bouncer at each stage only cares which stamps you already have and which stage you are standing at - not the order in which you collected them.
DP framing:
- State -
dp[mask]ordp[mask][last]: best cost having used exactly the items inmask. - Choice - the next unused item to add, or a whole submask to form the next group.
- Base case -
dp[0] = 0, ordp[1 << start][start] = 0for path problems. - Order - increasing
maskworks, because adding bits only makes the integer bigger (BFS if every move costs 1).
Say it out loud in the interview:
- "n is at most 20, so I can afford 2^n states; bit i means item i is used."
- "The extra dimension I need is ... (last node / current group load / nothing)."
- "Transitions add one bit or a submask; answer is at the full mask
(1 << n) - 1."
How it works
Trace for Minimum Number of Work Sessions: tasks [1, 2, 3], session length 3. dp[mask] = fewest sessions to finish the tasks in mask; a submask may form one session if its sum ≤ 3.
| mask | tasks | sum | best split | dp |
|---|---|---|---|---|
| 000 | - | 0 | base | 0 |
| 001 | {1} | 1 | one session | 1 |
| 010 | {2} | 2 | one session | 1 |
| 011 | {1,2} | 3 | one session | 1 |
| 100 | {3} | 3 | one session | 1 |
| 101 | {1,3} | 4 | {3} + dp[001] | 2 |
| 110 | {2,3} | 5 | {3} + dp[010] | 2 |
| 111 | {1,2,3} | 6 | {3} + dp[011] | 2 |
Every row only looks at smaller masks, so a simple increasing loop is a valid order. For unit-cost moves on a graph (visit all nodes) the same (node, mask) states are explored with BFS instead.
Java template
import java.util.*; class BitmaskDp { static final int INF = Integer.MAX_VALUE / 2; // 1) Path / TSP shape: dp[mask][last] = min cost to visit `mask`, ending at `last`. static int shortestCoverPath(int[][] cost) { int n = cost.length, full = (1 << n) - 1; int[][] dp = new int[1 << n][n]; for (int[] row : dp) Arrays.fill(row, INF); for (int s = 0; s < n; s++) dp[1 << s][s] = 0; // base: start anywhere for (int mask = 1; mask <= full; mask++) { for (int last = 0; last < n; last++) { if ((mask & (1 << last)) == 0 || dp[mask][last] == INF) continue; for (int nxt = 0; nxt < n; nxt++) { if ((mask & (1 << nxt)) != 0) continue; // already used int nm = mask | (1 << nxt); dp[nm][nxt] = Math.min(dp[nm][nxt], dp[mask][last] + cost[last][nxt]); } } } int best = INF; for (int last = 0; last < n; last++) best = Math.min(best, dp[full][last]); return best; } // 2) Partition shape: split all items into the fewest groups whose sum <= cap. static int minGroups(int[] items, int cap) { int n = items.length, full = (1 << n) - 1; int[] sum = new int[1 << n]; for (int mask = 1; mask <= full; mask++) { int low = Integer.numberOfTrailingZeros(mask); sum[mask] = sum[mask & (mask - 1)] + items[low]; // reuse smaller subset } int[] dp = new int[1 << n]; Arrays.fill(dp, INF); dp[0] = 0; for (int mask = 1; mask <= full; mask++) { for (int sub = mask; sub > 0; sub = (sub - 1) & mask) { // all non-empty submasks if (sum[sub] <= cap && dp[mask ^ sub] != INF) { // problem-specific "group is valid" dp[mask] = Math.min(dp[mask], dp[mask ^ sub] + 1); } } } return dp[full]; } }
Complexity
| Shape | Time | Space |
|---|---|---|
dp[mask], add one item |
O(2^n · n) | O(2^n) |
dp[mask][last] (TSP / Hamiltonian) |
O(2^n · n²) | O(2^n · n) |
| Submask enumeration over all masks | O(3^n) | O(2^n) |
BFS over (node, mask) |
O(2^n · (n + E)) | O(2^n · n) |
3^20 is about 3.5 · 10^9, so submask loops are realistic only up to about n = 15-16; above that, prefer one-item-at-a-time transitions.
Variations
- Hamiltonian path / TSP -
dp[mask][last]. Find the Shortest Superstring, classic travelling salesman. - BFS with a visited mask - unit edges, multiple starts at once. Shortest Path Visiting All Nodes.
- Assignment / matching - the next person is
popcount(mask), so the mask alone is the state. Minimum XOR Sum of Two Arrays. - Partition into groups - submask enumeration, or
dp[mask] = (groups, load of the open group)to stay at O(2^n · n). Minimum Number of Work Sessions to Finish the Tasks. - k buckets, minimise the max -
dp[child][mask]with submask choice, or backtracking with pruning. Fair Distribution of Cookies. - Game states - memoise win/lose on the set of numbers already taken. Can I Win.
- Profile DP - the mask is the shape of a grid column boundary (domino tilings); a harder relative.
Common mistakes
- Java precedence:
mask & 1 << i == 0does not compile (and in C it is wrong); always write(mask & (1 << i)) == 0. - Writing
sub = (sub - 1) & maskwith a loop condition that allowssub == 0- it wraps back tomaskforever. - Forgetting the extra dimension (e.g. current node) and merging states that are genuinely different.
- Memory blow-up:
int[1 << 20][20]is about 80 MB - check whetherlastcan be dropped or use BFS with a visited array. - Using
1 << nwithn >= 31in anint; switch tolongmasks or reconsider the approach.
The constraint is the clue and the trap. Bitmask DP on n = 25 with an O(3^n) submask loop is roughly 8 · 10^11 steps. Always multiply out states × transitions before you start coding.
All practice problems
3 problems on this site use this pattern, easiest first. Solve them in order and say the instinct out loud before you code.