Bitmask Dynamic Programming: Optimization and Implementation

Dynamic Programming By Michael Chen, Distinguished Engineer 24 min read

1. Introduction and Core Concepts

Bitmask dynamic programming is a powerful optimization technique that combines the exhaustive state representation of bit manipulation with the efficiency of dynamic programming. At its core, it is used to solve combinatorial problems where the state of the system can be described as a subset of a small set of elements, typically with size n ≤ 20-22. Instead of representing subsets as lists or sets, we encode them as integers where each bit indicates the presence or absence of an element.

The fundamental insight behind bitmask dynamic programming is that an integer's binary representation provides a compact, hashable, and computationally efficient way to represent any subset of n items. For a set of n elements indexed from 0 to n-1, a bitmask is an n-bit integer where the i-th bit is 1 if the i-th element is included in the subset, and 0 otherwise. For example, with n=4, the mask 1011 (binary) = 11 (decimal) represents the subset {0, 1, 3}. This allows us to enumerate all 2^n possible subsets simply by iterating integers from 0 to (1 << n) - 1.

Why does this matter? Many NP-hard problems such as the Traveling Salesman Problem (TSP), assignment problems, set cover, and Hamiltonian path problems have naive solutions that require O(n!) or O(n * 2^n) brute-force enumeration with significant overhead. Bitmask DP reduces this overhead dramatically by enabling O(1) state transitions using bitwise operations and by memoizing overlapping subproblems. While the theoretical complexity remains exponential, the constant factors are so small and the implementation so cache-friendly that it makes solving n=20 instances feasible in milliseconds, whereas pure backtracking would take hours. This makes it an indispensable tool in competitive programming, operations research, and systems optimization where n is small but the search space is enormous.

When to Recognize a Bitmask DP Problem

You should consider bitmask dynamic programming when you see three key signals: first, the problem involves permutations, subsets, or assignments of a small set (n ≤ 21); second, the problem asks for an optimal value (minimum cost, maximum profit, count of ways) over all subsets or permutations; and third, the problem exhibits optimal substructure where the solution for a subset can be built from solutions for its smaller subsets. Classic keywords include "visit all cities exactly once," "assign n workers to n jobs," "find minimum cost to cover all," or "count Hamiltonian paths."

Unlike traditional 1D or 2D DP where the state is defined by indices, bitmask DP's state is typically defined as dp[mask][i] where mask represents the set of already visited/processed elements and i represents the last element added or the current position. This two-dimensional state captures both what has been done and where you are, which is essential for problems with ordering constraints. Understanding this state definition is the first step to mastering the paradigm.

2. How It Works

The mechanism of bitmask dynamic programming rests on three pillars: state representation, transition, and iteration order. The state dp[mask][last] stores the optimal value for having visited exactly the set of nodes represented by mask and ending at node last. The base case is usually dp[1 << i][i] for each starting node i, representing the cost of starting at i having visited only i. From there, we systematically expand masks by adding one new element at a time.

Transitions are performed using bitwise operations. To check if element j is already in the mask, we test if mask & (1 << j) != 0. To add element j to the mask, we compute new_mask = mask | (1 << j). To remove element j, we use mask ^ (1 << j) or mask & ~(1 << j). To iterate over all set bits in a mask, we can use the trick while mask: lsb = mask & -mask. These operations are executed in a single CPU cycle, making the inner loop extremely fast compared to set or list manipulations.

The iteration order is critical for correctness. We must ensure that when we compute dp[mask][i], all smaller submasks that could transition to mask have already been computed. The standard approach is to iterate masks in increasing order from 0 to (1 << n) - 1. Since any transition adds a bit, new_mask > mask numerically, this guarantees that dp[mask] is finalized before it is used to update dp[new_mask]. This is a bottom-up, iterative DP that avoids recursion overhead and stack limits, although a top-down memoized recursion is also common and sometimes more intuitive.

The Anatomy of a State Transition

Consider the TSP transition: dp[new_mask][j] = min(dp[new_mask][j], dp[mask][i] + cost[i][j]) where j is not in mask and new_mask = mask | (1 << j). This reads as: the best cost to visit the set new_mask and end at j is the minimum over all possible previous endpoints i of the best cost to visit mask ending at i plus the cost to travel from i to j. We are essentially building larger subsets from smaller ones, trying every possible last step. This exhaustive yet memoized exploration is what guarantees optimality while avoiding recomputation of the same subproblem exponentially many times.

