Pebellslearn by building
0%
RoadmapsMatrixCoursesSandboxSign in
Planning DP  ›  Lesson

Bitmask DP & the Exact Tour

Bitmask DP 36 minWhen the state is a set, store the set as the bits of an integer
You're building a piece ofMeridian — the day planner
This piece — best_tour(): Sequences a small round of stops exactly, with a table indexed by the set of stops visited.
Scenario The afternoon courier covers a handful of stops on one-way streets and has to come back to the depot. There are few enough stops to find the exact best round rather than a good guess, as long as the planner doesn't try every order.
Your task
Build best_tour(dist). dist[a][b] is the km from stop a to stop b (one-way streets make it lopsided), and stop 0 is the depot. Find the shortest round that leaves the depot, visits every other stop exactly once and returns. Return {"km": total, "order": stops}, where stops starts and ends with 0. There are 2 to 10 stops. For ties, fill dp[mask][j] (the shortest route from 0 through exactly the stops in mask, ending at j), then choose the last stop as the smallest j with the lowest dp[full][j] + dist[j][0], and walk back: each earlier stop is the smallest k in the remaining set with dp[before][k] + dist[k][j] == dp[mask][j]. Example: best_tour([[0, 2, 9], [4, 0, 3], [5, 8, 0]]) → {"km": 10, "order": [0, 1, 2, 0]}.

Bitmask DP & the Exact Tour

In short: when the rest of a route depends only on which stops are already visited and where the van is now — never on the order it took to get there — the set of done stops is the whole state, and a set of up to about 20 items fits in the bits of one integer. dp[S][j] is the shortest route that visits exactly the stops in set S and ends at j; filling it in plain numeric order always has every smaller set ready first, because turning on a bit only ever makes a number bigger.

The bitmask 0b1011 drawn as four bit boxes 1,0,1,1 over positions 3,2,1,0, with stops 0, 1 and 3 circled as "in the set" and stop 2 crossed out as "not in the set"

The afternoon courier leaves the depot, drops parcels at a handful of stops and comes back. The streets are one-way, so going from stop 2 to stop 3 isn't the same distance as going from 3 to 2. With 4 stops there are 6 possible orders, and with 10 stops there are 362,880. Trying every order is fine today and too slow by next month. Guessing (always drive to the nearest unvisited stop) is fast, and it's often wrong by a lot.

Between those two there's an exact method that works well up to about 15 stops. It rests on one question: what does the rest of the route actually depend on? It doesn't depend on the order you've driven so far. It depends only on which stops are done and where the van is now.

A set is the state

Suppose two partial routes have both visited stops {0, 1, 3} and are both standing at stop 3. One took 14 km to get there and the other took 6. Whatever the best way to finish is, it's the same for both, because the same stops are left and the van is in the same place. So the 14 km route can never be part of the best tour, and you only need to remember the 6.

That gives the state:

dp[S][j] = the shortest route that starts at the depot, visits exactly the stops in set S, and ends at stop j.

To fill it, ask which stop came just before j. It was some k in S other than j, and the route to k visited exactly S without j:

dp[S][j] = min over k of dp[S − {j}][k] + dist[k][j]

Then the full tour is min over j of dp[all][j] + dist[j][0].

Storing a set in an integer

A set of up to 20 stops fits in the bits of one integer, where bit i is on when stop i is in the set. 0b1011 is the set {0, 1, 3}, read right to left. Every set operation is then a single machine instruction:

Set operation Bit operation
the set of all n stops (1 << n) - 1
is k in S? S & (1 << k)
add k S | (1 << k)
remove k S ^ (1 << k) (when k is in S)
how many stops bin(S).count("1")

Operator precedence catches people out, so check yourself:

What does this print?
mask = 0b1011
print(bin(mask | 1 << 2), mask >> 1 & 1, mask & (1 << 2))

A mask is also just a number, and the most common bug is forgetting that:

broken — fix it

0b1010 is the set {1, 3}, but this prints [2, 3]. Test stop k's own bit.

Watching the table fill

