Bitmask DP - CSU083 | Shoolini University

Bitmask DP

1. Prerequisites

Before understanding Bitmask Dynamic Programming (Bitmask DP), ensure you are familiar with the following foundational concepts:

2. What is Bitmask DP?

Bitmask DP is a technique that combines bitmasking and dynamic programming to efficiently solve problems involving subsets.

2.1 Understanding Bitmasks

A bitmask is an integer where each bit represents whether an element in a set is included (1) or not (0). For example, a bitmask 101 (binary) represents the subset {first, third} of a three-element set.

2.2 DP State Representation

Bitmask DP problems typically define a state dp[mask] where:

2.3 State Transition

We iterate through possible states using bitwise operations. A common transition is:


for mask in range(1 << n):  # Iterate over all subsets
    for i in range(n):  # Consider adding element i
        if mask & (1 << i) == 0:  # If i is not in the subset
            new_mask = mask | (1 << i)  # Include i in the subset
            dp[new_mask] = min(dp[new_mask], dp[mask] + cost(mask, i))

3. Why Does This Algorithm Exist?

Bitmask DP is useful when solving problems that involve states of subsets. It is widely applied in:

4. When Should You Use Bitmask DP?

Use Bitmask DP when:

Avoid Bitmask DP when n is large because the state space O(2^n) becomes infeasible.

5. How Does It Compare to Alternatives?

5.1 Strengths

5.2 Weaknesses

5.3 Comparison with Other DP Approaches

"
Approach Complexity Best Used For
Bitmask DP O(2^n * n) Subset-based problems with small n
Memoized DP Varies Overlapping subproblems without subset enumeration
Top-Down DP Varies Tree-like problems, minimizing recomputation
Bottom-Up DP O(n*m) Knapsack, LCS, LIS-type problems

6. Basic Implementation

The following is a basic implementation of Bitmask DP for solving the Traveling Salesperson Problem (TSP), where a person must visit all cities exactly once and return to the starting city with the minimum cost.


from functools import lru_cache

INF = float('inf')

def tsp(cost_matrix):
    n = len(cost_matrix)
    all_visited = (1 << n) - 1  # All cities visited when bitmask is full 1s

    @lru_cache(None)
    def dp(mask, pos):
        if mask == all_visited:  # Base case: all cities visited
            return cost_matrix[pos][0]  # Return to starting city

        min_cost = INF
        for city in range(n):
            if mask & (1 << city) == 0:  # If city is not yet visited
                new_mask = mask | (1 << city)  # Visit the city
                new_cost = cost_matrix[pos][city] + dp(new_mask, city)
                min_cost = min(min_cost, new_cost)

        return min_cost

    return dp(1, 0)  # Start from city 0 with only it visited

# Example input: 4 cities (0 to 3) with given travel costs
cost_matrix = [
    [0, 10, 15, 20],
    [10, 0, 35, 25],
    [15, 35, 0, 30],
    [20, 25, 30, 0]
]

print("Minimum TSP cost:", tsp(cost_matrix))

7. Dry Run of the Algorithm

Let's dry run the algorithm on the following 4-city cost matrix:

"
City 0 1 2 3
0 0 10 15 20
1 10 0 35 25
2 15 35 0 30
3 20 25 30 0

7.1 Initial Call

7.2 Exploring Path

7.3 Tracking DP States

"
Step Mask (Binary) Position Next City Cost
1 0001 0 1 10
2 0011 1 2 35
3 0111 2 3 30
4 1111 3 0 20

7.4 Final Cost Calculation

The optimal path is: 0 → 1 → 2 → 3 → 0 with a cost of 10 + 35 + 30 + 20 = 95.

This dry run shows how Bitmask DP efficiently explores all possible routes and finds the minimal cost solution.

8. Time & Space Complexity Analysis

8.1 Worst-Case Time Complexity

In Bitmask DP, we process all possible subsets (bitmasks) and transition between states:

Thus, the worst-case time complexity is:

$$O(2^n \cdot n)$$

8.2 Best-Case Time Complexity

The best case occurs when early pruning reduces unnecessary calculations, such as:

Even in the best case, the complexity remains exponential in nature but may reduce in practice.

8.3 Average-Case Complexity

For a well-optimized implementation with pruning and caching, practical performance is close to:

$$O(2^n \cdot n)$$

However, in some scenarios (like sparse transitions), it can be closer to:

$$O(1.5^n)$$

due to pruning.

9. Space Complexity Analysis

9.1 Space Required for DP Table

Total space required:

$$O(2^n \cdot n)$$

9.2 Space Required for Memoization

Thus, memoization also contributes O(2^n \cdot n) to space complexity.

