leetcode / Beyond the interview / Bitmask DP advanced

Bitmask DP

Some problems ask you to pick an order or an assignment for a small set of items, say up to 20, and trying every order costs n!, which is hopeless past about 11. Bitmask DP notices that the future usually depends only on which items are already used, not on the order they were used in. So it stores one answer per subset, writes each subset as the bits of an integer, and replaces n! orders with 2n sets. That turns 20 items from 2.4 × 1018 orders into about a million states. It is rare in interviews and no list problem needs it, but when the input is tiny and the obvious search is factorial, it is the tool to reach for.

On this page
  1. Problem
  2. Intuition
  3. Mechanics
  4. The bit idioms
  5. State and transition
  6. When the set is not enough
  7. Worked example
  8. Implementation
  9. Variations and edge cases
  10. Complexity and tradeoffs
  11. Problems
  12. Also uses this pattern
  13. Connections
  14. Read first
  15. Read next
  16. Check yourself
  17. References

Problem

The questions look like search problems: give each worker one job at the lowest total cost, visit every city once on the shortest tour, split numbers into k groups of equal sum, cover a list of skills with the smallest team. Each choice uses up an item, and later choices are only limited by what is used up. The constraint that gives it away is a tiny n, usually 12 to 20, which is too small for a polynomial algorithm to be the point and too big for n! to finish.

SignalPatternWhy
n ≤ 20 and "each item is used exactly once"DP over subsets, dp[mask]Which items are used is all the future needs, and there are only 2n such sets.
Assign n people to n tasks with a cost tabledp[mask], the next person is popcount(mask)People go in a fixed order, so the size of the mask already says whose turn it is.
Visit every node once, and the cost depends on where you standdp[mask][last] (Held–Karp)The next step's cost depends on the current node, so the state needs the last node as well as the set.
Shortest walk that visits every node, edges unweightedBFS over (node, mask)Every move costs 1, so breadth-first order reaches each state at its shortest distance.
Split items into groups of a fixed sum, n ≤ 16dp[mask] = fill of the current groupGroups can be filled one at a time, so the set used decides how full the open group is.
n in the hundreds, or the order does matter beyond the setNot this pattern2n explodes; look for a greedy rule, a polynomial DP or a matching algorithm.

Intuition

Three workers, A, B and C, must each take one of jobs 0, 1 and 2. Trying every order means 3! = 6 assignments, and for 20 workers, 20! of them. Look at what those orders have in common. Once A and B have taken jobs 0 and 1, between them, C must take job 2, whichever of the two orders put them there. Both partial assignments face the same future, so only the cheaper one can be part of the best answer. Keep that one and throw the other away.

That is the whole idea: for every set of jobs already given out, remember only the cheapest way to give them out. A set of jobs is a row of yes/no answers, and a row of yes/no answers is an integer written in binary. Set {0, 1} is 011 = 3, set {0, 2} is 101 = 5. The DP table is then a plain list indexed by those integers, and adding job j to a set is one bit operation.

Greedy reasoning does not survive this. If each worker in turn takes their cheapest free job, A grabs job 1 for 2, and the total is 10. The best total is 9, because the DP also keeps the partial assignments that look worse early on.

The subset lattice for assigning 3 jobs: each node is a mask with its cheapest cost, and the best path is highlighted Masks are grouped by how many jobs they contain, from 000 on the left to 111 on the right. Each arrow adds one job. Mask 000 costs 0. Mask 001 costs 9. Mask 010 costs 2. Mask 100 costs 7. Mask 011 costs 8. Mask 101 costs 12. Mask 110 costs 5. Mask 111 costs 9. The best path adds job 1, then job 0, then job 2, for a total of 9. +2 +6 +1 000 cost 0 001 cost 9 010 cost 2 100 cost 7 011 cost 8 101 cost 12 110 cost 5 111 cost 9 bits read job 2, job 1, job 0; column k is the masks where k workers are done

Green masks lie on the cheapest path from 000 to 111, and the labels give the cost each step adds. Every mask keeps one number, however many paths reach it.

Mechanics

