βš™οΈAlgorithmIntermediate

Bitmask DP - Subset Enumeration

Key Points

  • β€’
    Bitmask DP subset enumeration lets you iterate all submasks of a given mask using the idiom for (s = mask; s > 0; s = (s - 1) & mask).
  • β€’
    The total number of (mask, submask) pairs over n bits is 3^n, because \( 2^{k} = 3^{n}\).
  • β€’
    To include the empty submask as well, use a do-while loop or break after processing s == 0.
  • β€’
    Enumerating all supersets of a fixed mask is symmetric and uses for (s = mask; s < (1u<<n); s = (s + 1) | mask).
  • β€’
    This technique is core to many 1700–2100 CF tasks: set cover DP, grouping/partitioning, subset convolution-like sums, and game DP on states.
  • β€’
    Always store masks in unsigned types and be careful with 1u<<n to avoid overflow; prefer __builti for ).
  • β€’
    Use this when transitions depend on every subsubset or supersubset; avoid it when O(3^n) is too large and consider SOS DP or zeta transforms.
  • β€’
    Space remains O(2^n) for arrays over masks, while time is often O(3^n) unless further structure reduces it.

Prerequisites

  • β†’Bitwise operations (&, |, ^, ~, shifts) β€” Submask and superset enumeration rely on bitwise AND/OR and safe shifting.
  • β†’Combinatorics and binomial coefficients β€” Understanding why total iterations sum to 3^n uses counting and the binomial theorem.
  • β†’Dynamic programming basics β€” Bitmask DP states and transitions build on standard DP principles.
  • β†’Two’s complement and unsigned arithmetic β€” Expressions like x & -x and safe usage of (1u<<n) depend on representation details.
  • β†’Time complexity analysis β€” Deciding feasibility (O(3^n) vs O(n 2^n)) requires asymptotic reasoning.

Detailed Explanation

Tap terms for definitions

01Overview

Bitmask DP with subset enumeration is a technique for iterating through all submasks (subsets) of a given bitmask efficiently using bitwise operations. A bitmask is an integer whose binary representation encodes a subset of an n-element universe: bit i is 1 if the i-th element is present. The classic loop for submasks is for (s = mask; s > 0; s = (s - 1) & mask), which walks through all submasks s of mask without checking membership explicitly. Dually, iterating over all supersets of a mask uses for (s = mask; s < (1u<<n); s = (s + 1) | mask). These patterns are building blocks in dynamic programming where the state is a subset of items: games on sets, covering and partitioning problems, and sum-over-subsets computations. A key fact is that if you enumerate every mask and all of its submasks, the total number of iterations is 3^n, derived from the binomial theorem. This often defines the time complexity of algorithms built on top of this enumeration. Mastering these idioms lets you write tight and fast code that avoids extra conditionals and leverages CPU-friendly bitwise operations.

02Intuition & Analogies

Think of a mask as a shopping list where each bit corresponds to an item on a shelf. A submask is any smaller shopping list you can make by crossing out some items. If your full list has k items, then there are 2^k possible smaller lists (including the empty one): keep or drop each chosen item. The submask loop is like a machine that repeatedly subtracts one from your current list (in binary), then cleans up anything that wasn’t on the original list. Subtracting one in binary flips the rightmost 1 to 0 and turns all trailing 0s into 1s; intersecting with the original mask (& mask) keeps only legitimate items. This guarantees you visit each valid smaller list exactly once with no duplicates.

Now imagine visiting every possible original list (every mask) in the store. For each, you go through all its smaller lists. Items not on the original list are irrelevant, but for each item you included, you had a choice for submasks to keep it or not. Across the entire store of possibilities, each item can be in one of three states with respect to a pair (submask, mask): absent from mask, present in mask but not in submask, or present in both. That’s three choices per item, hence 3^n total pairs. Superset enumeration is the reverse: starting from a minimum required list (mask), you add arbitrary missing items; again, each item has three roles across all (mask, superset) pairs, resulting in the same 3^n total shape. This β€œthree roles per bit” view is the core intuition behind the 3^n bound.

03Formal Definition

Let U be a universe with = n labeled elements, identified with bit positions 0..nβˆ’1. A subset S U corresponds to a mask m [0, 2^n) whose i-th bit iff i S. For a fixed mask m, a submask is any s such that s m, equivalently s & . The canonical enumeration order is defined by = ( - 1) & m with , which enumerates all submasks exactly once until . Dually, a superset of m is any s such that m s, equivalently s m with until . For a function f defined on subsets (masks), many DP recurrences take the form g(m) = h(s, m), where op is min, max, or sum. The cost of computing g for all m by naive submask enumeration is \( 2^{(m)}\). Using the identity \( 2^k = 3^n\), the total number of inner iterations is \((3^n)\). Similarly, enumerating all supersets for each m leads to \( 2^{n-k} = 3^n\).

04When to Use

Use submask enumeration when your transition or scoring function for a mask depends on all its subsubsets, such as: (1) Set cover or grouping DP: dp[m] = min over s βŠ† remaining of dp[m βˆͺ s] + cost(s). (2) Sum-over-subsets accumulation: F[m] = (\sum_{s \subseteq m} A[s]) when SOS DP is not required or n is small. (3) Game DP on subsets: compute Grundy or winning positions where moves remove arbitrary legal submasks. (4) Partitioning problems: picking the next team/group as any allowed submask of the unused elements. (5) Enumerating supersets for constraint satisfaction: given a required feature set m, find best/first superset s that satisfies or minimizes some criterion.

Avoid it when n is too large for (\Theta(3^n)) time (typically n > 22–24 on modern judges). Consider alternatives: SOS DP (bitset zeta transforms) to compute all (\sum_{s \subseteq m}) in (O(n 2^n)) instead of (O(3^n)); meet-in-the-middle to split n into two halves; or specialized pruning/branch-and-bound when cost structure allows it.

⚠️Common Mistakes

  • Forgetting the empty submask: The loop for (s = mask; s > 0; s = (s - 1) & mask) skips s = 0. If you need it, either handle it after the loop or use a do-while loop and break after processing s == 0.
  • Signed overflow or wrong shift: 1 << n is undefined for n = 31 on 32-bit signed ints. Use (1u << n) or (1ULL << n) and store masks in unsigned types (unsigned, uint32_t, uint64_t) depending on n.
  • Infinite loops from underflow: If s is unsigned and you write s = s - 1 without the & mask, it wraps to all bits set. Always do (s - 1) & mask and ensure you stop after hitting zero.
  • Enumerating submasks of the wrong set: The idiom s = (s - 1) & mask only walks submasks of mask. If you mean β€œremaining = full ^ used”, compute that exact mask first.
  • Double counting in DP: In partitioning/grouping DP, iterate only submasks that contain a chosen pivot element (e.g., the least set bit) to avoid counting the same group multiple times in different orders.
  • Using O(3^n) when O(n 2^n) suffices: For computing all F[m] = (\sum_{s \subseteq m} A[s]), prefer SOS DP for n β‰₯ ~20.
  • Popcount and bit positions: Mixing 0-based/1-based positions causes off-by-one errors when mapping problem elements to bits; stick to 0-based consistently.
  • Not caching costs: If cost(s) is reused across many transitions, precompute it for all s to avoid hidden multiplicative factors over an already expensive 3^n.

Key Formulas

Number of submasks

Explanation: If a mask m has k set bits, each of those k bits can be either kept (1) or dropped (0) in a submask, independently. Thus there are 2^k submasks in total, including the empty submask.

Total submask iterations

Explanation: There are C(n,k) masks with k ones, each with 2^k submasks. Summing over k and applying the binomial theorem with (1+2)^n yields 3^n, which is the total number of (mask, submask) pairs.

Total superset iterations

Explanation: For a fixed mask with k ones, there are 2^{n-k} supersets obtained by choosing any subset of the nβˆ’k zero bits to add. Summing over all k again gives 3^n.

Next submask recurrence

Explanation: Starting from s=m, repeatedly subtracting one flips the rightmost 1 to 0 and sets trailing bits to 1; AND with m filters to bits present in m. This enumerates each submask exactly once until s reaches 0.

Next superset recurrence

Explanation: Starting from the minimal superset m, incrementing and OR with m forces mandatory bits to 1 and walks all supersets in increasing order until 2^n.

Subset summation

Explanation: Many DP problems require accumulating contributions from all submasks s of m. Directly computing this for all m by submask enumeration is O(3^n), but SOS DP can reduce it to O(n 2^n).

Enumeration complexity

Explanation: The runtime for iterating all masks and their submasks grows like 3^n, which is often the dominant cost. Constants are small due to bitwise operations, but the exponential base matters.

Binomial theorem

Explanation: Setting x=1 and y=2 explains the identity leading to 3^n. It’s the combinatorial backbone behind counting (mask, submask) pairs.

Mex function

Explanation: In game DP over masks, the Grundy number of a state is the mex of reachable Grundy numbers. Submask enumeration often generates the reachable set S.

Counting submasks

Explanation: Summing 1 over all submasks yields the number of submasks, reinforcing the 2^k fact for a mask with k ones.

Complexity Analysis