Another common pattern is DP over subsets without a last dimension: dp[mask] represents the optimal value for the subset mask itself. For example, in the assignment problem where we assign jobs to workers, mask represents which jobs have been assigned, and the number of bits set in mask implicitly tells us which worker we are assigning next (k = popcount(mask)). The transition is dp[new_mask] = min(dp[new_mask], dp[mask] + cost[k][j]). This reduces the state space from O(n * 2^n) to O(2^n), a significant saving.

3. Implementation

Implementing bitmask dynamic programming in Python requires careful attention to initialization, infinity values, and bitwise operator precedence. Python's integers have arbitrary precision, so we are not limited to 32 or 64 bits, but performance degrades for n > 22 due to the 2^n state explosion. We typically use a 2D list dp of size (1 << n) x n initialized to infinity, or a dictionary for sparse states.

The most common implementation pattern is iterative bottom-up. We initialize the DP table, set base cases, then loop over all masks. Inside, we loop over all possible last nodes that are valid for that mask, and then try to extend to every next node not yet visited. Precomputing popcount or using Python's int.bit_count() (Python 3.8+) can accelerate transitions that depend on the number of set bits.

Example 1: Traveling Salesman Problem (Bottom-Up)

This implementation solves the classic TSP returning to the start (node 0). It finds the minimum cost Hamiltonian cycle.

import sys

def tsp_bitmask(cost):
    """
    Solves TSP using bitmask DP.
    cost[i][j] = cost to travel from i to j
    Returns minimum cost to visit all nodes and return to 0.
    """
    n = len(cost)
    N = 1 << n  # total number of masks = 2^n
    INF = float('inf')
    
    # dp[mask][i] = min cost to visit set 'mask' ending at city 'i'
    dp = [[INF] * n for _ in range(N)]
    
    # Base case: start at city 0, having visited only city 0
    dp[1][0] = 0  # mask 0001
    
    # Iterate over all masks
    for mask in range(N):
        for last in range(n):
            # If last is not in mask or state is unreachable, skip
            if not (mask & (1 << last)):
                continue
            if dp[mask][last] == INF:
                continue
            
            # Try to go to every next city not yet visited
            for nxt in range(n):
                if mask & (1 << nxt):
                    continue  # already visited
                new_mask = mask | (1 << nxt)
                new_cost = dp[mask][last] + cost[last][nxt]
                if new_cost < dp[new_mask][nxt]:
                    dp[new_mask][nxt] = new_cost
    
    # Answer: all cities visited, return to start
    full_mask = N - 1
    ans = INF
    for last in range(1, n):
        if dp[full_mask][last] + cost[last][0] < ans:
            ans = dp[full_mask][last] + cost[last][0]
    
    return ans

# Example usage
cost_matrix = [
    [0, 10, 15, 20],
    [10, 0, 35, 25],
    [15, 35, 0, 30],
    [20, 25, 30, 0]
]
print(tsp_bitmask(cost_matrix))  # Output: 80

Example 2: Assignment Problem (Workers to Jobs)

This variant uses a 1D DP over masks where popcount determines the current worker. It minimizes total assignment cost.

def assignment_bitmask(cost):
    """
    Assign n workers to n jobs with minimum cost.
    cost[i][j] = cost of assigning worker i to job j
    n workers, n jobs. dp[mask] where mask = set of jobs taken.
    """
    n = len(cost)
    N = 1 << n
    INF = float('inf')
    dp = [INF] * N
    dp[0] = 0
    
    for mask in range(N):
        # k = number of workers already assigned = popcount(mask)
        k = bin(mask).count('1')  # or mask.bit_count() in Python 3.8+
        if k >= n:
            continue
        for j in range(n):
            if mask & (1 << j):
                continue  # job j already taken
            new_mask = mask | (1 << j)
            new_cost = dp[mask] + cost[k][j]
            if new_cost < dp[new_mask]:
                dp[new_mask] = new_cost
    
    return dp[N - 1]