The bit idioms

With n items numbered 0 to n − 1, the set is an integer whose bit j is 1 when item j is in it. Test membership with mask >> j & 1, add an item with mask | 1 << j, remove one with mask & ~(1 << j), and count members with mask.bit_count() (Python 3.10+; bin(mask).count("1") before that). The empty set is 0 and the full set is (1 << n) − 1. These are the tricks from the bit manipulation page, used as a data structure.

State and transition

For the assignment problem, dp[mask] is the lowest cost of giving the jobs in mask to the first popcount(mask) workers. Worker i = popcount(mask) is next, and for each free job j, dp[mask | 1 << j] may improve to dp[mask] + cost[i][j]. Start from dp[0] = 0 and read the answer at the full mask. Adding a bit always makes the integer bigger, so looping over masks in increasing numeric order handles every mask after all the masks it is built from.

Why it is correct. Take any best complete assignment and stop it after k workers; the jobs they hold form some mask. If a cheaper way to fill that same mask existed, swapping it in would leave the remaining workers exactly the same jobs to choose from, and give a cheaper complete assignment, which is impossible. So every prefix of a best answer is a best answer for its own mask, and the DP, which keeps the best answer for every mask, cannot lose it. The argument only works because the rest of the problem depends on the set and nothing else. That is the question to ask before using this pattern: is the set enough to describe the future?

When the set is not enough

On a tour, the next leg's cost depends on where you are standing, so the set of visited cities is not enough. Add the last city to the state: dp[mask][last] is the shortest path that starts at city 0, visits exactly the cities in mask and ends at last. It extends to dp[mask | 1 << c][c] through dist[last][c]. This is the Held–Karp algorithm (Bellman found it the same year), and it is still the best known exact method for the general travelling salesman problem in the worst case. A tour is a cycle, so its start can be fixed at city 0 without losing any tour, which saves a factor of n.

Worked example

Assignment. The cost of each worker doing each job:

WorkerJob 0Job 1Job 2
A927
B643
C581

Every mask, in order of size, with the step that gave it its value:

MaskBitsJobs given outWorkers donedp[mask]Best last step
0000{}00start
1001{0}19A takes job 0 from mask 0: 0 + 9
2010{1}12A takes job 1 from mask 0: 0 + 2
4100{2}17A takes job 2 from mask 0: 0 + 7
3011{0, 1}28B takes job 0 from mask 2: 2 + 6
5101{0, 2}212B takes job 2 from mask 1: 9 + 3
6110{1, 2}25B takes job 2 from mask 2: 2 + 3
7111{0, 1, 2}39C takes job 2 from mask 3: 8 + 1

Mask 3 (011) shows the pruning at work: A on job 0 then B on job 1 costs 13, while A on job 1 then B on job 0 costs 8, so only 8 is kept. Following the best last steps back from 111 gives A → job 1, B → job 0, C → job 2, for a total of 9. The greedy order costs 10.

Held–Karp on four cities. The distances:

0123
00101520
11003525
21535030
32025300

Every reachable state, starting from city 0:

BitsVisitedEnds atShortest pathCame from
0001{0}00start
0011{0, 1}1100
0101{0, 2}2150
1001{0, 3}3200
0111{0, 1, 2}1502
0111{0, 1, 2}2451
1011{0, 1, 3}1453
1011{0, 1, 3}3351
1101{0, 2, 3}2503
1101{0, 2, 3}3452
1111{0, 1, 2, 3}1703
1111{0, 1, 2, 3}2653
1111{0, 1, 2, 3}3751

Close each full path by returning to city 0:

Last cityPath through allBack to 0Tour
1701080
2651580
3752095

The shortest tour is 0 → 2 → 3 → 1 → 0, length 80. Two endings tie at 80 because the distances are symmetric: each is the other tour driven backwards.

