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 definitions01Overview
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
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
Code Examples
1 #include <bits/stdc++.h> 2 using namespace std; 3 4 // Helper to print a mask as a bitstring of length n 5 string 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 11 int 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.
1 #include <bits/stdc++.h> 2 using namespace std; 3 4 int 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).
1 #include <bits/stdc++.h> 2 using namespace std; 3 4 int 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.
1 #include <bits/stdc++.h> 2 using 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 8 int 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.