Four stops on one-way streets: dist = [[0, 2, 9, 10], [3, 0, 6, 4], [8, 7, 0, 3], [6, 4, 5, 0]], where row a gives the distances from a. Rows are the sets that contain the depot, smallest first. Columns are the stop the route ends at.

watch it move · grid

Each cell reads only rows with one bit fewer, which are all filled earlier. So filling by the number of stops visited always has the inputs ready.

The first row is the set {0}: the van is at the depot having driven 0 km. A "-" is impossible, because a route must end at a stop it has visited and only the first row can end at the depot. 8 by 4 table. At row 0, column 0 — value 0. 20 of 32 cells filled.

1 / 10

A route with n stops has 2ⁿ sets and n endpoints, so there are 2ⁿ · n cells, and each one looks at up to n predecessors. That's O(2ⁿ · n²) time in all. Those 2ⁿ · n cells are also what the table holds in memory, so it costs O(2ⁿ · n) space — honestly exponential in both, just far smaller than listing every order outright. It's still exponential, but it grows far more slowly than (n − 1)!:

What does this print?
import math
n = 12
print(2 ** n * n, math.factorial(n - 1))

In what order?

The table needs every smaller set filled before any set that contains it. Sorting by the number of stops works, and so, conveniently, does plain numeric order, because adding a bit always makes the number bigger. Step through the size-sorted order for three stops:

step through it
1n = 3
2order = []
3for size in range(n + 1):
4 for mask in range(1 << n):
5 if bin(mask).count("1") == size:
6 order.append(mask)
7print(order)

Now fill the whole table yourself, with one row for every mask, including the ones without the depot, which stay at infinity. Change a distance and watch which cells move:

your code, drawn · grid

Your set table in numeric order. Every cell compares cells in a row with one bit fewer, and that row is always higher up the table.

Recovering the order

The table stores distances, so recover the tour by walking back, the way you did for knapsack. Start from the best last stop j. Remove j from the set, and find the k whose cell plus dist[k][j] gives exactly dp[S][j]. Repeat until you reach the depot. On a symmetric map, going round clockwise and anticlockwise always tie, so the task fixes a rule: the smallest-numbered stop wins every tie.

The same trick on other sets

Anything whose future depends only on which things are done can use a mask. Here's a common interview shape, in its own words:

Interview-style problem: one job per driver. n drivers and n jobs. cost[d][j] is what it costs driver d to do job j. Give every driver exactly one job and every job exactly one driver, at the lowest total cost.

Constraints: 1 ≤ n ≤ 12.

Examples: [[4, 2, 8], [4, 3, 7], [3, 1, 6]] → 12. [[5]] → 5.

The state is the set of jobs already taken. If the jobs are handed out to drivers in order, the number of jobs taken is the next driver's number, so the driver doesn't need to be part of the state:

python

Your exercises use the same move four more times: counting job orders that respect "this before that", covering every licence type with the fewest drivers, pairing drivers two to a van, and a sweep that can start and end anywhere.

❓ Cross-question: "Why not just use a Python set or frozenset as the key?" You can, with a dict as the table, and it's a fine first version. An integer is much smaller and faster, though: it indexes a list directly, hashes instantly, and you get "all subsets in a valid order" for free from range(1 << n).

Where the wall is

Python in the browser manages roughly 10 million simple table steps a second, and the table costs n² · 2ⁿ steps:

stops n steps at 10 million a second
10 102,400 0.01 s
12 589,824 0.06 s
15 7,372,800 0.7 s
16 16,777,216 1.7 s
20 419,430,400 42 s

Fifteen stops is the most that finishes within a second, and each stop after that more than doubles the work. That is why the exercises here stay at 12 or fewer. Beyond about 20 stops even a fast language runs out of time and memory, and that's where exact answers give way to approximations.


Idioms & real-world patterns

Fill order for free

for mask in range(1 << n) visits every set after all of its subsets. You rarely need to sort by size, except when a picture reads better that way, as the one above does.

⚡ Advanced: enumerating submasks

