Bitmask DP

Harddynamic programming · bit manipulation · subsets · tsp · bfs⏱ 6 min readUpdated

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.

PatternBitmask DP

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

How to spot it If the problem says n is at most 12 to 20 and asks for the best way to visit, assign or group every item exactly once, think Bitmask DP first.

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...

  • n is 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

Tip

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] or dp[mask][last]: best cost having used exactly the items in mask.
  • Choice - the next unused item to add, or a whole submask to form the next group.
  • Base case - dp[0] = 0, or dp[1 << start][start] = 0 for path problems.
  • Order - increasing mask works, because adding bits only makes the integer bigger (BFS if every move costs 1).

Say it out loud in the interview:

  1. "n is at most 20, so I can afford 2^n states; bit i means item i is used."
  2. "The extra dimension I need is ... (last node / current group load / nothing)."
  3. "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

java
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 == 0 does not compile (and in C it is wrong); always write (mask & (1 << i)) == 0.
  • Writing sub = (sub - 1) & mask with a loop condition that allows sub == 0 - it wraps back to mask forever.
  • 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 whether last can be dropped or use BFS with a visited array.
  • Using 1 << n with n >= 31 in an int; switch to long masks or reconsider the approach.
Warning

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.

My notes saved in this browser