9.3 Space Optimization Strategies

10. Trade-offs in Bitmask DP

10.1 Strengths

10.2 Weaknesses

10.3 When to Choose Alternative Approaches

"
Approach When to Use Complexity
Bitmask DP Subset-based problems with small n (≤ 20) O(2^n * n)
Branch & Bound When pruning can reduce search space O(1.5^n) (approx.)
Greedy Algorithms When approximation is acceptable O(n log n)
Heuristics (e.g., Genetic Algorithms) For large n where exact solutions are impractical Varies

10.4 Conclusion

Bitmask DP is a powerful tool when subset enumeration is required, but its exponential complexity limits its use to small input sizes. When n grows beyond 20, alternative approaches should be considered.

11. Optimizations & Variants

11.1 Common Optimizations

To improve the efficiency of Bitmask DP, consider the following optimizations:

11.1.1 Memoization

Using @lru_cache in Python or a hash map in other languages reduces redundant calculations.


from functools import lru_cache

@lru_cache(None)
def dp(mask, pos):
    # Base case and recursive calls
11.1.2 Bitwise Pruning

Instead of iterating over all elements, iterate only over unset bits:


for city in range(n):
    if not (mask & (1 << city)):  # If city is not visited
        new_mask = mask | (1 << city)
11.1.3 Iterative DP with Bitmask Compression

Instead of recursion, use a bottom-up DP approach to avoid function call overhead.

11.2 Variants of Bitmask DP

There are different versions of Bitmask DP optimized for various problems:

12. Iterative vs. Recursive Implementations

12.1 Recursive Implementation (Top-Down)

Uses function calls and memoization to store subproblem results.


from functools import lru_cache

@lru_cache(None)
def dp(mask, pos):
    if mask == (1 << n) - 1:
        return cost_matrix[pos][0]
    min_cost = float('inf')
    for city in range(n):
        if not (mask & (1 << city)):
            new_cost = cost_matrix[pos][city] + dp(mask | (1 << city), city)
            min_cost = min(min_cost, new_cost)
    return min_cost

12.2 Iterative Implementation (Bottom-Up)

Uses a loop to populate DP states instead of recursion.


n = len(cost_matrix)
dp = [[float('inf')] * n for _ in range(1 << n)]
dp[1][0] = 0  # Start from city 0

for mask in range(1 << n):
    for pos in range(n):
        if mask & (1 << pos):
            for city in range(n):
                if not (mask & (1 << city)):  # If city not visited
                    new_mask = mask | (1 << city)
                    dp[new_mask][city] = min(dp[new_mask][city], 
                                             dp[mask][pos] + cost_matrix[pos][city])

# Get result from dp[(1<<n)-1][0]

12.3 Performance Comparison

"
Implementation Time Complexity Space Complexity Best For
Recursive (Top-Down) O(2^n * n) O(2^n * n) (Memoization) Problems with natural recursion
Iterative (Bottom-Up) O(2^n * n) O(2^n * n) (DP Table) Memory-constrained environments

Conclusion: Use recursive when stack depth isn't an issue; otherwise, prefer iterative for better control.

13. Edge Cases & Failure Handling

13.1 Common Edge Cases

Bitmask DP can fail if certain conditions are not handled properly. Here are the key edge cases:

14. Test Cases to Verify Correctness

14.1 Sample Test Cases

Use various cases to ensure robustness:


def test_bitmask_dp():
    cost_matrix_1 = [[0, 10], [10, 0]]  # Small input (n=2)
    assert tsp(cost_matrix_1) == 20, "Test case 1 failed"

    cost_matrix_2 = [[0, 10, 15], [10, 0, 35], [15, 35, 0]]  # Basic case (n=3)
    assert tsp(cost_matrix_2) == 60, "Test case 2 failed"

    cost_matrix_3 = [[0]]  # Single node case
    assert tsp(cost_matrix_3) == 0, "Test case 3 failed"

    cost_matrix_4 = [
        [0, 10, float('inf')],
        [10, 0, 5],
        [float('inf'), 5, 0]
    ]  # Disconnected graph
    assert tsp(cost_matrix_4) == float('inf'), "Test case 4 failed"

    cost_matrix_5 = [
        [0, -10, 20],
        [-10, 0, -5],
        [20, -5, 0]
    ]  # Negative weights (invalid case)
    try:
        tsp(cost_matrix_5)
        assert False, "Test case 5 should fail due to negative weights"
    except ValueError:
        pass

    print("All test cases passed!")

test_bitmask_dp()

14.2 Test Coverage

Ensure that test cases cover:

15. Real-World Failure Scenarios

15.1 Practical Issues in Applications

Bitmask DP can fail in real-world scenarios due to:

15.2 Mitigation Strategies

By considering these real-world failures, we ensure Bitmask DP remains reliable in practical applications.

16. Real-World Applications & Industry Use Cases

16.1 Where is Bitmask DP Used?

Bitmask DP is widely used in various fields, especially where subset optimizations are required:

17. Open-Source Implementations

17.1 Where to Find Open-Source Implementations?

Many open-source repositories provide efficient Bitmask DP implementations:

17.2 Analyzing a Sample Implementation

For example, a C++ implementation of TSP using Bitmask DP from KACTL:


int tsp(int mask, int pos, vector<vector<int>>& cost, vector<vector<int>>& dp) {
    if (mask == (1 << cost.size()) - 1) return cost[pos][0]; // Return to start
    if (dp[mask][pos] != -1) return dp[mask][pos];

    int ans = INT_MAX;
    for (int city = 0; city < cost.size(); city++) {
        if (!(mask & (1 << city))) {
            ans = min(ans, cost[pos][city] + tsp(mask | (1 << city), city, cost, dp));
        }
    }
    return dp[mask][pos] = ans;
}

18. Practical Project: Optimized Delivery Route Planner

18.1 Project Overview

This project helps a logistics company optimize delivery routes for multiple cities using Bitmask DP.

18.2 Python Implementation

We use a distance matrix to calculate the optimal delivery route:


import itertools
from functools import lru_cache

INF = float('inf')

def delivery_route_optimizer(distance_matrix):
    n = len(distance_matrix)
    all_visited = (1 << n) - 1  # Bitmask for all cities visited

    @lru_cache(None)
    def dp(mask, pos):
        if mask == all_visited:
            return distance_matrix[pos][0]  # Return to warehouse
        min_cost = INF
        for city in range(n):
            if not (mask & (1 << city)):  # If city not visited
                new_mask = mask | (1 << city)
                new_cost = distance_matrix[pos][city] + dp(new_mask, city)
                min_cost = min(min_cost, new_cost)
        return min_cost

    return dp(1, 0)  # Start from warehouse (city 0)

# Example distance matrix (4 cities)
distance_matrix = [
    [0, 10, 20, 25],
    [10, 0, 15, 35],
    [20, 15, 0, 30],
    [25, 35, 30, 0]
]

print("Optimized Delivery Route Cost:", delivery_route_optimizer(distance_matrix))

18.3 Project Enhancements

This project showcases a real-world use case where Bitmask DP provides an optimal solution.

19. Competitive Programming & System Design Integration

19.1 Bitmask DP in Competitive Programming

Bitmask DP is widely used in programming contests like Codeforces, LeetCode, and AtCoder due to its ability to solve subset-related problems optimally.

19.1.1 Problem Patterns
19.1.2 Strategy for Competitive Programming

19.2 Bitmask DP in System Design

Bitmask DP can be integrated into large-scale systems where subset-based optimizations are needed:

19.2.1 Example Use Cases
19.2.2 Example: Role-Based Access Control (RBAC)

In a role-based system, permissions can be stored as bitmasks:


READ = 1 << 0   # 0001
WRITE = 1 << 1  # 0010
EXECUTE = 1 << 2 # 0100
DELETE = 1 << 3  # 1000

admin_permissions = READ | WRITE | EXECUTE | DELETE  # 1111
user_permissions = READ | WRITE  # 0011

def has_permission(user_mask, permission):
    return (user_mask & permission) != 0

print(has_permission(user_permissions, WRITE))  # True
print(has_permission(user_permissions, EXECUTE))  # False

Such an approach ensures fast permission checks in real-time systems.

20. Assignments

20.1 Solve At Least 10 Problems Using Bitmask DP

Solve the following problems to master the concept:

  1. TSP - Traveling Salesman Problem (SPOJ)
  2. Codeforces 489C - Given Length and Sum of Digits
  3. AtCoder DP - Matching Problem
  4. Find the largest subset where the sum is a perfect square.
  5. Partition an array into two subsets with equal sums.
  6. Determine the minimum cost of connecting all houses using a subset of roads.
  7. Find the longest path in a directed acyclic graph using Bitmask DP.
  8. Implement a subset-based knapsack problem where weights are in a bitmask format.
  9. Write a program that simulates a turn-based game strategy using Bitmask DP.
  10. Find the smallest number of people needed to complete all given tasks, where each person has a set of skills.

20.2 Use Bitmask DP in a System Design Problem

Design a microservice that assigns cloud resources efficiently using a Bitmask DP approach. Consider:

20.3 Practice Under Time Constraints

Attempt solving the problems under timed conditions to improve efficiency: