Bitmask DP

Delivery routes, chip drilling, scheduling small teams: when the state has to remember which of n things are already done, store that set as the bits of an integer. For n up to about 20 this turns n! brute force into something that runs in a second.

advanced⏱ 12 min read
01

Learn

The idea, the mechanics and the cost.

01A subset is just an integer

Number the items 0..n−1. A subset is an n-bit number: bit j is 1 when item j is in the set. With 4 cities, 1011₂ = 11 means cities 1, 2 and 4 (bits 0, 1, 3).

All set operations become one CPU instruction:

  • is j in the set? mask & (1 << j)
  • add j: mask | (1 << j); remove j: mask & ~(1 << j)
  • the full set: (1 << n) − 1; all subsets: for mask in 0 .. 2ⁿ−1

So an array indexed by mask, dp[mask], stores one answer per subset: 2ⁿ entries. For n = 20 that is about a million, comfortable. For n = 30 it is a billion, too many. That limit tells you when to think of bitmask DP: the constraints say n ≤ 20 or so, and the problem needs to remember “which ones are used”.

Note that if mask′ adds an element to mask, then mask′ > mask, so increasing order is a valid fill order.

A subset is an integer: one bit per city1011city4321= 1011₂ = 11= {1, 2, 4}is j visited?mask & (1 << j)add jmask | (1 << j)all visitedmask == (1 << n) − 1

02Travelling salesman with dp[mask][i]

Visit every city exactly once and return to the start, minimising total distance. Brute force tries (n−1)! orders.

The key observation: once you have visited a set of cities and stand at city i, the order you visited them in no longer matters for the rest of the trip. So the state is

dp[mask][i] = the cheapest path that starts at city 1, visits exactly the cities in mask, and ends at i.

  • base: dp[{1}][1] = 0;
  • transition: from (mask, i) go to an unvisited j: dp[mask | j][j] = min(…, dp[mask][i] + D[i][j]);
  • answer: min over i of dp[full][i] + D[i][1], closing the loop.

For the 4 cities in the figure the best tour is 1 → 2 → 4 → 3 → 1 = 10 + 25 + 30 + 15 = 80. Store the predecessor of each state to print the route.

TSP: every city once, then home1015203525301234best: 1 → 2 → 4 → 3 → 1 = 10 + 25 + 30 + 15 = 80dp[mask][i]: visited mask, standing at i

03Exponential, but far less so, and a glimpse of NP

There are 2ⁿ·n states and each tries n next cities, so the time is O(2ⁿ·n²) with O(2ⁿ·n) memory. For n = 20 that is about 4·10⁸ simple steps, a second or two in C++. Brute force would need 20! ≈ 2.4·10¹⁸.

It is still exponential, and that is no accident. TSP is NP-hard: nobody knows a polynomial algorithm, and finding one would prove P = NP. Bitmask DP is the honest best for exact answers on small n. For larger inputs, practice switches to heuristics and approximations, which is where the capstone stage picks up.

The same pattern solves many “assign / order a small set” problems: Hamiltonian paths, assigning n workers to n jobs, packing items into the fewest trips, and more. When you see n ≤ 20, try writing the state as dp[mask] or dp[mask][last].

Operations (log scale)n=103.6·10^61.0·10^5n=151.3·10^127.4·10^6n=202.4·10^184.2·10^8all tours: n!bitmask DP: 2ⁿ·n²

Cost at a glance

TSP with bitmask DPO(2ⁿ·n²)
MemoryO(2ⁿ·n)
Brute force over all toursO(n!)

Remember

  1. A subset of n ≤ ~20 items fits in an integer’s bits, so dp[mask] stores one answer per subset.
  2. For TSP, “which cities are visited” plus “where am I now” is all the future needs, giving dp[mask][i].
  3. O(2ⁿ·n²) beats n! enormously but is still exponential, as expected for an NP-hard problem.
02

Play

Step through the algorithm, then try it on your own input.

👀 What to watch: Rows are masks (which cities are visited), columns the city you end at. Watch cells fill from small masks to the full 1111 row, then the tour closes back to city 1.

Interactive visualizerfocus here, then space ← →

Bitmask DP: TSP (4 cities, start 1)

mask\\end1234
00010···
0011····
0101····
0111····
1001····
1011····
1101····
1111····
Travelling Salesman: visit every city once and return, at minimum cost. State = (which cities visited as a bitmask, current city). Base: at city 1 alone, cost 0.

Pseudocode

 1 dp[mask][i] = cheapest path from city 1 visiting exactly `mask`, ending at i 2 base: dp[{1}][1] = 0 3 transition: dp[mask|1<<j][j] = min over i in mask of dp[mask][i] + D[i][j] 4 answer = min over i of dp[full][i] + D[i][1]   // close the tour
1 / 1
03

Check

Three questions. Pick an answer to see why.

Q1

With 5 items, which mask is the set {0, 2, 4}?

Q2

Why can dp[mask][i] forget the order in which the cities in mask were visited?

Q3

Roughly how many dp[mask][i] states are there for n = 16?

04

Practice

Real problems to lock it in, easiest first.