Held–Karp on four cities: the shortest tour and the states it passes through Four cities with every distance drawn. The shortest tour, 0, 2, 3, 1, 0, is highlighted and has length 80. On the right, the states along it: mask 0001 ending at 0 costs 0; mask 0101 ending at 2 costs 15; mask 1101 ending at 3 costs 45; mask 1111 ending at 1 costs 70; then the return to city 0 makes 80. 10 15 20 35 25 30 0 1 2 3 0001 end 0, cost 0 0101 end 2, cost 15 1101 end 3, cost 45 1111 end 1, cost 70 + 10 back to 0 = 80

City 0 is the fixed start. Green edges form the shortest tour; dashed edges are the others. Each box on the right is one DP state the tour passes through, and its bits read city 3, 2, 1, 0.

Implementation

import math


def min_assignment_cost(cost):
    n = len(cost)
    dp = [math.inf] * (1 << n)
    dp[0] = 0
    for mask in range(1 << n):
        if dp[mask] == math.inf:
            continue
        worker = mask.bit_count()
        if worker == n:
            continue
        for job in range(n):
            if not mask >> job & 1:
                bigger = mask | 1 << job
                dp[bigger] = min(dp[bigger], dp[mask] + cost[worker][job])
    return dp[(1 << n) - 1]

Variations and edge cases

Top-down. The same recurrence with a cache is often quicker to write, and visits only the masks it needs:

from functools import cache

def min_assignment_cost(cost):
    n = len(cost)

    @cache
    def best(mask):
        worker = mask.bit_count()
        if worker == n:
            return 0
        return min(cost[worker][job] + best(mask | 1 << job)
                   for job in range(n) if not mask >> job & 1)

    return best(0)

Held–Karp. Two dimensions, the set and the last city:

import math


def shortest_tour(dist):
    n = len(dist)
    dp = [[math.inf] * n for _ in range(1 << n)]
    dp[1][0] = 0
    for mask in range(1, 1 << n, 2):
        for last in range(n):
            here = dp[mask][last]
            if here == math.inf:
                continue
            for city in range(1, n):
                if not mask >> city & 1:
                    bigger = mask | 1 << city
                    dp[bigger][city] = min(dp[bigger][city], here + dist[last][city])
    full = (1 << n) - 1
    return min(dp[full][last] + dist[last][0] for last in range(1, n))

Groups of equal sum. Fill groups one at a time; fill[mask] is how full the open group is after using the items in mask, and an item may only join if it fits. The answer is yes when the full mask is reachable:

def can_partition_k(nums, k):
    total = sum(nums)
    if total % k:
        return False
    side, n = total // k, len(nums)
    fill = [-1] * (1 << n)
    fill[0] = 0
    for mask in range(1 << n):
        if fill[mask] < 0:
            continue
        for i in range(n):
            if not mask >> i & 1 and fill[mask] + nums[i] <= side:
                fill[mask | 1 << i] = (fill[mask] + nums[i]) % side
    return fill[-1] == 0

Every submask of a mask. Some recurrences split a set into two parts. The step sub = (sub − 1) & mask walks through every submask from largest to 0, skipping the integers that are not submasks. For mask 11 (1011) it visits 1011, 1010, 1001, 1000, 0011, 0010, 0001, 0000: 8 of them, one per subset of its 3 set bits. Over all masks of n bits the total is 3n, since each item is outside the mask, inside it but not the submask, or in both.

sub = mask
while True:
    visit(sub)
    if sub == 0:
        break
    sub = (sub - 1) & mask
  • Unreachable states. Start every entry at infinity (or −1) and skip it when extending. Otherwise an unreachable state passes its default value along as if it were real.
  • Operator precedence. In Python, << and >> bind tighter than & and |, so mask & 1 << j means what it says. Write the brackets anyway if they help you read it.
  • Memory. dp[mask][last] with n = 20 is about 21 million entries. In Python that is too slow and too big; keep n at 16 or so for two dimensions, or store each row as an array.
  • Fixing a start. For a cycle, fixing city 0 is safe. For a path that may start anywhere, seed dp[1 << i][i] = 0 for every i instead.

Complexity and tradeoffs

The assignment DP has 2n masks and tries n jobs from each: O(n · 2n) time and O(2n) space. Held–Karp has n · 2n states and tries n next cities from each: O(n2 · 2n) time and O(n · 2n) space. The brute-force baseline tries every order, n!:

nEvery order, n!Held–Karp, n² 2nAssignment, n 2nAll submask pairs, 3n
4242566481
840,32016,3842,0486,561
12479,001,600589,82449,152531,441
1620,922,789,888,00016,777,2161,048,57643,046,721
202,432,902,008,176,640,000419,430,40020,971,5203,486,784,401
  • Where the limit sits. At about 107 to 108 simple steps per second, n · 2n is comfortable up to n = 20, n2 2n up to about 16, and 3n up to about 15. Constraints that stop just there are a hint.
  • Against backtracking. For yes/no questions like equal-sum groups, backtracking with good pruning often finishes much faster in practice; the DP's advantage is a guaranteed bound.
  • Against polynomial algorithms. Assignment is solvable in O(n3) by the Hungarian method, so for large n bitmask DP is the wrong tool. Travelling salesman is NP-hard, so nothing polynomial is known; for large n, real solvers use branch and bound or heuristics.
  • When the set is not enough. If the future depends on the order beyond a last element or two, the state grows past 2n · n and the pattern no longer pays.

Problems

No problem on the four lists has this page as its home, which is why it sits under Beyond the interview.

Also uses this pattern

These problems have a different home page, but this idea is part of their solution.

For practice beyond the lists, in rising difficulty:

Connections

Read first

Read next

No page builds on this one yet.

Bitmask DP is DP whose state happens to be a set, stored through bit manipulation. It is what permutation backtracking becomes once you notice that many orders reach the same set: the cache merges them. Its yes/no cousins, Matchsticks to Square and Partition to K Equal Sum Subsets, live on the constraint backtracking page, where pruning usually beats the DP in practice. BFS over (node, mask) pairs is graph traversal on a state graph, and visiting every node once is the Hamiltonian cousin of the Eulerian path, which uses every edge once and is far easier.

Check yourself

The assignment state is only the mask. How does the DP know which worker is next?

Workers are handled in a fixed order, A then B then C, and each takes exactly one job, so a mask with k bits set means the first k workers are done. The next worker is popcount(mask), which is why it does not need its own dimension.

Why does the greedy order cost 10 when the best is 9?

A takes job 1 for 2, which looks cheapest, but that pushes the others onto jobs 2 and 0 for 8 more. The DP keeps the best value for every set, including sets that look worse after one step, so a cheaper finish is never thrown away.

Why can Held–Karp always start at city 0, and why does it need the last city in its state?

A tour is a cycle, so any tour can be read starting from city 0 without changing its length. The last city matters because the next leg's length depends on it: two paths through the same cities but ending in different places have different futures.

Looping over every mask and then every submask of it: why 3n steps rather than 4n?

Each (mask, submask) pair gives every item one of three roles: not in the mask, in the mask but not the submask, or in both. An item cannot be in the submask without being in the mask, so the fourth combination never happens, and there are exactly 3n pairs.

References

  • Held, M. and Karp, R. M., "A dynamic programming approach to sequencing problems", Journal of the Society for Industrial and Applied Mathematics 10(1):196–210, 1962.
  • Bellman, R., "Dynamic programming treatment of the travelling salesman problem", Journal of the ACM 9(1):61–63, 1962.
  • Kuhn, H. W., "The Hungarian method for the assignment problem", Naval Research Logistics Quarterly 2:83–97, 1955.
  • Cormen, Leiserson, Rivest and Stein, Introduction to Algorithms, 3rd edition, chapter 15, "Dynamic Programming", and section 34.5.4 on the travelling-salesman problem.
  • Skiena, S., The Algorithm Design Manual, the catalog entry "Traveling Salesman Problem".
  • Sedgewick and Wayne, Algorithms, 4th edition, chapter 6, "Context", on intractability.
  • Knuth, D. E., The Art of Computer Programming, volume 4A, section 7.1.3, "Bitwise Tricks and Techniques".
  • The NeetCode roadmap and the Blind 75, Grind 169, NeetCode 150 and NeetCode 250 lists, used as the index of topics and problems.