Using Bitmask DP Pattern
1. Compressing State with Bitmasks
Bitmask dynamic programming (DP) is a technique used to optimize state representation in DP problems where the state space can be efficiently encoded using bits. This is particularly useful when dealing with subsets or combinations of elements, where each element can either be included or excluded.
| State Representation | Description | Example |
|---|---|---|
| Bitmask | A binary number where each bit represents the inclusion or exclusion of an element. | 1010 represents elements at indices 1 and 3 included, others excluded. |
| State Size | For n elements, the number of possible states is 2^n. |
For 4 elements, there are 2^4 = 16 possible states. |
Example: Subset Generation Using Bitmasks
const elements = ['A', 'B', 'C', 'D'];
const n = elements.length;
for (let mask = 0; mask < (1 << n); mask++) {
const subset = [];
for (let i = 0; i < n; i++) {
if (mask & (1 << i)) {
subset.push(elements[i]);
}
}
console.log('Subset: ', subset);
}
mask & (1 << i) checks if the i-th bit is set in mask.
Example: Counting Set Bits
function countSetBits(mask) {
let count = 0;
while (mask > 0) {
count += mask & 1;
mask >>= 1;
}
return count;
}
// Example usage
const mask = 0b1101; // Binary: 1101
console.log(countSetBits(mask)); // Output: 3
2. Implementing Traveling Salesman Problem
The Traveling Salesman Problem (TSP) can be solved efficiently using bitmask dynamic programming when the number of cities is small (typically up to 20). The idea is to use a bitmask to represent the set of visited cities and compute the minimum cost to reach each city from the starting point.
| State | Description | Transition |
|---|---|---|
| dp[mask][i] | Minimum cost to reach city i with the set of visited cities represented by mask |
For each city j not in mask, update dp[mask | (1 << j)][j] = min(dp[mask | (1 << j)][j], dp[mask][i] + cost[i][j]) |
Example: TSP with 4 Cities
const cities = 4;
const cost = [
[0, 10, 15, 20],
[10, 0, 35, 25],
[15, 35, 0, 30],
[20, 25, 30, 0]
];
let dp = Array(1 << cities).fill().map(() => Array(cities).fill(Infinity));
dp[1][0] = 0;
for (let mask = 1; mask < (1 << cities); mask++) {
for (let u = 0; u < cities; u++) {
if ((mask & (1 << u)) === 0) continue;
for (let v = 0; v < cities; v++) {
if ((mask & (1 << v)) !== 0) continue;
const newMask = mask | (1 << v);
dp[newMask][v] = Math.min(dp[newMask][v], dp[mask][u] + cost[u][v]);
}
}
}
let minCost = Infinity;
for (let i = 1; i < cities; i++) {
minCost = Math.min(minCost, dp[(1 << cities) - 1][i] + cost[i][0]);
}
console.log(minCost); // Output: 80
3. Solving Assignment Problems
Bitmask Dynamic Programming (DP) is particularly effective for solving assignment problems where we need to assign tasks to agents or resources optimally. These problems often involve minimizing cost or maximizing benefit while ensuring each task is assigned to exactly one agent.
| Problem | Description | Approach | Time Complexity |
|---|---|---|---|
| Task Assignment | Assigning n tasks to n agents such that each agent gets one task and total cost is minimized. | Use bitmask DP where each bit represents whether a task has been assigned. State: dp[mask] = minimum cost to assign tasks represented by bits in mask. |
O(n * 2^n) |
| Job Scheduling | Assigning jobs to workers with different capabilities and costs. | State: dp[mask] = minimum cost to schedule jobs represented by mask. |
O(n * 2^n) |
Example: Minimize Cost to Assign Tasks
const n = 3;
const cost = [
[10, 20, 30],
[40, 50, 60],
[70, 80, 90]
];
let dp = new Array(1 << n).fill(Infinity);
dp[0] = 0;
for (let mask = 0; mask < (1 << n); mask++) {
const count = mask.toString(2).split('1').length - 1;
for (let i = 0; i < n; i++) {
if ((mask & (1 << i)) === 0) {
const newMask = mask | (1 << i);
dp[newMask] = Math.min(dp[newMask], dp[mask] + cost[count][i]);
}
}
}
console.log(dp[(1 << n) - 1]); // Output: 150
mask represents which tasks have already been assigned. For each state, we try assigning the next unassigned task to an agent and update the DP table accordingly.
4. Finding Hamiltonian Path
A Hamiltonian path visits each node in a graph exactly once. When using bitmask dynamic programming, we can efficiently explore all possible paths in a graph with up to 20 nodes.
| State | Description | Transition | Value |
|---|---|---|---|
| dp[mask][i] | Minimum cost to reach node i with the set of visited nodes represented by mask |
For each node j in mask, update dp[mask ∪ {i}][i] |
Cost of the path |
Example: Finding Minimum Cost Hamiltonian Path
const n = 4;
const graph = [
[0, 10, 15, 20],
[5, 0, 35, 25],
[10, 30, 0, 40],
[20, 25, 30, 0]
];
const dp = Array(1 << n).fill().map(() => Array(n).fill(Infinity));
dp[1][0] = 0;
for (let mask = 1; mask < (1 << n); mask++) {
for (let u = 0; u < n; u++) {
if ((mask & (1 << u)) === 0) continue;
for (let v = 0; v < n; v++) {
if ((mask & (1 << v)) !== 0) continue;
const newMask = mask | (1 << v);
dp[newMask][v] = Math.min(dp[newMask][v], dp[mask][u] + graph[u][v]);
}
}
}
let minCost = Infinity;
for (let i = 0; i < n; i++) {
minCost = Math.min(minCost, dp[(1 << n) - 1][i]);
}
console.log(minCost); // Output: 80
5. Solving Subset Sum with Bitmask
The bitmask dynamic programming (DP) approach is a powerful technique for solving subset sum problems efficiently when the size of the set is small (typically up to 20 elements). It leverages bitmasks to represent subsets and uses bitwise operations for state transitions.
| Property | Description | Notes |
|---|---|---|
| Bitmask Representation | Each subset is represented as a bitmask where the i-th bit indicates whether element i is included in the subset. | For a set of size n, there are 2^n possible subsets. |
| State Definition | dp[mask] = true if subset represented by mask sums to a target value. | Can be extended to store the actual sum or subset elements. |
| Transition | For each element, update dp[mask] based on including or excluding the element. | Transition: dp[mask | (1 << i)] = dp[mask] || dp[mask | (1 << i)] |
Example: Subset Sum with Bitmask
function subsetSumWithBitmask(nums, target) {
const n = nums.length;
const dp = new Array(1 << n).fill(false);
dp[0] = true;
for (let mask = 0; mask < (1 << n); mask++) {
if (!dp[mask]) continue;
for (let i = 0; i < n; i++) {
if ((mask & (1 << i)) === 0) {
const newMask = mask | (1 << i);
dp[newMask] = true;
}
}
}
return dp[(1 << n) - 1];
}
// Example usage:
const nums = [3, 1, 2, 5];
const target = 6;
console.log(subsetSumWithBitmask(nums, target)); // true (subset [1, 2, 3] sums to 6)
6. Implementing Set Cover Problem
The Set Cover problem involves finding the minimum number of subsets from a collection that cover all elements in a universe. When using bitmask dynamic programming, each subset is represented as a bitmask, and the state represents which elements are covered.
| State | Description | Transition |
|---|---|---|
| dp[mask] | Minimum number of subsets needed to cover elements represented by the bitmask mask |
For each subset S, update dp[mask | S] if dp[mask] + 1 < dp[mask | S] |
Example: Set Cover with Bitmask DP
const universe = [1, 2, 3, 4];
const subsets = [
[1, 2],
[2, 3],
[3, 4],
[1, 4]
];
const n = universe.length;
const m = subsets.length;
// Convert universe to bitmask
const universeMask = (1 << n) - 1;
// Initialize dp array
const dp = Array(1 << n).fill(Infinity);
dp[0] = 0;
// Convert subsets to bitmask representation
const subsetMasks = subsets.map(subset => {
let mask = 0;
for (const elem of subset) {
const idx = universe.indexOf(elem);
mask |= (1 << idx);
}
return mask;
});
// Fill dp table
for (let mask = 0; mask < (1 << n); mask++) {
if (dp[mask] === Infinity) continue;
for (let i = 0; i < m; i++) {
const newMask = mask | subsetMasks[i];
dp[newMask] = Math.min(dp[newMask], dp[mask] + 1);
}
}
// Result is minimum subsets to cover entire universe
console.log(dp[universeMask]); // Output: 2 (subsets [1,2] and [3,4])
7. Solving Partition Problem
The bitmask dynamic programming (DP) pattern is particularly useful for solving partition problems where we need to divide a set into two subsets with specific properties, such as equal sum or equal size.
| Problem | Description | Solution Approach | Time Complexity |
|---|---|---|---|
| Equal Sum Partition | Partition an array into two subsets with equal sum. | Use bitmask DP to iterate through all possible subsets and check if any subset sums to half of the total sum. | O(n * 2^n) |
| Subset Sum | Determine if a subset exists with a given sum. | Use bitmask DP to explore all subsets and track achievable sums. | O(n * 2^n) |
Example: Equal Sum Partition
function canPartition(nums) {
const total = nums.reduce((a, b) => a + b, 0);
if (total % 2 !== 0) return false;
const target = total / 2;
const dp = new Array(1 << nums.length).fill(false);
dp[0] = true;
for (let mask = 0; mask < (1 << nums.length); mask++) {
if (!dp[mask]) continue;
for (let i = 0; i < nums.length; i++) {
if ((mask & (1 << i)) === 0) {
const newMask = mask | (1 << i);
const sum = getSum(mask, nums) + nums[i];
if (sum === target) return true;
dp[newMask] = true;
}
}
}
return false;
}
function getSum(mask, nums) {
let sum = 0;
for (let i = 0; i < nums.length; i++) {
if ((mask & (1 << i)) !== 0) sum += nums[i];
}
return sum;
}
8. Finding Shortest Hamiltonian Path
The Hamiltonian Path problem involves finding the shortest path that visits each node exactly once in a weighted graph. When using bitmask dynamic programming (DP), we can efficiently solve this problem for small graphs (up to ~20 nodes) by leveraging the state representation of subsets.
| State | Description | Transition | Time Complexity |
|---|---|---|---|
| dp[mask][u] | Minimum cost to reach node u having visited all nodes in the subset represented by mask |
For each node v not in mask, update dp[mask ∪ {v}][v] with min(dp[mask][u] + weight[u][v]) |
O(n × 2^n) |
Example: Shortest Hamiltonian Path using Bitmask DP
const n = 4;
const dist = [
[0, 10, 15, 20],
[10, 0, 35, 25],
[15, 35, 0, 30],
[20, 25, 30, 0]
];
let dp = Array(1 << n).fill().map(() => Array(n).fill(Infinity));
dp[1][0] = 0;
for (let mask = 1; mask < (1 << n); mask++) {
for (let u = 0; u < n; u++) {
if ((mask & (1 << u)) === 0) continue;
for (let v = 0; v < n; v++) {
if ((mask & (1 << v)) !== 0) continue;
let newMask = mask | (1 << v);
dp[newMask][v] = Math.min(dp[newMask][v], dp[mask][u] + dist[u][v]);
}
}
}
let minCost = Infinity;
for (let i = 0; i < n; i++) {
minCost = Math.min(minCost, dp[(1 << n) - 1][i]);
}
console.log(minCost); // Output: 80
9. Implementing State Transitions
Bitmask Dynamic Programming (DP) is particularly useful for problems involving state transitions where each state can be represented as a bitmask. The key idea is to use bits to represent the presence or absence of elements in a set, and transitions between states are computed using bitmask operations.
| State Representation | Description | Example |
|---|---|---|
| Bitmask | Each bit represents whether an element is included in the current state. | 1010 represents elements 1 and 3 in a set of size 4. |
| State Transition | Transition from one bitmask to another using bitwise operations. | state & (state - 1) removes the rightmost set bit. |
Example: Subset Generation Using Bitmask
function generateSubsets(nums) {
const n = nums.length;
const result = [];
for (let mask = 0; mask < (1 << n); mask++) {
const subset = [];
for (let i = 0; i < n; i++) {
if (mask & (1 << i)) {
subset.push(nums[i]);
}
}
result.push(subset);
}
return result;
}
// Example usage:
console.log(generateSubsets([1, 2, 3]));
Example: Counting Subsets with Specific Properties
function countSubsetsWithSum(nums, target) {
const n = nums.length;
let count = 0;
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 === target) {
count++;
}
}
return count;
}
// Example usage:
console.log(countSubsetsWithSum([1, 2, 3, 4], 5)); // Output: 2
10. Solving Job Assignment
The bitmask dynamic programming (DP) pattern is particularly useful for solving job assignment problems where each job must be assigned to exactly one worker, and each worker can handle at most one job. This is a classic application of bitmask DP to optimize resource allocation.
| State Representation | Description | Usage |
|---|---|---|
| mask | A bitmask representing which jobs have been assigned to workers. | Used to track job assignments efficiently in O(1) time. |
Example: Assigning Jobs to Workers
const jobs = [10, 20, 30]; // Cost of assigning job to worker
const workers = [15, 25, 35]; // Cost of assigning worker to job
// Using bitmask DP to find minimum cost assignment
function minCostJobAssignment(jobs, workers) {
const n = jobs.length;
const dp = new Array(1 << n).fill(Infinity);
dp[0] = 0;
for (let mask = 0; mask < (1 << n); mask++) {
const assignedJobs = bitCount(mask);
for (let i = 0; i < n; i++) {
if ((mask & (1 << i)) === 0) {
const newMask = mask | (1 << i);
dp[newMask] = Math.min(dp[newMask], dp[mask] + jobs[assignedJobs] * workers[i]);
}
}
}
return dp[(1 << n) - 1];
}
function bitCount(mask) {
let count = 0;
while (mask > 0) {
count += mask & 1;
mask >>= 1;
}
return count;
}
console.log(minCostJobAssignment(jobs, workers)); // Output: 105
11. Handling Constraint Satisfaction
| Constraint Type | Description | Implementation | Example |
|---|---|---|---|
| Exclusive | Only one of a set of options can be selected | Use bitmask to ensure only one bit is set | const mask = (1 << i) | (1 << j); // Only one of i or j can be set |
| Inclusive | At least one of a set of options must be selected | Ensure at least one bit is set in the mask | if (mask & ((1 << n) - 1)) { // At least one of n options is set } |
| Mutual Exclusion | Two or more options cannot be selected together | Check for overlap in bitmasks | if ((mask1 & mask2) === 0) { // No overlap between masks } |
| Dependency | One option requires another to be selected | Ensure required bits are set when dependent bits are set | if ((mask & requiredMask) === requiredMask) { // Required options are set } |
Example: Constraint Satisfaction in Bitmask DP
function countValidCombinations(n, constraints) {
let count = 0;
const total = 1 << n;
for (let mask = 0; mask < total; mask++) {
let valid = true;
for (const [type, condition] of constraints) {
if (type === 'exclusive') {
const bits = mask & condition;
if (bits & (bits - 1)) { // More than one bit set
valid = false;
break;
}
} else if (type === 'mutual') {
const bits = mask & condition;
if (bits !== 0 && bits !== condition) { // Not all or none
valid = false;
break;
}
} else if (type === 'dependency') {
const required = mask & condition.required;
const dependent = mask & condition.dependent;
if (dependent !== 0 && required === 0) {
valid = false;
break;
}
}
}
if (valid) count++;
}
return count;
}
countValidCombinations function evaluates all constraints for each possible bitmask and counts valid combinations.12. Optimizing Memory Usage
| Technique | Description | Memory Impact | Applicable Use Cases |
|---|---|---|---|
| State Space Reduction | Use only necessary bits to represent states, avoiding full bitmask expansion. | Reduces memory from O(2^n) to O(k), where k is the number of relevant states. | When only a subset of bits are active or relevant in the problem domain. |
| Bitset Optimization | Use compact data structures like std::bitset or custom bit manipulation for faster access. |
Improves cache locality and reduces memory overhead. | For problems with fixed-size state representations (e.g., up to 64 bits). |
| Space-Time Trade-offs | Use memoization with hash tables or arrays to trade memory for faster lookups. | Can increase memory usage but reduce time complexity in some cases. | When frequent state lookups are required and performance is critical. |
| Iterative Deepening | Process states in increasing order of bitmask size to avoid storing all states at once. | Reduces peak memory usage by processing smaller states first. | For problems where state size varies significantly and full enumeration is not needed. |
Example: Reducing State Space
#include <vector>
#include <algorithm>
// Instead of storing all 2^N states, only store relevant ones
int bitmask_dp_optimized(const std::vector<int>& weights, int max_weight) {
const int n = weights.size();
std::vector<int> dp(max_weight + 1, 0);
for (int i = 0; i < n; ++i) {
// Process in reverse to avoid reusing updated values
for (int w = max_weight; w >= weights[i]; --w) {
dp[w] = std::max(dp[w], dp[w - weights[i]] + weights[i]);
}
}
return dp[max_weight];
}
Example: Bitset Optimization
#include <bitset>
// Use bitset for compact representation of states
const int MAX_N = 64;
std::bitset<MAX_N> visited;
// Efficiently check and set state bits
void mark_visited(int index) {
visited.set(index);
}
bool is_visited(int index) {
return visited.test(index);
}
Memory Optimization Strategies
- State Space Reduction: Limit the number of unique states by filtering out irrelevant bits or combinations.
- Bitset Usage: Leverage optimized bit manipulation libraries to reduce memory overhead and improve access speed.
- Iterative Deepening: Process states in stages to manage peak memory consumption.
- Memoization Trade-offs: Balance between memory usage and lookup performance based on problem constraints.