# Example: 3 workers, 3 jobs
cost = [
    [9, 2, 7],
    [6, 4, 3],
    [5, 8, 1]
]
print(assignment_bitmask(cost))  # Output: 9 (2+3+4 or similar optimal)

Example 3: Top-Down Memoization with lru_cache

For problems where not all states are reachable, a recursive memoized approach can be faster and more intuitive. Python's functools.lru_cache handles memoization automatically.

from functools import lru_cache

def tsp_top_down(cost):
    n = len(cost)
    FULL = (1 << n) - 1
    INF = float('inf')

    @lru_cache(maxsize=None)
    def dp(mask, pos):
        # mask = visited set, pos = current city
        if mask == FULL:
            return cost[pos][0]  # return to start
        best = INF
        for nxt in range(n):
            if mask & (1 << nxt) == 0:  # not visited
                new_cost = cost[pos][nxt] + dp(mask | (1 << nxt), nxt)
                if new_cost < best:
                    best = new_cost
        return best

    return dp(1, 0)  # start at city 0 with mask 0001

print(tsp_top_down(cost_matrix))

Example 4: Counting Hamiltonian Paths with Bitmask DP

Beyond optimization, bitmask DP can count the number of valid permutations that satisfy constraints, such as counting Hamiltonian paths in a directed graph.

def count_hamiltonian_paths(graph, n):
    """
    Count Hamiltonian paths in a directed graph.
    graph is adjacency matrix: graph[i][j] = 1 if edge i->j exists
    """
    N = 1 << n
    # dp[mask][i] = number of ways to visit mask ending at i
    dp = [[0] * n for _ in range(N)]
    
    # Base: single node paths
    for i in range(n):
        dp[1 << i][i] = 1
    
    for mask in range(N):
        for last in range(n):
            if not (mask & (1 << last)):
                continue
            if dp[mask][last] == 0:
                continue
            for nxt in range(n):
                if mask & (1 << nxt):
                    continue
                if graph[last][nxt] == 0:
                    continue
                new_mask = mask | (1 << nxt)
                dp[new_mask][nxt] += dp[mask][last]
    
    full = N - 1
    total = sum(dp[full][i] for i in range(n))
    return total

# Example graph: 4 nodes
graph = [
    [0, 1, 1, 0],
    [0, 0, 1, 1],
    [0, 0, 0, 1],
    [1, 0, 0, 0]
]
print(count_hamiltonian_paths(graph, 4))

4. Step-by-Step Example

Let us walk through a concrete TSP example with n=4 cities labeled 0, 1, 2, 3. The cost matrix is symmetric for simplicity, and we must start and end at city 0. The costs are: 0-1:10, 0-2:15, 0-3:20, 1-2:35, 1-3:25, 2-3:30. The optimal tour is 0 -> 1 -> 3 -> 2 -> 0 with cost 10+25+30+15 = 80. We will see how the DP discovers this.

We have N=16 masks (0 to 15). We initialize dp[0001][0]=0. All other states are INF. We iterate masks in order. For mask=0001 (1), only last=0 is valid. We try to extend to nxt=1,2,3. This sets dp[0011][1]=10, dp[0101][2]=15, dp[1001][3]=20. These represent the cost of going directly from 0 to each other city.

Next, mask=0011 (3) represents visited {0,1}. Valid last nodes are 0 and 1, but only dp[0011][1]=10 is reachable. From here we can go to 2 or 3: dp[0111][2] = 10+35=45, dp[1011][3]=10+25=35. Similarly, mask=0101 (5) with visited {0,2} yields dp[0111][1]=15+35=50 and dp[1101][3]=15+30=45. Mask=1001 (9) yields dp[1011][1]=20+25=45 and dp[1101][2]=20+30=50.

