DEV Community

Dev Cookies
Dev Cookies

Posted on

πŸ“ Blog 9: DP with Bitmasking (Advanced State Compression) 🎭

Dynamic Programming combined with bitmasking allows us to handle problems where states are subsets of elements. Instead of tracking states with arrays, we compress the information into a bitmask integer (binary representation of chosen/un-chosen states).

This technique shines in combinatorial optimization problems like Traveling Salesman, Hamiltonian paths, partitioning, and subset DP.


πŸ”‘ Key Concepts

  1. Bitmask Representation
  • Each bit in an integer represents whether an element is included or not.
  • Example: For n = 4, mask 1011 means elements {0,1,3} are chosen.
  1. State Definition
  • dp[mask]: Best answer considering subset represented by mask.
  • dp[pos][mask]: Best answer ending at pos with visited nodes in mask.
  1. Transitions
  • Add/remove an element by flipping a bit.
  • Iterate over all bits set/unset in a mask.

πŸ“Œ Classic Problems & Patterns

1. Traveling Salesman Problem (TSP)

Find the shortest path visiting all cities exactly once and return to start.

class TSP {
    private static final int INF = 1_000_000;

    public int tsp(int[][] dist) {
        int n = dist.length;
        int N = 1 << n;
        int[][] dp = new int[n][N];

        // Initialize DP
        for (int i = 0; i < n; i++) {
            Arrays.fill(dp[i], INF);
        }
        dp[0][1] = 0; // start at city 0, visited mask = 000..1

        for (int mask = 1; mask < N; mask++) {
            for (int u = 0; u < n; u++) {
                if ((mask & (1 << u)) == 0) continue; // u not visited
                for (int v = 0; v < n; v++) {
                    if ((mask & (1 << v)) != 0) continue; // v already visited
                    int nextMask = mask | (1 << v);
                    dp[v][nextMask] = Math.min(dp[v][nextMask], dp[u][mask] + dist[u][v]);
                }
            }
        }

        int ans = INF;
        for (int i = 0; i < n; i++) {
            ans = Math.min(ans, dp[i][N-1] + dist[i][0]);
        }
        return ans;
    }
}
Enter fullscreen mode Exit fullscreen mode

2. Partition into K Equal Sum Subsets

Check if an array can be partitioned into k subsets with equal sum.

class PartitionK {
    public boolean canPartitionKSubsets(int[] nums, int k) {
        int sum = Arrays.stream(nums).sum();
        if (sum % k != 0) return false;
        int target = sum / k, n = nums.length;

        boolean[] dp = new boolean[1 << n];
        int[] subsetSum = new int[1 << n];

        dp[0] = true;

        for (int mask = 0; mask < (1 << n); mask++) {
            if (!dp[mask]) continue;
            for (int i = 0; i < n; i++) {
                if ((mask & (1 << i)) != 0) continue; // already taken
                int nextMask = mask | (1 << i);
                if (dp[nextMask]) continue;
                if (nums[i] + subsetSum[mask] % target <= target) {
                    dp[nextMask] = true;
                    subsetSum[nextMask] = subsetSum[mask] + nums[i];
                }
            }
        }
        return dp[(1 << n) - 1];
    }
}
Enter fullscreen mode Exit fullscreen mode

3. Counting Hamiltonian Paths

class HamiltonianPaths {
    public int countHamiltonianPaths(int[][] graph) {
        int n = graph.length;
        int[][] dp = new int[1 << n][n];
        dp[1][0] = 1; // start at 0

        for (int mask = 1; mask < (1 << n); mask++) {
            for (int u = 0; u < n; u++) {
                if ((mask & (1 << u)) == 0) continue;
                for (int v = 0; v < n; v++) {
                    if ((mask & (1 << v)) != 0) continue;
                    if (graph[u][v] == 1) {
                        dp[mask | (1 << v)][v] += dp[mask][u];
                    }
                }
            }
        }

        int ans = 0;
        for (int i = 0; i < n; i++) {
            ans += dp[(1 << n) - 1][i];
        }
        return ans;
    }
}
Enter fullscreen mode Exit fullscreen mode

4. Minimum XOR Sum of Two Arrays

class MinXORSum {
    public int minimumXORSum(int[] nums1, int[] nums2) {
        int n = nums1.length;
        int[] dp = new int[1 << n];
        Arrays.fill(dp, Integer.MAX_VALUE / 2);
        dp[0] = 0;

        for (int mask = 0; mask < (1 << n); mask++) {
            int i = Integer.bitCount(mask); // number of assigned elements
            for (int j = 0; j < n; j++) {
                if ((mask & (1 << j)) == 0) {
                    dp[mask | (1 << j)] = Math.min(dp[mask | (1 << j)],
                                                   dp[mask] + (nums1[i] ^ nums2[j]));
                }
            }
        }
        return dp[(1 << n) - 1];
    }
}
Enter fullscreen mode Exit fullscreen mode

⚑ Key Takeaways

  • Bitmask DP compresses exponential state space into integers.
  • Useful for subset problems, assignment problems, Hamiltonian/TSP problems.
  • Works best when n <= 20 (since 2^n states explode otherwise).
  • Combine with memoization for recursive style or iterative tabulation.

Top comments (0)