Data Structures & Algorithms (DSA) Module 14 – Dynamic programming
Bitmask DP: subsets as states
Use the bits of an integer as the set of items already chosen, solve the travelling salesperson problem with Held-Karp and see when 2ⁿ states fit.
What you will learn
- Represent subsets as integers in DP states
- Solve the travelling salesperson problem with Held-Karp
- Estimate when 2^n states are feasible
Before you start
What you’ll learn · Before you start
What you will learn
- Represent subsets as integers in DP states
- Solve the travelling salesperson problem with Held-Karp
- Estimate when 2^n states are feasible
Before you start
On this page
Bitmask DP stores a set inside a DP state as one integer, whose bit number i says whether item i belongs to the set. The 2ⁿ integers from 0 to 2ⁿ − 1 are exactly the subsets of n items, so they can index a table. Two shapes of state cover most problems: dp[mask] when only the set matters, as when tasks are handed out one at a time, and dp[mask][last] when the last item also matters, as on a route. The second shape is the Held-Karp algorithm for the travelling salesperson problem, published by Held and Karp in 1962 and found by Bellman the same year: the shortest round trip through n places in O(2ⁿ · n²) time, where trying every order costs (n − 1)! routes. Because 2²⁰ is about a million, 16 to 20 items is the usual limit.
Sets as integers
Five operations are all a bitmask DP needs. The examples use mask = 0b1011, the set of items 0, 1 and 3.
| Operation | Python | Example |
|---|---|---|
| Is item i in the set? | (mask >> i) & 1 |
item 2: 0, item 3: 1 |
| Add item i | mask | (1 << i) |
add item 2: 0b1111 = 15 |
| Remove item i | mask & ~(1 << i) |
remove item 1: 0b1001 = 9 |
| How many items? | mask.bit_count() |
3 |
| Every item of n | (1 << n) - 1 |
n = 4: 0b1111 = 15 |
Removing an item clears a bit, which gives a smaller number, so every subset of a mask is numerically smaller than the mask. A loop that counts masks upwards therefore finishes every set before any set that contains it, and that is the only order a subset DP needs.
Held-Karp: a set and the last stop
A round trip starts at spot 0 and must visit every spot once before it returns. Once some spots have been visited, the rest of the trip depends on two things only: which spots are left and where the traveller stands. The order of the earlier visits no longer matters. So the state is dp[mask][last]: the shortest route that starts at spot 0, visits exactly the spots in mask and stops at last.
The sets of four spots that contain the start, as masks
Text description of the diagram
The diagram shows the eight sets of four spots that contain spot 0, in four rows by size, each labelled with its mask in binary and in decimal. Bit i of a mask is 1 when spot i is in the set.
- Size 1: {0}, mask 0001 = 1.
- Size 2: {0, 1} = 3, {0, 2} = 5 and {0, 3} = 9.
- Size 3: {0, 1, 2} = 7, {0, 1, 3} = 11 and {0, 2, 3} = 13.
- Size 4: {0, 1, 2, 3} = 15.
An arrow joins each set to every set with one more spot: {0} to all three sets of size 2, each set of size 2 to the two sets of size 3 that contain it, and each set of size 3 to the full set. Adding a spot sets a bit that was 0, so every arrow leads to a larger number, and visiting the masks in increasing order finishes every set before any set that contains it.
The route to (mask, last) arrived from some other spot prev of mask, so dp[mask][last] is the smallest dp[mask without last][prev] + d[prev][last]. The program writes it forwards instead: from every reachable state it tries each spot not yet in the mask. The answer is the smallest dp[all][last] + d[last][0], the trip home included. There are 2ⁿ · n states with up to n choices each, hence O(2ⁿ · n²) time and O(2ⁿ · n) memory.
The shortest tour of 12 spots
Twelve tourist spots, with travel times in minutes made up for practice, the same in both directions:
"""Held-Karp on 12 tourist spots with synthetic travel times, checked against trying every order on the first 8."""
import random
from itertools import permutations
SPOTS = ["Hotel", "Fort", "Museum", "Lake", "Market", "Temple", "Garden", "Gallery", "Bridge", "Tower", "Bazaar", "Station"]
rng = random.Random(12)
n = len(SPOTS)
minutes = [[0] * n for _ in range(n)]
for a in range(n):
for b in range(a + 1, n):
minutes[a][b] = minutes[b][a] = rng.randint(5, 40) # synthetic travel times, the same both ways
INF = float("inf")
def held_karp(d):
"""The shortest round trip from spot 0 through every spot, and the tour itself."""
n = len(d)
full = (1 << n) - 1
# dp[mask][last]: the shortest route that starts at 0, visits exactly the spots of mask and stops at last
dp = [[INF] * n for _ in range(1 << n)]
came = [[-1] * n for _ in range(1 << n)]
dp[1][0] = 0 # mask 1 holds spot 0 alone
for mask in range(1, 1 << n): # increasing: every smaller set is finished before a set that contains it
if not mask & 1:
continue
for last in range(n):
here = dp[mask][last]
if here == INF:
continue
for nxt in range(n):
if mask >> nxt & 1:
continue
bigger = mask | 1 << nxt
if here + d[last][nxt] < dp[bigger][nxt]:
dp[bigger][nxt] = here + d[last][nxt]
came[bigger][nxt] = last
best, last = min((dp[full][v] + d[v][0], v) for v in range(1, n)) if n > 1 else (0, 0)
tour, mask = [], full
while last != -1: # walk the recorded choices back to the start
tour.append(last)
mask, last = mask & ~(1 << last), came[mask][last]
return best, tour[::-1] + [0]
def every_order(d):
n = len(d)
return min(sum(d[a][b] for a, b in zip((0, *p), (*p, 0))) for p in permutations(range(1, n)))
best, tour = held_karp(minutes)
print("shortest round trip of 12 spots:", best, "minutes")
print(" -> ".join(SPOTS[v] for v in tour))
print(f"states: {2**n:,} sets x {n} last spots = {2**n * n:,}")
small = [row[:8] for row in minutes[:8]]
print("first 8 spots: Held-Karp", held_karp(small)[0], "minutes, every one of the 5,040 orders", every_order(small)) Output
shortest round trip of 12 spots: 117 minutes Hotel -> Gallery -> Station -> Fort -> Market -> Bridge -> Garden -> Tower -> Museum -> Bazaar -> Lake -> Temple -> Hotel states: 4,096 sets x 12 last spots = 49,152 first 8 spots: Held-Karp 110 minutes, every one of the 5,040 orders 110
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 tour.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
The best round trip takes 117 minutes, and following the recorded choices backwards from the last stop rebuilds it. The table has 49,152 states. On the first 8 spots the DP and a brute force over all 5,040 orders agree on 110 minutes; on all 12, the brute force would have 39,916,800 orders to try.
How large can n get?
The table compares the (n − 1)! orders a brute force tries with the 2ⁿ · n states and 2ⁿ · n² transition steps of Held-Karp.
| n | Orders | States | Steps |
|---|---|---|---|
| 8 | 5,040 | 2,048 | 16,384 |
| 12 | 4.0 × 10⁷ | 49,152 | 5.9 × 10⁵ |
| 16 | 1.3 × 10¹² | 1.0 × 10⁶ | 1.7 × 10⁷ |
| 20 | 1.2 × 10¹⁷ | 2.1 × 10⁷ | 4.2 × 10⁸ |
| 25 | 6.2 × 10²³ | 8.4 × 10⁸ | 2.1 × 10¹⁰ |
Every extra spot doubles the states and adds a little more per state, so the DP grows like 2ⁿ while the brute force grows like a factorial. Memory gives out about when time does: 20 spots already need 2 × 10⁷ entries. The same encoding serves other problems. To assign n tasks to n people, dp[mask] alone is enough, because the size of the mask says which task comes next. To split items into groups, such as tasks into working days, the DP walks the submasks of every mask, 3ⁿ steps in all.
Exercise: the shortest delivery round trip
The exercise asks for the shortest round trip through up to 16 delivery points, with travel times that may differ in the two directions.
Exercise · Hard · Python, JavaScript
The shortest round trip through every delivery point
A rider starts at point 0, must visit every delivery point exactly once and come back to point 0. dist[a][b] is the travel time from point a to point b, and it need not equal dist[b][a] (one-way streets). Write shortest_round_trip(dist) (JavaScript: shortestRoundTrip(dist)), which returns the length of the shortest such round trip, for 1 ≤ n ≤ 16 points. A single point needs no travel at all, so its answer is 0.
For [[0, 7, 12, 9], [7, 0, 5, 14], [12, 5, 0, 6], [9, 14, 6, 0]] the answer is 27: 0 → 1 → 2 → 3 → 0 takes 7 + 5 + 6 + 9. The tests also use two points with different times each way, a one-way loop where only one direction is cheap, 9 points checked against every order, the 12 tourist spots of the lesson and 13 generated points.
Python · Starter code · route.py
def shortest_round_trip(dist):
"""The shortest route that starts at point 0, visits every point once and returns to point 0."""
raise NotImplementedError("write shortest_round_trip") The sample tests · test_route.py
from itertools import permutations
from route import shortest_round_trip
SPOTS = [
[0, 35, 22, 38, 27, 14, 29, 5, 28, 35, 22, 34],
[35, 0, 19, 40, 5, 14, 33, 28, 15, 26, 18, 8],
[22, 19, 0, 17, 9, 37, 26, 30, 10, 6, 8, 37],
[38, 40, 17, 0, 19, 10, 32, 33, 12, 32, 13, 39],
[27, 5, 9, 19, 0, 25, 40, 15, 8, 40, 15, 37],
[14, 14, 37, 10, 25, 0, 10, 30, 31, 35, 35, 29],
[29, 33, 26, 32, 40, 10, 0, 39, 6, 10, 17, 21],
[5, 28, 30, 33, 15, 30, 39, 0, 27, 28, 29, 24],
[28, 15, 10, 12, 8, 31, 6, 27, 0, 12, 21, 20],
[35, 26, 6, 32, 40, 35, 10, 28, 12, 0, 26, 28],
[22, 18, 8, 13, 15, 35, 17, 29, 21, 26, 0, 28],
[34, 8, 37, 39, 37, 29, 21, 24, 20, 28, 28, 0],
]
def generated(n, seed, one_way=False):
"""n points with times from 5 to 40, the same in every language (a Lehmer generator)."""
d, x = [[0] * n for _ in range(n)], seed
for a in range(n):
for b in range(n):
if a == b or (not one_way and b < a):
continue
x = (x * 48271) % 2147483647
d[a][b] = 5 + x % 36
if not one_way:
d[b][a] = d[a][b]
return d
def every_order(d):
return min(sum(d[a][b] for a, b in zip((0, *p), (*p, 0))) for p in permutations(range(1, len(d))))
def test_one_and_two_points():
"""a single point needs no travel; two points go there and back"""
assert shortest_round_trip([[0]]) == 0
assert shortest_round_trip([[0, 4], [6, 0]]) == 10
def test_four_points():
"""0 -> 1 -> 2 -> 3 -> 0 takes 7 + 5 + 6 + 9"""
assert shortest_round_trip([[0, 7, 12, 9], [7, 0, 5, 14], [12, 5, 0, 6], [9, 14, 6, 0]]) == 27
def test_one_way_loop():
"""only the direction 0 -> 1 -> 2 -> 3 -> 0 is cheap"""
assert shortest_round_trip([[0, 1, 9, 9], [9, 0, 1, 9], [9, 9, 0, 1], [1, 9, 9, 0]]) == 4
def test_nine_points_against_every_order():
"""9 points with one-way times, checked against all 40,320 orders"""
d = generated(9, 77, one_way=True)
assert shortest_round_trip(d) == every_order(d) == 102
def test_twelve_tourist_spots():
"""the 12 spots of the lesson"""
assert shortest_round_trip(SPOTS) == 117
def test_thirteen_generated_points():
"""13 generated points"""
assert shortest_round_trip(generated(13, 2028)) == 127 JavaScript · Starter code · route.mjs
/** The shortest route that starts at point 0, visits every point once and returns to point 0. */
export function shortestRoundTrip(dist) {
throw new Error('write shortestRoundTrip');
} The sample tests · route.test.mjs
import { test, assert } from 'mysmartcopilot:test';
import { shortestRoundTrip } from './route.mjs';
const SPOTS = [
[0, 35, 22, 38, 27, 14, 29, 5, 28, 35, 22, 34],
[35, 0, 19, 40, 5, 14, 33, 28, 15, 26, 18, 8],
[22, 19, 0, 17, 9, 37, 26, 30, 10, 6, 8, 37],
[38, 40, 17, 0, 19, 10, 32, 33, 12, 32, 13, 39],
[27, 5, 9, 19, 0, 25, 40, 15, 8, 40, 15, 37],
[14, 14, 37, 10, 25, 0, 10, 30, 31, 35, 35, 29],
[29, 33, 26, 32, 40, 10, 0, 39, 6, 10, 17, 21],
[5, 28, 30, 33, 15, 30, 39, 0, 27, 28, 29, 24],
[28, 15, 10, 12, 8, 31, 6, 27, 0, 12, 21, 20],
[35, 26, 6, 32, 40, 35, 10, 28, 12, 0, 26, 28],
[22, 18, 8, 13, 15, 35, 17, 29, 21, 26, 0, 28],
[34, 8, 37, 39, 37, 29, 21, 24, 20, 28, 28, 0],
];
/** n points with times from 5 to 40, the same in every language (a Lehmer generator). */
function generated(n, seed, oneWay = false) {
const d = Array.from({ length: n }, () => new Array(n).fill(0));
let x = seed;
for (let a = 0; a < n; a++) {
for (let b = 0; b < n; b++) {
if (a === b || (!oneWay && b < a)) continue;
x = (x * 48271) % 2147483647;
d[a][b] = 5 + (x % 36);
if (!oneWay) d[b][a] = d[a][b];
}
}
return d;
}
/** Every order of the points after 0, tried. */
function everyOrder(d) {
const rest = d.map((_, i) => i).slice(1);
let best = Infinity;
const walk = (at, left, total) => {
if (!left.length) {
best = Math.min(best, total + d[at][0]);
return;
}
left.forEach((v, i) => walk(v, [...left.slice(0, i), ...left.slice(i + 1)], total + d[at][v]));
};
walk(0, rest, 0);
return best;
}
test('a single point needs no travel; two points go there and back', () => {
assert.equal(shortestRoundTrip([[0]]), 0);
assert.equal(shortestRoundTrip([[0, 4], [6, 0]]), 10);
});
test('0 -> 1 -> 2 -> 3 -> 0 takes 7 + 5 + 6 + 9', () => {
assert.equal(shortestRoundTrip([[0, 7, 12, 9], [7, 0, 5, 14], [12, 5, 0, 6], [9, 14, 6, 0]]), 27);
});
test('only the direction 0 -> 1 -> 2 -> 3 -> 0 is cheap', () => {
assert.equal(shortestRoundTrip([[0, 1, 9, 9], [9, 0, 1, 9], [9, 9, 0, 1], [1, 9, 9, 0]]), 4);
});
test('9 points with one-way times, checked against all 40,320 orders', () => {
const d = generated(9, 77, true);
assert.equal(everyOrder(d), 102);
assert.equal(shortestRoundTrip(d), 102);
});
test('the 12 spots of the lesson', () => {
assert.equal(shortestRoundTrip(SPOTS), 117);
});
test('13 generated points', () => {
assert.equal(shortestRoundTrip(generated(13, 2028)), 127);
}); A hint
Keep dp[mask][last]: the shortest route that starts at point 0, visits exactly the points whose bits are set in mask and stops at last. Start from dp[1][0] = 0 and go through the masks in increasing order; from each reachable state, try every point next not yet in the mask. At the end, add the trip from last back to point 0 and take the smallest total.
Results of the sample tests
| Test | Result | Details |
|---|
What your code printed
The sample tests run on this device, in your browser (Pyodide, QuickJS): nothing is sent to mysmartcopilot.com. The first run of each language downloads it: Python (about 13.5 MB) or JavaScript (about 0.6 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.
Self-check
Check yourself
5 questions about this lesson. Every answer and why it is right is on the page, behind “Show the answer”. Your score stays in this browser.
Held-Karp proved, its tours checked, and its time and memory measured Pro: opens with a Pro, Premium or Ultimate pass
A table of a million states earns trust only with a proof and a check. This part proves why the set and the last stop are enough, checks the DP and its rebuilt tours against a brute force on 200 random instances, and measures its time and memory from 12 to 18 spots.
In this part:
- Why the set and the last stop are enough: the proof
- Checked against every order on 200 instances
- Time and memory, measured
1 runnable example (Python) · about 3 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Assignment with one mask, and grouping with submasks in 3ⁿ steps Pro: opens with a Pro, Premium or Ultimate pass
Many bitmask problems need less state than a tour and some need more work per state. This part assigns ten tasks to ten volunteers with dp[mask] and compares it with all 3,628,800 assignments and a greedy rule, then packs twelve tasks into the fewest working days by walking submasks, and proves the 3ⁿ bound.
In this part:
- Ten tasks, ten volunteers, one mask
- When the order of the tasks may be fixed
- Packing tasks into days with submasks
- Why walking every submask costs 3ⁿ: the proof
1 runnable example (Python) · about 2 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Held-Karp in JavaScript, Java, C++ and Go, with flat tables and shifts that wrap Pro: opens with a Pro, Premium or Ultimate pass
In compiled languages the table becomes one flat array, which is smaller and faster than lists of lists, and the width of the integer that holds a mask starts to matter. This part runs Held-Karp in four more languages on the 12 spots and on 16 generated spots, and shows where shifts go wrong.
In this part:
- JavaScript, as it runs in the browser
- Java, C++ and Go, built and run
- Shifts and widths: the mask must fit its integer
- The lowest set bit, and removing an item
3 runnable examples (C++, Go, Java, JavaScript, Python) · about 3 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Interview questions
-
Fresher to mid · Warm-up
In a DP over subsets stored as bitmasks, why is it safe to process the masks in increasing numeric order, even though mask 8 has fewer items than mask 7?
A subset DP only needs every subset of a mask to be finished before the mask itself, not every smaller set. A subset of mask has some of mask's bits and none of the others, so removing an item turns a 1 into a 0 and always gives a smaller number; equivalently, adding an item always gives a larger one. Counting from 0 to 2ⁿ − 1 therefore meets every subset of a mask before the mask. Mask 8 (one item) comes after mask 7 (three items), but 8 is not a subset of 7, so the order of the two does not matter. Processing by number of items would also work, and is useful when only two layers of sizes are kept in memory.
-
Mid to senior
Your Held-Karp solution for 20 cities is correct but runs out of memory in Python, where it keeps lists of lists. Estimate its memory, and say what you change to fit it in 256 MB without losing exactness.
-
Fresher to mid
Ten delivery agents and ten orders: cost[i][j] is the time agent j needs for order i. A greedy rule gives each order in turn the fastest free agent. Show why that can fail, and give an exact method that finishes well under a second.
-
Senior to staff
A rider must visit 14 pickup and drop points from a depot, and each drop may only happen after its pickup. How does Held-Karp change, and what does the rule do to the number of states?
Key takeaways
- A mask stores a set in an integer: bit i says whether item i is in it, and every subset of a mask is a smaller number.
- Held-Karp keeps dp[mask][last], the shortest route through mask that stops at last: O(2ⁿ · n²) time against (n − 1)! orders.
- dp[mask] is enough when the size of the mask tells you what comes next, as in assignment.
- About 16 to 20 items is the limit, because states double with every item and memory runs out with them.
References
- A dynamic programming approach to sequencing problems (M. Held and R. M. Karp, J. SIAM 10(1), 1962) (Society for Industrial and Applied Mathematics)
- Dynamic programming treatment of the travelling salesman problem (R. Bellman, J. ACM 9(1), 1962) (Association for Computing Machinery)
- The Hungarian method for the assignment problem (H. W. Kuhn, Naval Research Logistics Quarterly 2, 1955) (Wiley)
- Dictionary of Algorithms and Data Structures: traveling salesman (National Institute of Standards and Technology (NIST))
- Built-in types: int.bit_count() (Python Software Foundation)
- Binary bitwise operations (The Python Language Reference) (Python Software Foundation)
- tracemalloc: trace memory allocations (Python Software Foundation)
- Java Language Specification, Java SE 25: 15.19 Shift operators (Oracle)
- Class Integer: bitCount(int) (Java SE 25) (Oracle)
- Working Draft, Programming Languages: C++ [bit.count] (std::popcount) (ISO/IEC JTC1/SC22/WG21)
- Package math/bits: OnesCount (The Go Authors)
- ECMAScript 2026 Language Specification: Number::leftShift (Ecma International)
- ACM/IEEE-CS/AAAI Computer Science Curricula 2023: Algorithmic Foundations (AL) (ACM, IEEE Computer Society and AAAI)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress