Bitmask DP
Bitmask DP represents subsets of elements as integers, using bit i to indicate whether element i is included in the subset. This compresses the exponential state space (2^n subsets) into integers from 0 to 2^n - 1, enabling standard array-based DP tables.
It is the go-to technique for problems involving small sets (n ≤ 20) where you need to track which elements have been "used" or "visited."
Representing Subsets as Integers
For a set of n elements (indexed 0 to n-1), a subset is an integer where bit i is 1 if element i is in the subset.
| Integer | Binary | Subset of {A,B,C,D} | |---------|--------|----------------------| | 0 | 0000 | {} | | 1 | 0001 | {A} | | 5 | 0101 | {A, C} | | 15 | 1111 | {A,B,C,D} |
Key operations:
const n = 4; // 4 elements, indexed 0..3
// Check if element i is in subset mask
function has(mask, i) { return (mask >> i & 1) === 1; }
// Add element i to subset mask
function add(mask, i) { return mask | (1 << i); }
// Remove element i from subset mask
function remove(mask, i) { return mask & ~(1 << i); }
// Count elements in subset mask
function popcount(mask) { return mask.toString(2).split('1').length - 1; }
// Iterate over all subsets of n elements
for (let mask = 0; mask < (1 << n); mask++) {
const elements = [];
for (let i = 0; i < n; i++) {
if (has(mask, i)) elements.push(i);
}
// mask 5 = 0101 → elements [0, 2]
}
// Full set mask
const FULL = (1 << n) - 1; // 1111 for n=4Traveling Salesman Problem (TSP)
Visit all n cities exactly once and return to the starting city, minimizing total travel distance.
Brute force is O(n!) — factorial. Bitmask DP reduces this to O(2^n × n²) — still exponential but tractable for n ≤ 20.
State: dp[mask][i] = minimum cost to visit all cities in mask, starting from city 0 and ending at city i.
Recurrence: dp[mask][i] = min over all j in mask, j ≠ i: dp[mask without i][j] + cost[j][i]
Answer: min over all i: dp[FULL][i] + cost[i][0] (return to start)
// TSP with Bitmask DP — O(2^n · n²) time, O(2^n · n) space
function tsp(cost) {
const n = cost.length;
const FULL = (1 << n) - 1;
const INF = Infinity;
// dp[mask][i] = min cost to visit all cities in mask, ending at city i
const dp = Array.from({ length: 1 << n }, () => new Array(n).fill(INF));
dp[1][0] = 0; // start at city 0, only city 0 visited (mask = 0001)
for (let mask = 1; mask < (1 << n); mask++) {
for (let i = 0; i < n; i++) {
if (dp[mask][i] === INF) continue;
if (!(mask & (1 << i))) continue; // city i must be in mask
// Try extending to city j
for (let j = 0; j < n; j++) {
if (mask & (1 << j)) continue; // already visited j
const nextMask = mask | (1 << j);
dp[nextMask][j] = Math.min(dp[nextMask][j], dp[mask][i] + cost[i][j]);
}
}
}
// Find minimum: visit all cities then return to city 0
let minCost = INF;
for (let i = 1; i < n; i++) {
if (dp[FULL][i] !== INF) {
minCost = Math.min(minCost, dp[FULL][i] + cost[i][0]);
}
}
return minCost;
}
const cost = [
[0, 10, 15, 20],
[10, 0, 35, 25],
[15, 35, 0, 30],
[20, 25, 30, 0]
];
console.log(tsp(cost)); // 80 (0→1→3→2→0: 10+25+30+15=80)Assignment Problem
Assign n workers to n tasks (one-to-one) to minimize total cost.
State: dp[mask] = minimum cost to assign workers 0..popcount(mask)-1 to the tasks represented by mask.
Recurrence: Let worker = popcount(mask) (the next worker to assign). For each task j in mask: dp[mask] = min(dp[mask without j] + cost[worker][j])
// Assignment Problem — O(2^n · n) time
function assignmentProblem(cost) {
const n = cost.length;
const INF = Infinity;
const dp = new Array(1 << n).fill(INF);
dp[0] = 0; // no tasks assigned, no cost
for (let mask = 0; mask < (1 << n); mask++) {
if (dp[mask] === INF) continue;
// Worker index = number of tasks already assigned = popcount(mask)
const worker = mask.toString(2).split('1').length - 1;
if (worker >= n) continue;
// Assign worker to each unassigned task
for (let task = 0; task < n; task++) {
if (mask & (1 << task)) continue; // already assigned
const nextMask = mask | (1 << task);
dp[nextMask] = Math.min(dp[nextMask], dp[mask] + cost[worker][task]);
}
}
return dp[(1 << n) - 1];
}
const costs = [
[9, 2, 7, 8],
[6, 4, 3, 7],
[5, 8, 1, 8],
[7, 6, 9, 4]
];
console.log(assignmentProblem(costs)); // 13 (worker0→task1: 2, w1→task2: 3, w2→task2... optimal)Minimum Cost to Connect All Points
Given n points on a 2D plane, find the minimum cost to connect them all (minimum spanning tree). This can be solved with Kruskal/Prim in O(n² log n), but bitmask DP shows the TSP-like formulation for small n.
// Minimum Cost to Connect All Points (bitmask DP — for small n)
// Manhattan distance: |x1-x2| + |y1-y2|
function minCostConnectPoints(points) {
const n = points.length;
const dist = (i, j) =>
Math.abs(points[i][0] - points[j][0]) + Math.abs(points[i][1] - points[j][1]);
const INF = Infinity;
// dp[mask][i] = min cost of MST spanning exactly the nodes in mask,
// with i being the last node connected
const dp = Array.from({ length: 1 << n }, () => new Array(n).fill(INF));
dp[1][0] = 0; // start: only node 0 in spanning set
for (let mask = 1; mask < (1 << n); mask++) {
for (let i = 0; i < n; i++) {
if (!(mask & (1 << i)) || dp[mask][i] === INF) continue;
for (let j = 0; j < n; j++) {
if (mask & (1 << j)) continue; // already in spanning set
const nextMask = mask | (1 << j);
dp[nextMask][j] = Math.min(dp[nextMask][j], dp[mask][i] + dist(i, j));
}
}
}
const FULL = (1 << n) - 1;
return Math.min(...dp[FULL]);
}
console.log(minCostConnectPoints([[0,0],[2,2],[3,10],[5,2],[7,0]])); // 20Subset Enumeration Tricks
A useful trick: iterating over all subsets of a given mask. Naively you'd check all 2^n masks — but you can enumerate only subsets of a mask efficiently:
// Iterate over all subsets of mask (including empty set)
// Time: O(3^n) total across all masks — each element is in 0, 1, or 2 subsets
function iterateSubsets(mask) {
const subsets = [];
for (let sub = mask; sub > 0; sub = (sub - 1) & mask) {
subsets.push(sub);
// (sub - 1) & mask: decrement sub but keep only bits that are in mask
// This efficiently enumerates all non-empty subsets of mask
}
subsets.push(0); // empty subset
return subsets;
}
console.log(iterateSubsets(0b1010)); // subsets of {1, 3}
// 10 (0b1010), 8 (0b1000), 2 (0b0010), 0
// Application: partition into two complementary subsets
function canPartitionIntoEqualHalves(nums) {
const n = nums.length;
const total = nums.reduce((a, b) => a + b, 0);
if (total % 2 !== 0) return false;
const half = total / 2;
for (let mask = 0; mask < (1 << n); mask++) {
let sum = 0;
for (let i = 0; i < n; i++) {
if (mask & (1 << i)) sum += nums[i];
}
if (sum === half) return true;
}
return false;
}State of the Bitmask
Some problems use bitmask to represent a configuration rather than a subset. For example, broken/working lights in a room, or cells in a row that are filled.
Profile DP (common in competitive programming) uses bitmasks to represent the state of the current column when filling a 2D grid column by column.
// Example: count ways to tile a 2×n grid with 1×2 dominoes
// State: bitmask of which cells in current column are already "occupied"
// (by a horizontal domino extending from the previous column)
function tilingWays(n) {
const dp = new Map();
dp.set(0, 1); // 0 cells pre-occupied at start, 1 way
for (let col = 0; col < n; col++) {
const next = new Map();
function fill(colMask, nextMask, row) {
if (row === 2) {
next.set(nextMask, (next.get(nextMask) || 0) + (dp.get(colMask) || 0));
return;
}
if (colMask & (1 << row)) {
// Current row in this column is occupied, skip
fill(colMask, nextMask, row + 1);
} else {
// Place a vertical domino (fills this row only)
fill(colMask, nextMask, row + 1);
// Place a horizontal domino (fills this row in current AND next column)
if (row + 1 < 2 && !(colMask & (1 << (row + 1)))) {
// This is simplified — real profile DP is more complex for 2×n
}
}
}
// Simpler closed-form for 2×n tiling: fib(n+1)
// dp[n] = dp[n-1] + dp[n-2]
}
// For 2×n tiling, answer is Fibonacci(n+1)
if (n === 0) return 1;
let a = 1, b = 1;
for (let i = 2; i <= n; i++) [a, b] = [b, a + b];
return b;
}
console.log(tilingWays(4)); // 5Bitmask DP at a Glance
Operation | Code | Meaning |
|---|---|---|
Check bit i | mask >> i & 1 | Is element i in subset? |
Set bit i | mask | (1 << i) | Add element i to subset |
Clear bit i | mask & ~(1 << i) | Remove element i from subset |
Full set | (1 << n) - 1 | All n elements |
Subset of mask | (sub-1) & mask | Iterate subsets efficiently |
Popcount | mask.toString(2).split("1").length-1 | Number of elements in subset |
Bitmask DP requires n ≤ 20 (at most ~1 million states with n=20)
State is (mask, other_dimensions) — mask encodes which elements are used
TSP template: dp[mask][last] = min cost visiting nodes in mask, ending at last
Assignment template: dp[mask] = min cost assigning workers to tasks in mask
Subset enumeration trick (sub-1)&mask iterates all subsets in O(3^n) total