Some set DPs split a set S into two parts, for example "which stops go on van A, and the rest on van B". The loop sub = S; while sub: ...; sub = (sub - 1) & S visits every non-empty submask of S. Over all S together that's 3ⁿ steps rather than 4ⁿ, which is what makes "split the set" DPs practical for about 12 to 14 items.


Quick practice

What does this print?
mask = 0b0110
print(bin(mask ^ (1 << 1)), bin(mask | (1 << 3)), bin(mask & (mask - 1)))
broken — fix it

The shortest round trip is 10 km, but this prints inf because the base case — "starting at the depot alone costs 0" — is missing. Add it back.

🎯 Your turn

Write best_tour(dist). dist[a][b] is the km from a to b, and stop 0 is the depot. Find the shortest round from the depot through every stop exactly once and back. Return {"km": total, "order": [0, …, 0]}.

  • best_tour([[0, 2, 9], [4, 0, 3], [5, 8, 0]]) → {"km": 10, "order": [0, 1, 2, 0]}

Ties: the last stop is the smallest j with the lowest dp[full][j] + dist[j][0]. Walking back, each earlier stop is the smallest k that reproduces the cell exactly.

Then press ▶ Run, tap the Live App try chips, and hit ✓ Check. Green means the exact tour is built. ✅

Practice — level up

0/6 solved

Solve each in its own editor — Run to try, Check to grade. Stuck? Reveal a Hint, or the full Solution + walkthrough.

Warm-upSets in the right order

A set of stops is stored as an integer whose bit i is on when stop i is in the set. Write masks_by_size(n) returning every mask for n stops (0 to 2ⁿ − 1), ordered by how many stops it contains, and by value among masks of the same size. masks_by_size(2) → [0, 1, 2, 3]; masks_by_size(3) → [0, 1, 2, 4, 3, 5, 6, 7].

bitmask-dpbinary
DrillOrders that respect the rules

A driver has n jobs numbered 0 to n − 1, and before holds [a, b] pairs meaning job a must be done before job b. Write count_orders(n, before) returning how many orders of all n jobs respect every pair. count_orders(3, [[0, 2]]) → 3.

Constraints: n ≤ 12.

bitmask-dpdp
BuildCover every licence

A job needs every licence type 0 to m − 1 on board (van, crane, hazardous goods, …). licences[i] lists the types driver i holds. Write fewest_drivers(licences, m) returning the fewest drivers who together hold all m types, or -1 if even everyone together can't. fewest_drivers([[0, 1], [1, 2], [2]], 3) → 2.

Constraints: m ≤ 10, up to 15 drivers.

bitmask-dpdp
BossTwo to a van

Night deliveries go out two drivers to a van. cost[i][j] (symmetric) is how badly drivers i and j work together. There is an even number of drivers. Write pair_vans(cost) returning the lowest total cost of splitting all drivers into pairs. pair_vans([[0, 3, 8, 6], [3, 0, 5, 9], [8, 5, 0, 2], [6, 9, 2, 0]]) → 5.

Constraints: an even number of drivers, at most 12.

bitmask-dpdp
CapstoneStart anywhere, end anywhere

A parcel sweep has to visit every stop once, but the van is already out, so it may start at any stop and end at any stop (no return trip). dist[a][b] is the km from a to b, not necessarily symmetric. Write shortest_sweep(dist) returning the fewest km. shortest_sweep([[0, 1, 9], [1, 0, 1], [9, 1, 0]]) → 2.

Constraints: 1 to 10 stops.

held-karpbitmask-dpdp
ExplainWhy a set is enough

The tour table is indexed by (set of stops visited, current stop) and never remembers the order the stops were visited in. Explain why that is enough to find the best tour, how many states that makes for n stops compared with trying every order, and why it still stops being practical somewhere around 20 stops.

best_tour.py
Call the function you wrote — just like your app's frontend would. Edit the inputs and hit Call.
best_tour(dist) → dictThe shortest round from depot 0 through every stop and back: its km and the order, starting and ending at 0.
try:
returned
Press Run to execute your code.
Press Check to run the tests for this lesson.