Let n be the number of bits (elements). For a fixed mask m with ), the number of submasks is 2^k. If you perform an O(1) operation per submask, the time for that mask is O(2^k). If you repeat this for all masks, the total time is sum over all masks of O(2^{popcount(m)}). Grouping masks by k, there are C(n,k) masks with k set bits, giving total time O(βˆ‘_{k=0}^{n} C(n,k) 2^k) = O(3^n). This is tight because each pair (mask, submask) is visited exactly once by the standard idiom, and each visit does at least O(1) work. Superset enumeration is symmetric with total time O(βˆ‘_{k=0}^{n} C(n,k) 2^{n-k}) = O(3^n). Space complexity is typically O(2^n) when you store arrays indexed by masks (e.g., dp[mask], cost[mask], or precomputed values per subset). Additional overhead includes O(1) variables per loop and possibly O(2^n) auxiliary arrays if you precompute costs or perform transforms (e.g., SOS DP uses O(2^n) memory as well). When using 64-bit masks ( for safety), space for storing masks themselves is minimal compared to arrays over all subsets. If you must compute F[m] = A[s] for all m, consider SOS DP which runs in O(n 2^n) time and O(2^n) space, asymptotically better than O(3^n) for large n. In practice on competitive programming judges, plain submask enumeration is feasible up to around n β‰ˆ 22–24 for tight O(3^n) solutions with small constants. Beyond that, prefer transforms or problem-specific pruning to keep runtime within limits.

Code Examples

Enumerate all submasks of a mask (including the empty set)
1#include <bits/stdc++.h>
2using namespace std;
3
4// Helper to print a mask as a bitstring of length n
5string bits(unsigned m, int n){
6 string s(n, '0');
7 for(int i = 0; i < n; ++i) if(m & (1u<<i)) s[n-1-i] = '1'; // MSB on left for readability
8 return s;
9}
10
11int main(){
12 ios::sync_with_stdio(false);
13 cin.tie(nullptr);
14
15 int n = 5; // number of bits (universe size)
16 unsigned mask = 0b10110u; // the set {1,2,4}
17
18 cout << "Mask(" << bits(mask, n) << ") submasks including empty:\n";
19
20 // do-while form includes s == 0 exactly once
21 unsigned s = mask;
22 do {
23 cout << bits(s, n) << "\n";
24 if(s == 0) break; // stop after processing empty submask
25 s = (s - 1) & mask; // next submask idiom
26 } while(true);
27
28 // Note: If you want to skip the empty submask, use for(s = mask; s > 0; s = (s-1) & mask)
29
30 return 0;
31}
32

This program prints all submasks of a given mask using the standard idiom s = (s - 1) & mask. The do-while variant ensures that the empty submask (s = 0) is included exactly once. The bits() helper formats masks as fixed-length bitstrings for clarity.

Time: O(2^k) where k = popcount(mask)Space: O(1)
Compute F[mask] = sum of A[submask] for all submasks (direct 3^n approach)
1#include <bits/stdc++.h>
2using namespace std;
3
4int main(){
5 ios::sync_with_stdio(false);
6 cin.tie(nullptr);
7
8 int n = 18; // Keep n moderate; O(3^n) work. Try n <= 20 on typical judges.
9 const int N = 1 << 18; // We will only use (1<<n) entries.
10
11 // Read or generate input array A of size 2^n. Here we generate small random values.
12 int size = 1 << n;
13 vector<long long> A(size), F(size, 0);
14 mt19937_64 rng(12345);
15 uniform_int_distribution<int> dist(0, 9);
16 for(int m = 0; m < size; ++m) A[m] = dist(rng);
17
18 // Direct subset enumeration: F[m] = sum_{s subset m} A[s]
19 for(int m = 0; m < size; ++m){
20 long long acc = 0;
21 unsigned s = (unsigned)m;
22 // Enumerate all submasks including 0 via do-while
23 do {
24 acc += A[s];
25 if(s == 0) break;
26 s = (s - 1u) & (unsigned)m;
27 } while(true);
28 F[m] = acc;
29 }
30
31 // Output a small sample
32 for(int m = 0; m < min(size, 8); ++m){
33 cout << "m=" << m << " F[m]=" << F[m] << '\n';
34 }
35
36 // Note: For large n, prefer SOS DP (O(n 2^n)) to compute all F faster.
37 return 0;
38}
39

Given an array A over all masks, we compute F[m] = sum of A[s] across all submasks s of m by directly enumerating submasks for each m. This showcases the classic O(3^n) pattern: each pair (m, s) with s βŠ† m is visited once. For larger n, a zeta transform (SOS DP) reduces the time to O(n 2^n).

Time: O(3^n) overall across all masksSpace: O(2^n)
Enumerate all supersets of a required mask to find minimal cost superset
1#include <bits/stdc++.h>
2using namespace std;
3
4int main(){
5 ios::sync_with_stdio(false);
6 cin.tie(nullptr);
7
8 int n = 6; // number of features
9 int size = 1 << n;
10
11 // Suppose cost[s] is known for each set s (e.g., price of a bundle). We'll generate mock costs.
12 vector<int> cost(size, INT_MAX/4);
13 // Some random bundles with random costs
14 vector<pair<unsigned,int>> bundles = {
15 {0b000111u, 5}, {0b101010u, 7}, {0b111000u, 4}, {0b111111u, 12}, {0b010001u, 3}
16 };
17 for(auto [b, c] : bundles) cost[b] = min(cost[b], c);
18
19 // We want the minimal cost superset of required mask 'need' (e.g., required features)
20 unsigned need = 0b001011u; // require features {0,1,3}
21
22 // Precompute bestCost[s]: best available cost for exactly s (or INF if not sold)
23 // If combining bundles is allowed, you would do DP over OR; here we pick exactly one superset
24
25 int best = INT_MAX/4; unsigned arg = need;
26 for(unsigned s = need; s < (1u<<n); s = (s + 1u) | need){
27 if(cost[s] < best){ best = cost[s]; arg = s; }
28 }
29
30 if(best >= INT_MAX/4) cout << "No available superset found.\n";
31 else {
32 cout << "Best superset mask = " << bitset<6>(arg) << ", cost = " << best << "\n";
33 }
34
35 return 0;
36}
37

Given a required feature set need, we iterate over all supersets using s = (s + 1) | need to find the one with minimal cost. This loop touches exactly the 2^{n-popcount(need)} supersets of need in increasing order without testing unrelated masks.

Time: O(2^{n - popcount(need)})Space: O(1)
Partition by allowed groups: dp[mask] = min groups to cover mask (submask enumeration)
1#include <bits/stdc++.h>
2using namespace std;
3
4// We are given n elements (n <= 20) and a boolean array allowed[s] for all submasks s.
5// In one step, we may take any allowed non-empty submask of the remaining elements.
6// Goal: cover all elements with the minimum number of steps.
7
8int main(){
9 ios::sync_with_stdio(false);
10 cin.tie(nullptr);
11
12 int n = 12; // keep n moderate; DP is O(3^n) in worst case
13 int N = 1 << n;
14
15 // Example: an element set partitioned into 3 cliques of size 4; only subsets within a clique are allowed.
16 // Build allowed[s] true iff s is subset of exactly one block of 4 consecutive indices.
17 vector<char> allowed(N, 0);
18 vector<int> blockStart = {0, 4, 8};
19 for(int b : blockStart){
20 int blockMask = 0;
21 for(int i = 0; i < 4; ++i) blockMask |= (1 << (b + i));
22 // all non-empty submasks of this block are allowed
23 for(int s = blockMask; s; s = (s - 1) & blockMask) allowed[s] = 1;
24 }
25
26 const int INF = 1e9;
27 vector<int> dp(N, INF);
28 dp[0] = 0; // zero elements need zero groups
29
30 // Iterate masks in increasing order of bits set for better cache behavior (optional)
31 vector<int> order(N);
32 iota(order.begin(), order.end(), 0);
33 stable_sort(order.begin(), order.end(), [](int a, int b){ return __builtin_popcount((unsigned)a) < __builtin_popcount((unsigned)b); });
34
35 for(int m : order){
36 if(dp[m] == INF) continue;
37 if(m == N-1) continue; // already full
38 // remaining elements to cover
39 unsigned rem = ((unsigned)N - 1u) ^ (unsigned)m;
40 if(rem == 0) continue;
41 // Choose a pivot (least significant remaining bit) to avoid duplicates in grouping order
42 unsigned pivot = rem & -rem; // lowbit
43 // Enumerate submasks s of rem that include pivot and are allowed
44 for(unsigned s = rem; s; s = (s - 1u) & rem){
45 if(!(s & pivot)) continue; // enforce pivot in s to avoid permutations
46 if(!allowed[s]) continue; // only allowed groups
47 int nm = m | (int)s;
48 dp[nm] = min(dp[nm], dp[m] + 1);
49 }
50 }
51
52 int ans = dp[N-1];
53 cout << "Minimum number of groups to cover all elements: " << ans << "\n";
54
55 return 0;
56}
57

We have n elements and a predicate allowed[s] that tells whether submask s can be taken in one move (e.g., forming a valid team). The DP state dp[mask] is the minimal number of moves to cover exactly the elements in mask. For each mask, we enumerate submasks of the remaining elements, but we require each chosen submask to contain a pivot bit (the least significant remaining bit) to avoid counting the same partition multiple times in different orders. This is a standard pattern when using submask enumeration for grouping/covering problems.

Time: O(3^n) in the worst case (pruning by allowed and pivot reduces constants)Space: O(2^n)