Now consider mask=0111 (7) visited {0,1,2}. There are two ways to reach it: ending at 1 with cost 50 (via 0-2-1) and ending at 2 with cost 45 (via 0-1-2). For dp[0111][1]=50, we can extend to 3: dp[1111][3]=50+25=75. For dp[0111][2]=45, we extend to 3: dp[1111][3]=min(75, 45+30=75)=75. So both paths give 75 to reach the full set ending at 3 via different orders. Continuing this process for all masks, we eventually fill dp[1111][1]=70 (path 0-2-3-1), dp[1111][2]=75 (path 0-1-3-2), dp[1111][3]=75 (path 0-1-2-3). The final answer is min over last of dp[1111][last]+cost[last][0]: dp[1111][1]+10=80, dp[1111][2]+15=90, dp[1111][3]+20=95. The minimum 80 corresponds to the optimal tour.

This walk demonstrates two crucial properties: overlapping subproblems (mask 0111 ending at 1 can be reached via multiple orders but we keep only the best) and optimal substructure (the best tour for 4 cities contains the best tour for a subset of 3 cities plus one edge). Without memoization, we would recompute the cost for subset {0,1,2} ending at 2 many times across different permutations.

5. Complexity Analysis

Bitmask dynamic programming trades exponential time for exact optimality. Understanding its complexity is essential to know when it is applicable and when it will fail. The time complexity is dominated by the number of states multiplied by the transition cost per state.

For the standard TSP formulation with state dp[mask][last], there are 2^n * n states. For each state, we iterate over up to n possible next nodes to transition, leading to O(n^2 * 2^n) time. For the assignment problem with state dp[mask] only, there are 2^n states and n transitions per state, giving O(n * 2^n) time. While both are exponential, the latter is significantly faster for the same n. Space complexity is O(n * 2^n) for the 2D table and O(2^n) for the 1D table, which often becomes the limiting factor before time does due to memory constraints.

In practice, n=20 results in about 20M states for TSP (20 * 2^20 ≈ 20M) which fits in memory (~160 MB for integers) and runs in a few seconds in Python with optimizations (using array or PyPy) or milliseconds in C++. For n=25, states exceed 800M, making it infeasible. This sharp exponential cliff is why bitmask DP is only suitable for small n. Best, average, and worst cases are often identical because we iterate over all masks regardless of graph sparsity, though sparse graphs can prune transitions and improve average-case performance.

Operation / Problem Best Case Average Case Worst Case Space Complexity
TSP (dp[mask][last]) Full Computation O(n^2 * 2^n) O(n^2 * 2^n) O(n^2 * 2^n) O(n * 2^n)
Assignment Problem (dp[mask]) O(n * 2^n) O(n * 2^n) O(n * 2^n) O(2^n)
Single State Transition O(1) O(1) O(n) O(1)
Sparse Graph TSP (pruned edges) O(n * 2^n) O(n * m * 2^n / n^2) O(n^2 * 2^n) O(n * 2^n)
Counting Subsets / SOS DP O(n * 2^n) O(n * 2^n) O(n * 2^n) O(2^n)

It is important to distinguish between theoretical Big O and practical runtime. Bitwise operations are extremely cheap, and iterating masks sequentially is cache-friendly. Using Python lists of lists is slower than using arrays from the array module or numpy, but still acceptable for n ≤ 20. For larger n, consider using iterative deepening, meet-in-the-middle, or approximation algorithms instead.

Memory optimization is often more critical than time. Storing dp as a list of arrays of type 'int' or using dictionary for sparse DP can reduce memory. Another technique is to use rolling arrays if transitions only depend on masks with one fewer bit, though for TSP this is not applicable since we need all masks. For counting problems, using Python's integers for dp values is fine, but for optimization problems, using large INF values and early pruning helps.

6. Variations and Optimizations

Several powerful variations extend the basic bitmask DP framework. One of the most important is SOS DP (Sum Over Subsets DP), which computes for every mask the sum or minimum over all its submasks in O(n * 2^n) instead of O(3^n) naive. This is invaluable for problems like "for each mask, find the maximum value among its submasks" and is used in advanced counting and game theory problems.

Another optimization is using BFS-style DP for shortest path on subset graphs. Instead of iterating masks in numeric order, we use a queue and only process reachable states. This is particularly effective for sparse transition graphs where many masks are unreachable due to constraints, such as TSP with forbidden edges or precedence constraints (e.g., must visit city A before city B).

Bitset optimization and meet-in-the-middle can push the limits further. For n=40, we can split the set into two halves of 20 each, solve each half with bitmask DP, and combine results. This reduces complexity from O(2^40) to O(2^20), albeit with more complex merging logic. Similarly, using bitset to represent transitions can accelerate operations using whole-word parallelism, especially in C++ with std::bitset, though Python's integers already provide some of this benefit.

Precomputation and Pruning Techniques

Precomputing popcount for all masks up to 2^n allows O(1) lookup of set bit counts, which is useful for assignment DP. Precomputing the list of set bits or unset bits for each mask can also speed up inner loops by avoiding repeated bit checks. For example, we can precompute lowest set bit and iterate only over valid transitions.

Pruning using branch and bound can be integrated: maintain a global best answer and skip any state where dp[mask][last] already exceeds the best known full solution (plus a heuristic lower bound like MST). This does not improve worst-case complexity but can dramatically improve average-case performance on optimization problems. Using iterative DP with early exit when dp values exceed a threshold is another practical heuristic for contest settings.

Finally, consider using Numba or PyPy for speed. Python loops over 2^n * n^2 iterations become slow for n=20 (over 400M operations). Compiling the inner loop with Numba's @jit decorator can achieve 10-100x speedup, making n=22 feasible in Python. Alternatively, using memoization with recursion and lru_cache may be faster for sparse problems where many states are never visited, as it only computes reachable states.

7. Real-World Applications

While bitmask DP is often taught in the context of abstract algorithmic puzzles, its real-world applications are surprisingly broad, especially in systems where the number of entities is small but the configuration space is large. In web server and database query optimization, the query planner must find the optimal join order for a SQL query with n tables. With n ≤ 10-12 tables, the optimizer uses bitmask DP (similar to TSP) to find the cheapest join order, where cost is estimated I/O and CPU. Systems like PostgreSQL and MySQL use this exact technique, known as DPccp or DPHyp, to enumerate subsets of tables and keep the cheapest plan for each subset.

In network routing and VLSI design, bitmask DP solves Steiner tree and Hamiltonian path problems for small sets of terminal nodes. For example, laying out wires on a chip to connect a set of pins with minimum total wire length is a Steiner tree problem solved with DP over subsets of terminals. Similarly, in delivery logistics with a small number of stops (e.g., last-mile delivery with 15-20 packages per van), the route optimization is precisely TSP solved with bitmask DP to minimize fuel and time.

In computational biology, bitmask DP is used for genome assembly and protein folding where n is the number of fragments or residues in a small window. In game AI, it solves endgame positions in chess or Go where the board has few pieces left, and in feature selection for machine learning where we need to find the optimal subset of k features from n ≤ 20 candidates to maximize model accuracy without exhaustive search overhead.

Use Case Typical n Problem Modeled As Benefit of Bitmask DP
Database Query Optimization (Join Ordering) 8 - 12 tables Optimal binary tree / TSP variant Finds optimal plan in < 100ms vs hours for brute force
Delivery Route Planning (Last-Mile) 15 - 20 stops Traveling Salesman Problem Guarantees minimal distance, saves fuel
VLSI Wire Routing 10 - 18 terminals Steiner Tree / Hamiltonian Path Minimizes chip area and signal delay
Job Scheduling on Single Machine 15 - 20 jobs Assignment with precedence constraints Minimizes total weighted completion time
Feature Selection (ML) 15 - 22 features Subset optimization Finds globally optimal feature subset
Network Reliability / Set Cover 18 - 22 nodes Set Cover / Coverage DP Ensures coverage with minimum cost

The common thread across these applications is that n is naturally small due to physical or logical constraints, but the cost of a suboptimal solution is high. Bitmask DP provides an exact, provably optimal solution where heuristics might fail, and its deterministic runtime makes it suitable for production systems with latency SLAs. As hardware improves, the feasible n slowly increases, expanding its applicability.

8. Comparison with Alternatives

Bitmask DP is not the only way to solve subset and permutation problems. Understanding its trade-offs against alternatives helps you choose the right tool for the job. Brute force enumeration generates all permutations (n!) and evaluates each, which is simple but infeasible beyond n=10. Backtracking with pruning explores the search tree depth-first and can cut branches early using bounds, which is better for sparse or constrained problems but still exponential in the worst case and does not memoize overlapping subproblems.

Greedy algorithms make locally optimal choices (e.g., always go to the nearest unvisited city) and run in O(n^2) or O(n log n), but they do not guarantee global optimality and can be arbitrarily bad for TSP. Branch and bound combines backtracking with a lower bound heuristic (like MST for TSP) to prune aggressively; it can solve larger n than bitmask DP in practice for some instances but has no worst-case guarantee and can still be exponential. BFS/DP with bitmask is essentially a breadth-first search over the subset lattice with memoization, which guarantees optimality and has predictable performance.

For larger n (n > 25), approximation algorithms (e.g., Christofides 1.5-approximation for metric TSP), genetic algorithms, or simulated annealing are preferred. They sacrifice optimality for scalability. Bitmask DP sits in the sweet spot of small n where exact optimality is required and feasible.

Feature Bitmask DP Brute Force (n!) Backtracking + Pruning Greedy Heuristic
Time Complexity O(n^2 * 2^n) O(n! * n) O(n!) worst, better avg O(n^2 log n)
Space Complexity O(n * 2^n) O(n) O(n) O(n)
Guarantees Optimality Yes (exact) Yes (exact) Yes (exact) No (approximation)
Max Feasible n (Python) 20 - 22 10 - 11 12 - 15 (depends on pruning) 10^5+
Handles Overlapping Subproblems Yes (memoization) No Partially (no memo) No
Implementation Complexity Moderate Simple Moderate-High Simple
Best For Small n, need exact optimum Tiny n, correctness check Constrained problems, sparse graphs Large n, need fast approximate

In summary, choose bitmask DP when n ≤ 22 and you need an exact answer with predictable runtime. Choose backtracking when constraints heavily prune the search space. Choose greedy or approximation when n is large and near-optimal is acceptable. Often, a hybrid approach is best: use bitmask DP for subproblems of size ≤ 20 within a larger divide-and-conquer framework.

9. Common Mistakes and Best Practices

Even experienced developers stumble on bitmask DP implementation details. The most frequent mistake is off-by-one errors in mask size: allocating dp of size 1 << n but iterating to (1 << n) inclusive, or confusing 0-indexed vs 1-indexed bits. Always remember that masks range from 0 to (1 << n) - 1 inclusive, and that mask (1 << n) is out of bounds. Another common error is incorrect operator precedence: mask & 1 << j is parsed as mask & (1 << j) due to shift having higher precedence than &, but mask & 1 == 0 is parsed as mask & (1 == 0). Always parenthesize bitwise expressions explicitly.

A second major pitfall is integer overflow or using too small an INF value. In Python this is less of an issue due to big integers, but in other languages, dp values can overflow. Use INF = 10**18 for cost problems. Also, failing to initialize dp with INF and then taking min can lead to incorrect results if you initialize with 0. Always check for INF before transitioning to avoid overflow when adding costs to INF.

Performance pitfalls include using Python's bin(mask).count('1') inside the innermost loop for every mask, which is slow. Precompute popcounts in an array popcnt[mask] for all masks once at the start. Similarly, iterating over all n next nodes for every state even when many are already visited wastes time; instead, iterate only over unset bits using while loop with bit tricks: remaining = (~mask) & ((1 << n)-1). Memory pitfalls include using a list of lists of Python ints for dp when n=22 (22 * 4M ≈ 88M integers) which will exhaust RAM. Use array('l') or dictionary for sparse cases.

Best Practices Checklist

Finally, document your state definition clearly in comments: what does dp[mask][i] mean? What is the base case and transition? This not only helps reviewers but also prevents you from mixing up mask and last dimensions during debugging. Writing a small helper function to print masks in binary (format(mask, 'b').zfill(n)) is invaluable for debugging state transitions.

10. Interview Questions

Bitmask dynamic programming is a favorite in senior and competitive programming interviews because it tests both bit manipulation fluency and DP reasoning. Interviewers expect you to recognize the pattern quickly and implement it without errors under time pressure. Below are common questions with high-level solution sketches.

Q1: Traveling Salesman Problem - Minimum Cost to Visit All Cities

Given a cost matrix cost[n][n], find the minimum cost to start at city 0, visit every city exactly once, and return to city 0. n ≤ 20. This is the canonical bitmask DP problem. Expected solution is O(n^2 * 2^n) DP as shown in the Implementation section. Follow-up: how would you reconstruct the actual path, not just the cost? Answer: store parent[mask][last] = previous city that gave optimal value, then backtrack from full mask.

Q2: Minimum Cost to Assign Tasks (Assignment Problem)

There are n workers and n jobs, cost[i][j] is cost to assign worker i to job j. Assign each worker to exactly one job and each job to exactly one worker to minimize total cost. n ≤ 20. Solution: dp[mask] where mask is set of jobs assigned; k = popcount(mask) is next worker. Transition dp[mask | (1 << j)] = min(dp[mask | (1 << j)], dp[mask] + cost[k][j]). Complexity O(n * 2^n). Variation: count number of assignments with cost ≤ K, or find if assignment exists with constraints.

Q3: Count Number of Hamiltonian Paths in a DAG

Given a directed acyclic graph with n ≤ 20 nodes, count the number of Hamiltonian paths (paths that visit every node exactly once). Solution: dp[mask][last] = number of ways to visit mask ending at last. Base dp[1 << i][i]=1. Transition only if edge last->nxt exists. Sum dp[FULL][i] over i. Follow-up: how to handle large counts modulo 1e9+7? Use modulo arithmetic. This tests counting vs optimization DP.

Q4: Smallest Sufficient Team / Set Cover with Bitmask

Given n skills (n ≤ 16) and m people (m ≤ 60), each person has a subset of skills, find the smallest team that covers all skills. This is a set cover problem solved with DP over skill masks: dp[mask] = smallest team (as bitmask or size) to cover skill mask. Transition dp[mask | person_mask] = min(dp[mask | person_mask], dp[mask] + 1). Complexity O(m * 2^n). This is LeetCode 1125 and tests DP over subsets without a last dimension.

Q5: Partition Array for Maximum Sum with Bitmask Constraints

Given n ≤ 20 items with values and a compatibility matrix, find the maximum value subset of size k where all items are pairwise compatible. Solution: iterate over all masks, check if popcount(mask)==k and mask is a clique (all pairs compatible) using precomputed compatibility masks, then compute sum. Optimization: use DP to prune. This tests combining bitmask enumeration with constraint checking and is often asked as a variation of maximum clique or maximum independent set.

For all these questions, interviewers look for: correct state definition, proper base case, bitwise transition logic, complexity analysis, and handling of edge cases (n=0, n=1, disconnected graph). Practice implementing these from scratch without referencing templates, and be ready to explain why bitmask DP is chosen over backtracking (memoization of overlapping subproblems) and what its limits are.

Frequently Asked Questions

What is Bitmask Dynamic Programming and how does it work?

Bitmask Dynamic Programming is a fundamental concept in computer science. It involves specific techniques and data organization methods designed to solve computational problems efficiently. Understanding the core mechanics helps developers choose the right approach for their applications.

What is the time complexity of Bitmask Dynamic Programming?

The time complexity of Bitmask Dynamic Programming varies depending on the specific operation and implementation. Common complexities range from O(1) constant time for basic operations to O(n log n) or O(n²) for more complex operations. The article above provides detailed complexity analysis for each operation.

When should I use Bitmask Dynamic Programming in my projects?

Use Bitmask Dynamic Programming when your application requires the specific performance characteristics it provides. Consider factors like data size, operation frequency, memory constraints, and whether the problem domain matches the strengths of Bitmask Dynamic Programming. The real-world applications section above covers common use cases.

What are the advantages and disadvantages of Bitmask Dynamic Programming?

The main advantages of Bitmask Dynamic Programming include efficient performance for its target use cases and well-understood theoretical guarantees. Disadvantages may include implementation complexity, memory overhead, or performance limitations in certain edge cases. Always benchmark against alternatives for your specific workload.

How does Bitmask Dynamic Programming compare to its alternatives?

Bitmask Dynamic Programming offers distinct trade-offs compared to alternative approaches. While it excels in certain scenarios, other algorithms or data structures may be better suited for different problem constraints. The comparison table in the article provides a detailed analysis of these trade-offs.