Bitmask Techniques — Comprehensive Pattern Guide


What Are Bitmask Techniques?

Bitmask techniques use binary representations and bitwise operations to efficiently solve problems involving:

  1. Subsets and combinations: Representing sets as binary numbers
  2. State compression: Encoding complex states in integers
  3. Fast set operations: Using bitwise AND, OR, XOR for O(1) operations
  4. Dynamic programming: Tracking “which elements have been processed”

Key Idea: An n-bit integer can represent a subset of n elements, where bit i indicates whether element i is included.

Example: Set {0, 1, 2, 3}, subset {0, 2, 3}
Binary: 1101 (reading right to left: positions 0, 2, 3 are set)
Decimal: 13

All subsets of {0, 1, 2}:
000 (0) = {}
001 (1) = {0}
010 (2) = {1}
011 (3) = {0, 1}
100 (4) = {2}
101 (5) = {0, 2}
110 (6) = {1, 2}
111 (7) = {0, 1, 2}

When to use bitmask: n ≤ 20-24 elements (2^20 ≈ 1 million states)


Quick Navigation: I need to…

I need to… Technique Numbered LC Jump to
List every subset of n items Subset enumeration 78 Subsets §2 · walkthrough
Split items into k fair groups / buckets Subset DP over groups 2305 Fair Distribution, 698 Partition to K §3 · walkthrough
Assign n workers to n jobs, min cost Assignment DP dp[mask] 1879 Min XOR Sum of Two Arrays §3 Pattern 2
Visit ALL nodes, cost depends on where I am now TSP dp[mask][last] 847 Shortest Path Visiting All Nodes §3 Pattern 1 · walkthrough
Aggregate a value over all submasks of every mask SOS DP 1178 Valid Words for Puzzles §5 · walkthrough
Find max XOR of a subset XOR (linear) basis 1707 Max XOR With Element §5 max_xor_subset
Tile a grid / narrow board Profile DP dp[col][profile] 1349 Max Students §6 Template 3

The one split that matters: are you manipulating the bits of ONE number (bit tricks — §1, §4) or is the set of chosen items itself the DP state key (bitmask DP — §3, §5)? Different skill, same syntax.


Master LeetCode Comparison Table

LC # Title Diff State Transition (one-liner) Cost
78 Subsets Easy mask ∈ [0, 2^n) bit i set → include nums[i] O(2^n · n)
698 Partition to K Equal Sum Subsets Medium dp[mask] = filled amount of current bucket (mod target) add unused item if it fits current bucket O(2^n · n)
2305 Fair Distribution of Cookies Medium dp[j][mask] = min unfairness giving mask to first j kids give kid j some submask s of mask O(k · 3^n)
847 Shortest Path Visiting All Nodes Hard (node, mask) = at node, visited set mask BFS to neighbor, mask │ (1<<nb) O(2^n · n²)
1178 Number of Valid Words for Each Puzzle Hard cnt[wordmask] frequency sum cnt[sub] over submasks of puzzle containing first letter O(W·7 + P·2^7)

Read the State column top to bottom: mask alone (78, 698) → dp[j][mask] splitting a set (2305) → (node, mask) position + set (847) → precomputed masks queried by submask (1178). That progression is the learning ladder — each row adds one dimension to what the mask tracks.


Table of Contents

  1. Fundamental Bit Operations
  2. Subset Enumeration Techniques
  3. Bitmask DP Patterns
  4. Advanced Bit Tricks
  5. SOS (Sum Over Subsets) DP
  6. Common Problem Templates
  7. Practice Problems

Why This Pattern Exists

When n ≤ ~20 and you must track which subset of items is chosen/visited — not just how many — a bitmask encodes the subset as the bits of a single integer. Then 2^n possible subsets become plain array indices, and set operations (union, intersection, membership) become one machine instruction.

Concrete examples:

  • TSP: mask = set of cities already visited, so dp[mask][i] is “cheapest tour visiting exactly those cities, ending at i.”
  • Assignment: mask = set of workers already assigned, so dp[mask] is “cheapest way to assign those workers to the first popcount(mask) jobs.”

The size limit is the whole point: 2^20 ≈ 1,000,000 states is fine, but 2^25 (≈ 33M) starts to hurt and factorial enumeration (20! ≈ 2.4×10^18) is hopeless. A bitmask turns an exponential enumeration into an exponential table you fill once.

★ Insight ─────────────────────────────────────

  • The bitmask is not the algorithm — it is the state key. You still need a DP recurrence (or search) over those keys. Bitmask just makes “the set of used elements” a cheap, hashable index.
  • Reach for it only when the identity of the chosen elements matters. If you only need a count or a sum, a cheaper 1-D DP usually beats 2^n. ─────────────────────────────────────────────────

1. Fundamental Bit Operations

Basic Operations

# ============================================
# CHECK, SET, CLEAR, TOGGLE
# ============================================

mask = 0b10110  # Binary: 10110

# Check if bit n is set
def is_set(mask, n):
    return bool(mask & (1 << n))

# Set bit n to 1
def set_bit(mask, n):
    return mask | (1 << n)

# Clear bit n (set to 0)
def clear_bit(mask, n):
    return mask & ~(1 << n)

# Toggle bit n
def toggle_bit(mask, n):
    return mask ^ (1 << n)

# Example
n = 2
print(f"Is bit {n} set in {bin(mask)}? {is_set(mask, n)}")  # True
mask = clear_bit(mask, n)
print(f"After clearing bit {n}: {bin(mask)}")  # 0b10010

Counting and Finding Bits

# ============================================
# POPCOUNT, LOWEST BIT, TRAILING ZEROS
# ============================================

# Count number of set bits (popcount)
def popcount_builtin(mask):
    return bin(mask).count('1')

def popcount_kernighan(mask):
    """Brian Kernighan's algorithm - O(number of set bits)"""
    count = 0
    while mask:
        mask &= mask - 1  # Remove lowest set bit
        count += 1
    return count

# Get the lowest set bit
def lowest_bit(mask):
    return mask & -mask
    # Example: 10110 & -10110 = 00010

# Remove the lowest set bit
def remove_lowest_bit(mask):
    return mask & (mask - 1)
    # Example: 10110 → 10100

# Count trailing zeros (position of lowest set bit)
def count_trailing_zeros(mask):
    if mask == 0:
        return 32  # Or 64 for 64-bit
    return (mask & -mask).bit_length() - 1

# Position of rightmost set bit (1-indexed)
def rightmost_bit_position(mask):
    return (mask & -mask).bit_length()

# Examples
mask = 0b10110
print(f"Popcount: {popcount_builtin(mask)}")  # 3
print(f"Lowest bit: {bin(lowest_bit(mask))}")  # 0b10
print(f"Trailing zeros: {count_trailing_zeros(mask)}")  # 1

Checking Properties

# ============================================
# CHECKING BITMASK PROPERTIES
# ============================================

# Check if power of 2 (exactly one bit set)
def is_power_of_2(mask):
    return mask > 0 and (mask & (mask - 1)) == 0

# Check if two masks are disjoint (no common bits)
def are_disjoint(mask1, mask2):
    return (mask1 & mask2) == 0

# Check if mask1 is subset of mask2
def is_subset(mask1, mask2):
    return (mask1 & mask2) == mask1

# Get bits in mask1 but not in mask2
def difference(mask1, mask2):
    return mask1 & ~mask2

# Get all bits up to position n
def all_bits(n):
    return (1 << n) - 1
    # Example: n=5 → 0b11111 (31)

# Examples
print(is_power_of_2(8))  # True
print(is_power_of_2(6))  # False
print(are_disjoint(0b1100, 0b0011))  # True
print(is_subset(0b0101, 0b1101))  # True

★ Insight ─────────────────────────────────────

  • x & -x isolates the lowest set bit because two’s-complement negation flips-then-adds-1, so -x matches x above the lowest 1 (all flipped) and the lowest 1 aligns. This one identity powers popcount (Kernighan), trailing-zero count, and Fenwick tree index stepping — learn it once, reuse everywhere.
  • x & (x-1) clears the lowest set bit; x & (x+1)-style tricks turn bits on. The is_subset(a, b) == ((a & b) == a) test is the membership check bitmask DP leans on constantly — “is this smaller set contained in that one” in one AND. ─────────────────────────────────────────────────

2. Subset Enumeration Techniques

Iterate All Subsets

# ============================================
# ENUMERATE ALL 2^n SUBSETS
# ============================================

def enumerate_all_subsets(n):
    """Generate all subsets of {0, 1, ..., n-1}"""
    for mask in range(1 << n):
        subset = [i for i in range(n) if mask & (1 << i)]
        yield mask, subset

# Example: All subsets of {0, 1, 2}
for mask, subset in enumerate_all_subsets(3):
    print(f"{mask:03b} → {subset}")

# Output:
# 000 → []
# 001 → [0]
# 010 → [1]
# 011 → [0, 1]
# 100 → [2]
# 101 → [0, 2]
# 110 → [1, 2]
# 111 → [0, 1, 2]

Iterate All Submasks

# ============================================
# ENUMERATE ALL SUBMASKS OF A MASK
# ============================================

def enumerate_submasks(mask):
    """
    Generate all submasks of mask in descending order.

    Key technique: submask = (submask - 1) & mask
    Time: O(2^k) where k = popcount(mask)
    """
    submask = mask
    while submask > 0:
        yield submask
        submask = (submask - 1) & mask

# Example: Submasks of 0b1011 (11)
mask = 0b1011
print(f"Submasks of {bin(mask)}:")
for submask in enumerate_submasks(mask):
    print(f"  {submask:04b} ({submask})")

# Output:
# Submasks of 0b1011:
#   1011 (11)
#   1010 (10)
#   1001 (9)
#   1000 (8)
#   0011 (3)
#   0010 (2)
#   0001 (1)

# Including empty submask
def enumerate_submasks_with_zero(mask):
    """Include the empty submask (0)"""
    submask = mask
    while True:
        yield submask
        if submask == 0:
            break
        submask = (submask - 1) & mask

Iterate All Submask Pairs

# ============================================
# ENUMERATE ALL WAYS TO PARTITION A MASK
# ============================================

def enumerate_submask_pairs(mask):
    """
    Generate all ways to partition mask into two non-empty parts.
    Returns (submask, complement) pairs.
    """
    submask = mask
    while submask > 0:
        complement = mask ^ submask  # mask XOR submask
        if complement > 0:  # Both must be non-empty
            yield (submask, complement)
        submask = (submask - 1) & mask

# Optimization: Avoid duplicate pairs
def enumerate_unique_submask_pairs(mask):
    """Generate unique pairs where submask <= complement"""
    submask = mask
    while submask > 0:
        complement = mask ^ submask
        if submask <= complement and complement > 0:
            yield (submask, complement)
        submask = (submask - 1) & mask

# Example: Partition 0b111 (7)
mask = 0b111
print("All partitions:")
for s1, s2 in enumerate_unique_submask_pairs(mask):
    print(f"  {s1:03b} | {s2:03b}")

# Output:
#   001 | 110
#   010 | 101
#   011 | 100

Iterate k-Element Subsets (Gosper’s Hack)

# ============================================
# ENUMERATE ALL k-ELEMENT SUBSETS
# ============================================

def next_combination(mask):
    """
    Get next mask with same number of bits set.
    Gosper's hack - generates combinations in lexicographic order.
    """
    c = mask & -mask  # Rightmost set bit
    r = mask + c      # Add it
    return (((r ^ mask) >> 2) // c) | r

def enumerate_k_subsets(n, k):
    """Generate all C(n, k) subsets of size k from {0, 1, ..., n-1}"""
    if k == 0:
        yield 0
        return

    mask = (1 << k) - 1  # Start with k rightmost bits set
    limit = 1 << n

    while mask < limit:
        yield mask
        mask = next_combination(mask)

# Example: All 3-element subsets of 5 elements
print("All 3-element subsets of {0,1,2,3,4}:")
for mask in enumerate_k_subsets(5, 3):
    elements = [i for i in range(5) if mask & (1 << i)]
    print(f"  {mask:05b} → {elements}")

# Output:
#   00111 → [0, 1, 2]
#   01011 → [0, 1, 3]
#   01101 → [0, 2, 3]
#   01110 → [1, 2, 3]
#   10011 → [0, 1, 4]
#   ... (total of C(5,3) = 10 subsets)

Problem 78 — Subsets

Difficulty: Easy · This template solves: the Iterate All Subsets template.

Given an integer array nums of unique elements, return all possible subsets (the power set). The solution set must not contain duplicate subsets.

The bitmask insight: there are exactly 2^n subsets, and the integers 0 .. 2^n - 1 are in bijection with them. Integer mask encodes the subset “take nums[i] iff bit i is set.” So a single for mask in range(1 << n) loop enumerates the entire power set — no recursion, no backtracking bookkeeping.

class Solution:
    def subsets(self, nums: list[int]) -> list[list[int]]:
        n = len(nums)
        out = []
        for mask in range(1 << n):
            out.append([nums[i] for i in range(n) if mask & (1 << i)])
        return out

Trace on nums = [1, 2, 3] (bit 0 → 1, bit 1 → 2, bit 2 → 3):

mask  binary   bits set        subset
 0    000      —               []
 1    001      0               [1]
 2    010      1               [2]
 3    011      0,1             [1, 2]
 4    100      2               [3]
 5    101      0,2             [1, 3]
 6    110      1,2             [2, 3]
 7    111      0,1,2           [1, 2, 3]
                                → 2^3 = 8 subsets

★ Insight ─────────────────────────────────────

  • The mask-as-subset bijection is why bitmask beats recursion here: the loop counter mask is the subset. Bit order gives a free, deterministic ordering — no visited set, no dedup.
  • This only works because elements are unique. If nums has duplicates, two different masks can yield the same multiset — then you sort + skip, or count occurrences, because the bijection breaks. ─────────────────────────────────────────────────

3. Bitmask DP Patterns

Pattern 1: Hamiltonian Path / TSP Tour

Problem: Visit all n cities exactly once starting from city 0, minimizing total distance.

State: dp[mask][i] = minimum cost to visit cities in mask, ending at city i

Open path or closed tour? Decide before you write the return line. The DP table is identical for both; only the final answer differs.

Variant What it asks Answer line
Hamiltonian path (this code, LC 847) visit everything, stop wherever min(dp[full][i])
TSP tour (the classic TSP, DP §11) visit everything and return to 0 min(dp[full][i] + dist[i][0])

The two give genuinely different numbers — on the matrix below, the open path costs 65 and the closed tour costs 80. Returning the path cost on a problem that wanted the tour is a silent wrong answer, not a crash, so it survives your own testing and dies on submission.

def hamiltonian_path_cost(dist, n, close_tour=False):
    """
    Cheapest route from city 0 visiting every city exactly once (open path).
    Set `close_tour=True` to get the classic TSP tour instead.

    State: dp[mask][i] = min cost to visit cities in mask, ending at city i
    Transition: Try going from city i to unvisited city j

    Time: O(2^n * n^2)
    Space: O(2^n * n)
    """
    INF = float('inf')
    # dp[mask][i] = min cost to reach state (mask, i)
    dp = [[INF] * n for _ in range(1 << n)]

    # Base case: start at city 0
    dp[1][0] = 0

    # Fill DP table
    for mask in range(1 << n):
        for u in range(n):
            # Skip if city u not in mask or unreachable
            if not (mask & (1 << u)) or dp[mask][u] == INF:
                continue

            # Try going to unvisited city v
            for v in range(n):
                if mask & (1 << v):  # v already visited
                    continue

                next_mask = mask | (1 << v)
                dp[next_mask][v] = min(dp[next_mask][v],
                                      dp[mask][u] + dist[u][v])

    # THE ONLY LINE THAT DIFFERS between the two variants:
    full_mask = (1 << n) - 1
    if close_tour:
        return min(dp[full_mask][i] + dist[i][0] for i in range(n))   # TSP tour
    return min(dp[full_mask][i] for i in range(n))                    # open path

# Example usage
dist = [
    [0, 10, 15, 20],
    [10, 0, 35, 25],
    [15, 35, 0, 30],
    [20, 25, 30, 0]
]
print(hamiltonian_path_cost(dist, 4))                    # 65  -> 0->1->3->2, stop
print(hamiltonian_path_cost(dist, 4, close_tour=True))   # 80  -> 0->1->3->2->0

Problem 847 — Shortest Path Visiting All Nodes

Difficulty: Hard · This template solves: the dp[mask][last] state, in BFS form.

Undirected connected graph of n nodes. Return the length of the shortest path that visits every node. You may start and stop at any node, revisit nodes, and reuse edges.

This is TSP’s cousin: the state is still (mask, last) = “visited set mask, currently standing on last.” But because every edge has weight 1 and you may revisit, the shortest path is a plain BFS over that state space — the first time BFS reaches any (full_mask, *) is the answer. Revisiting means mask can stay the same while node changes, so this isn’t a Hamiltonian path (each node once); it’s “cover all nodes, steps minimized.”

from collections import deque

class Solution:
    def shortestPathLength(self, graph: list[list[int]]) -> int:
        n = len(graph)
        if n == 1:
            return 0
        full = (1 << n) - 1
        # Start BFS from EVERY node simultaneously (multi-source).
        seen = set()
        q = deque()
        for i in range(n):
            q.append((i, 1 << i, 0))   # (node, visited mask, dist)
            seen.add((i, 1 << i))

        while q:
            node, mask, dist = q.popleft()
            if mask == full:
                return dist
            for nb in graph[node]:
                nm = mask | (1 << nb)
                if (nb, nm) not in seen:
                    seen.add((nb, nm))
                    q.append((nb, nm, dist + 1))
        return -1

Trace on graph = [[1,2,3],[0],[0],[0]] — a star with hub 0 and leaves 1, 2, 3. full = 1111.

Start (dist 0): (0,0001) (1,0010) (2,0100) (3,1000)   all seeded

dist 1: from 0 → 1,2,3   gives (1,0011)(2,0101)(3,1001)
        from 1 → 0       gives (0,0011)
        from 2 → 0       gives (0,0101)
        from 3 → 0       gives (0,1001)
dist 2: (0,0011)→2,3: (2,0111)(3,1011);  (0,0101)→…(3,1101); (0,1001)→…(2,1101)
        also leaves push back to hub, growing masks
dist 3: reach (0,0111)→3 = (3,1111) ✓  → return 3?  No — hub must be re-entered.

Path 1→0→2→0→3 visits {1,0,2,3}=full in 4 steps → answer 4.

Running the code returns 4: to cover all leaves you must bounce through the hub between each, so the state (0, 0111) is reached at dist 3, then step to (3, 1111) at dist 4.

★ Insight ─────────────────────────────────────

  • Two encodings of the SAME state. Weighted TSP fills a dp[mask][last] table (Pattern 1). Unit-weight “visit all” uses BFS over (mask, last) — BFS is the DP, dequeue order guarantees the first full-mask hit is shortest.
  • Multi-source start is the trick that makes “start anywhere” free: seed all n start states at dist 0 instead of running n separate BFS runs. The 2^n · n state count is the ceiling, so BFS is O(2^n · n²) with the neighbor loop. ─────────────────────────────────────────────────

Pattern 2: Assignment Problem

Problem: Assign n workers to n jobs, minimize total cost.

State: dp[mask] = minimum cost to assign workers in mask to first k jobs

def min_cost_assignment(cost, n):
    """
    Assign n workers to n jobs to minimize cost.

    State: dp[mask] = min cost to assign workers in mask to jobs 0..k-1
           where k = popcount(mask)

    Time: O(2^n * n)
    Space: O(2^n)
    """
    INF = float('inf')
    dp = [INF] * (1 << n)
    dp[0] = 0

    for mask in range(1 << n):
        if dp[mask] == INF:
            continue

        # Number of jobs already assigned
        jobs_assigned = bin(mask).count('1')

        # Try assigning next job to each unassigned worker
        for worker in range(n):
            if mask & (1 << worker):  # Worker already assigned
                continue

            next_mask = mask | (1 << worker)
            dp[next_mask] = min(dp[next_mask],
                               dp[mask] + cost[worker][jobs_assigned])

    return dp[(1 << n) - 1]

# Example: 3 workers, 3 jobs
cost = [
    [9, 2, 7],  # Worker 0's cost for jobs 0, 1, 2
    [6, 4, 3],  # Worker 1's cost
    [5, 8, 1]   # Worker 2's cost
]
print(f"Minimum assignment cost: {min_cost_assignment(cost, 3)}")
# Optimal: Worker 0→Job 1 (2), Worker 1→Job 0 (6), Worker 2→Job 2 (1) = 9

Trace on the 3×3 matrix above. dp[mask] = min cost to assign the workers in mask, where jobs_assigned = popcount(mask) is the next job index to fill. Bit w set = worker w used.

cost = [[9,2,7],    # worker 0 → jobs 0,1,2
        [6,4,3],    # worker 1
        [5,8,1]]    # worker 2

dp[000] = 0                          (nothing assigned, next job = 0)

# popcount 0 → fill job 0
dp[001] = cost[0][0] = 9              (worker 0 → job 0)
dp[010] = cost[1][0] = 6              (worker 1 → job 0)
dp[100] = cost[2][0] = 5              (worker 2 → job 0)

# popcount 1 → fill job 1
dp[011] = min(9+cost[1][1]=13, 6+cost[0][1]=8) = 8    (w0,w1 used)
dp[101] = min(9+cost[2][1]=17, 5+cost[0][1]=7) = 7    (w0,w2 used)
dp[110] = min(6+cost[2][1]=14, 5+cost[1][1]=9) = 9    (w1,w2 used)

# popcount 2 → fill job 2
dp[111] = min(dp[011]+cost[2][2]=8+1=9,
              dp[101]+cost[1][2]=7+3=10,
              dp[110]+cost[0][2]=9+7=16) = 9

answer = dp[111] = 9

The winning path is dp[010]=6 (w1→job0) → dp[011]=8 (w0→job1, +2) → dp[111]=9 (w2→job2, +1), i.e. worker 1→job 0, worker 0→job 1, worker 2→job 2 — exactly the optimum in the comment.

Pattern 3: Partition DP

Problem: Merge/partition items optimally (like the merge lists problem).

State: dp[mask] = minimum cost to merge all items in mask

def optimal_partition_dp(items, n):
    """
    Generic partition DP template.
    Find optimal way to partition/merge items.

    State: dp[mask] = (min_cost, merged_result)
    Transition: Try all ways to split mask into two parts

    Time: O(3^n) - for each mask, enumerate all submasks
    Space: O(2^n)
    """
    # dp[mask] = (cost, data)
    dp = {}

    # Base cases: single items
    for i in range(n):
        dp[1 << i] = (0, items[i])

    # Process all masks in increasing order of popcount
    for mask in range(1, 1 << n):
        if mask in dp:  # Base case
            continue

        min_cost = float('inf')
        best_result = None

        # Try all ways to partition mask
        submask = mask
        while submask > 0:
            complement = mask ^ submask

            # Both parts must be non-empty and computed
            if complement > 0 and submask in dp and complement in dp:
                cost1, data1 = dp[submask]
                cost2, data2 = dp[complement]

                # Cost to merge these two parts
                merge_cost = compute_merge_cost(data1, data2)
                total_cost = cost1 + cost2 + merge_cost

                if total_cost < min_cost:
                    min_cost = total_cost
                    best_result = merge_data(data1, data2)

            submask = (submask - 1) & mask

        if best_result is not None:
            dp[mask] = (min_cost, best_result)

    full_mask = (1 << n) - 1
    return dp[full_mask][0]

# Helper functions (problem-specific)
def compute_merge_cost(data1, data2):
    # Example: cost based on sizes and medians
    return len(data1) + len(data2) + abs(median(data1) - median(data2))

def merge_data(data1, data2):
    # Merge sorted lists, concatenate, etc.
    return sorted(data1 + data2)

Problem 698 — Partition to K Equal Sum Subsets

Difficulty: Medium · This template solves: the Assignment dp[mask] state (which items used) with a “fill one bucket at a time” transition.

Given nums and integer k, return true if you can divide nums into k non-empty subsets whose sums are all equal.

If a valid split exists, every bucket sums to target = sum(nums) / k. The state dp[mask] answers “using exactly the items in mask, what is the fill level of the bucket currently being built, taken mod target?” Adding items greedily fills bucket 1 to target, then mod target resets to 0 and the next item starts bucket 2 — so k separate buckets are tracked implicitly by the running total, never as k explicit sets.

class Solution:
    def canPartitionKSubsets(self, nums: list[int], k: int) -> bool:
        total = sum(nums)
        if total % k:
            return False
        target = total // k
        n = len(nums)
        nums.sort(reverse=True)
        if nums[0] > target:
            return False

        full = (1 << n) - 1
        # dp[mask] = fill level of current bucket (mod target) using items in mask; -1 = unreachable
        dp = [-1] * (1 << n)
        dp[0] = 0
        for mask in range(1 << n):
            if dp[mask] == -1:
                continue
            for i in range(n):
                if mask & (1 << i):
                    continue
                if dp[mask] + nums[i] <= target:          # item fits current bucket
                    nxt = mask | (1 << i)
                    if dp[nxt] == -1:
                        dp[nxt] = (dp[mask] + nums[i]) % target
        return dp[full] == 0

Trace on nums = [4,3,2,3,5,2,1], k = 4 → total = 20, target = 5. A bucket closes each time the running sum hits 5 (mod 5 → 0). Reaching dp[full] == 0 means all items placed with the last bucket exactly closed:

5            = {5}
4+1          = {4,1}
3+2          = {3,2}
3+2          = {3,2}          four buckets, each sums to 5 → dp[1111111] = 0 → True

canPartitionKSubsets([1,2,3,4], 3) → total = 10, not divisible by 3 → False immediately.

★ Insight ─────────────────────────────────────

  • The % target is the whole trick: one scalar tracks which bucket you’re filling AND how full it is. When the running total crosses a multiple of target, the modulo silently “starts the next bucket” — so dp[mask] needs only 2^n entries, not 2^n · k.
  • Sort descending + prune nums[0] > target kills the exponential early: the biggest item must fit in a bucket, and large-first packing fails fast. Same idea powers the backtracking solution, but here it just trims the DP. ─────────────────────────────────────────────────

The other LC 698 solution (and why the complexities differ)

You will find this problem solved two ways, with two different costs. Both are correct; they are different algorithms, not a contradiction.

  Mod-target (this section) Peel-off (Bitmask DP guide §5)
State dp[mask] = fill level of the current bucket, mod target dp[mask] = how many complete buckets the items in mask form
Transition add one unused item remove one whole valid submask
Cost O(2ⁿ · n) O(3ⁿ)
Generalises to nothing much — it leans on every bucket having the same target any per-group predicate: unequal targets, capacity limits, min-max objectives (LC 1986, 2305, 1723)

Prefer mod-target when you actually have LC 698’s equal-target structure — it is asymptotically cheaper. Reach for peel-off the moment the groups stop being interchangeable, which is what the rest of the partition family looks like.

Problem 2305 — Fair Distribution of Cookies

Difficulty: Medium · This template solves: the Partition DP submask enumeration — split a set among k recipients.

You have n bags of cookies (cookies[i]). Distribute all bags to k children; each bag goes to one child. A child’s unfairness is the total cookies they get. Minimize the maximum unfairness across the k children.

Now the mask is “the set of bags still to hand out,” and we hand child j some submask s of what’s left. State dp[j][mask] = min possible max-unfairness after giving bags mask to the first j children. For each child, enumerate every submask s of mask as that child’s haul.

class Solution:
    def distributeCookies(self, cookies: list[int], k: int) -> int:
        n = len(cookies)
        full = (1 << n) - 1

        # Precompute sum of every submask in O(2^n) via lowest-bit recurrence.
        tot = [0] * (1 << n)
        for mask in range(1, 1 << n):
            low = mask & -mask
            i = low.bit_length() - 1
            tot[mask] = tot[mask ^ low] + cookies[i]

        INF = float('inf')
        dp = [[INF] * (1 << n) for _ in range(k + 1)]
        dp[0][0] = 0
        for j in range(1, k + 1):
            for mask in range(1 << n):
                s = mask
                while True:                     # every submask s of mask = child j's haul
                    if dp[j - 1][mask ^ s] != INF:
                        dp[j][mask] = min(dp[j][mask], max(dp[j - 1][mask ^ s], tot[s]))
                    if s == 0:
                        break
                    s = (s - 1) & mask
        return dp[k][full]

Trace on cookies = [8,15,10,20,8], k = 2. Total = 61. Best split hands one child three bags summing to 30 and the other two summing to 31 → max = 31 (returned). With k = 3 on [6,1,3,2,2,4,1,2] the optimum max is 7.

★ Insight ─────────────────────────────────────

  • 698 fixed each group’s target and asked feasibility; 2305 has no target and minimizes the max — so the objective is max(previous_children, this_child), not a sum. Same submask machinery, different combine operator.
  • The tot[mask] = tot[mask ^ lowbit] + cookies[i] precompute is the reusable idiom: subset sums for all 2^n masks in O(2^n), by peeling the lowest set bit off an already-computed smaller mask. The submask loop then costs O(3^n) overall (each mask visits its submasks). ─────────────────────────────────────────────────

Pattern 4: Maximum Independent Set

Problem: Select maximum subset with no two adjacent elements.

def max_independent_set(adj, n):
    """
    Find maximum independent set in a graph.

    State: dp[mask] = max independent set size for nodes in mask
    Constraint: No two selected nodes can be adjacent

    Time: O(2^n * n^2)
    Space: O(2^n)
    """
    dp = [0] * (1 << n)

    # Check all subsets
    for mask in range(1 << n):
        # Check if mask is an independent set
        is_independent = True
        for i in range(n):
            if not (mask & (1 << i)):
                continue
            for j in range(i + 1, n):
                if not (mask & (1 << j)):
                    continue
                if adj[i][j]:  # i and j are adjacent
                    is_independent = False
                    break
            if not is_independent:
                break

        if is_independent:
            dp[mask] = bin(mask).count('1')
        else:
            # Try removing one node
            for i in range(n):
                if mask & (1 << i):
                    dp[mask] = max(dp[mask], dp[mask ^ (1 << i)])

    return dp[(1 << n) - 1]

# Example: Graph with 4 nodes
adj = [
    [0, 1, 1, 0],
    [1, 0, 0, 1],
    [1, 0, 0, 1],
    [0, 1, 1, 0]
]
print(f"Max independent set size: {max_independent_set(adj, 4)}")
# Optimal: nodes {0, 3} or {1, 2}, size = 2

4. Advanced Bit Tricks

Isolation and Manipulation

# ============================================
# ISOLATE AND MANIPULATE SPECIFIC BITS
# ============================================

# Isolate rightmost 1-bit
def rightmost_1(x):
    return x & -x
    # 10110 → 00010

# Isolate rightmost 0-bit (turn it to 1)
def rightmost_0(x):
    return ~x & (x + 1)
    # 10101 → 00010

# Turn off rightmost 1-bit
def turn_off_rightmost_1(x):
    return x & (x - 1)
    # 10110 → 10100

# Turn on rightmost 0-bit
def turn_on_rightmost_0(x):
    return x | (x + 1)
    # 10101 → 10111

# Isolate rightmost block of 1s
def rightmost_block(x):
    return x ^ (x - 1)
    # 10111000 → 00000111 (last block of 1s)

# Turn off rightmost block of 1s
def turn_off_block(x):
    return x & (x + 1)
    # 10111000 → 10000000

Swap and Reverse

# ============================================
# SWAP AND REVERSE BITS
# ============================================

def swap_bits(x, i, j):
    """Swap bits at positions i and j"""
    if ((x >> i) & 1) != ((x >> j) & 1):
        x ^= (1 << i) | (1 << j)
    return x

def reverse_bits(x, width=32):
    """Reverse bits of a number"""
    result = 0
    for _ in range(width):
        result = (result << 1) | (x & 1)
        x >>= 1
    return result

# Example
x = 0b10110
print(f"Original: {x:05b}")
x = swap_bits(x, 1, 3)
print(f"After swapping bits 1 and 3: {x:05b}")

x = 0b10110
print(f"Reversed (5 bits): {reverse_bits(x, 5):05b}")

Gray Code

# ============================================
# GRAY CODE GENERATION
# ============================================

def gray_code(n):
    """
    Generate all 2^n Gray codes.
    Gray code: Adjacent codes differ by exactly 1 bit.

    Formula: gray(i) = i XOR (i >> 1)
    """
    return [i ^ (i >> 1) for i in range(1 << n)]

def inverse_gray(gray):
    """Convert Gray code back to binary"""
    binary = gray
    gray >>= 1
    while gray:
        binary ^= gray
        gray >>= 1
    return binary

# Example: 3-bit Gray codes
codes = gray_code(3)
print("Gray codes:")
for i, code in enumerate(codes):
    print(f"{i:03b} → {code:03b} (Gray)")

# Verify adjacent codes differ by 1 bit
for i in range(len(codes) - 1):
    diff = codes[i] ^ codes[i + 1]
    assert bin(diff).count('1') == 1, "Not a valid Gray code!"

5. SOS (Sum Over Subsets) DP

Standard SOS DP

Problem: For each mask, compute sum of values over all its submasks.

Naive: O(3^n) - for each mask, enumerate all submasks SOS DP: O(n × 2^n) - much faster!

def sos_dp(arr, n):
    """
    Sum Over Subsets DP.

    For each mask, compute: dp[mask] = Σ arr[submask] for all submasks of mask

    Key idea: Build up by considering one bit at a time.
    For bit i, dp[mask] = dp[mask with bit i off] + (dp[mask] if bit i is set)

    Time: O(n * 2^n)
    Space: O(2^n)
    """
    # Initialize with original array
    dp = arr[:]

    # For each bit position
    for i in range(n):
        # For each mask
        for mask in range(1 << n):
            # If bit i is set in mask
            if mask & (1 << i):
                # Add contribution from mask with bit i turned off
                dp[mask] += dp[mask ^ (1 << i)]

    return dp

# Example: Count number of submasks for each mask
n = 4
count = [1] * (1 << n)  # Each mask has itself as a submask
result = sos_dp(count, n)

print("Number of submasks (including self):")
for mask in range(1 << n):
    print(f"{mask:04b} ({mask:2d}) has {result[mask]:2d} submasks")

# Verification
for mask in range(1 << n):
    expected = 1 << bin(mask).count('1')  # 2^k where k = popcount
    assert result[mask] == expected

Problem 1178 — Number of Valid Words for Each Puzzle

Difficulty: Hard · This template solves: submask enumeration as a “sum over submasks” query (the problem SOS DP generalizes).

A word is valid for a puzzle if (1) it contains the puzzle’s first letter, and (2) every letter of the word appears in the puzzle. For each puzzle, return how many words are valid. (Only lowercase a–z; a puzzle has 7 distinct letters.)

Represent each word by a 26-bit mask of its distinct letters (order/count irrelevant). A word is valid for puzzle P iff wordmask is a submask of puzzlemask and contains the first-letter bit. Since a puzzle has only 7 letters, it has just 2^7 = 128 submasks — so for each puzzle, enumerate its submasks and sum the precomputed word-frequency of each.

from collections import Counter

class Solution:
    def findNumOfValidWords(self, words: list[str], puzzles: list[str]) -> list[int]:
        cnt = Counter()
        for w in words:
            m = 0
            for c in w:
                m |= 1 << (ord(c) - 97)   # distinct-letter mask
            cnt[m] += 1

        res = []
        for p in puzzles:
            first = 1 << (ord(p[0]) - 97)
            pmask = 0
            for c in p:
                pmask |= 1 << (ord(c) - 97)

            total = 0
            sub = pmask                    # enumerate submasks of the 7-letter puzzle
            while True:
                if sub & first:            # must contain first letter
                    total += cnt.get(sub, 0)
                if sub == 0:
                    break
                sub = (sub - 1) & pmask
            res.append(total)
        return res

Trace the counting idea on puzzle "aboveyz" (first letter a). Its letter set is {a,b,e,o,v,y,z}. Word "aaaa" → mask {a}, which is a submask containing a → counts. Word "asas" → mask {a,s}; s ∉ puzzle → not a submask → excluded. Over the sample the result is [1, 1, 3, 2, 4, 0].

★ Insight ─────────────────────────────────────

  • Flip the naive “for each word, test each puzzle” (O(W·P)) into “for each puzzle, look up submasks” (O(P · 2^7)). The bound comes from the puzzle side being fixed at 7 letters — the submask count is constant, independent of word count.
  • The if sub & first guard replaces condition (1). Every submask of pmask automatically satisfies condition (2), so the two rules collapse to one submask walk plus a single bit test — the same shape SOS DP would compute in bulk if puzzles shared masks. ─────────────────────────────────────────────────

Inverse SOS (Sum Over Supersets)

def superset_dp(arr, n):
    """
    For each mask, compute sum over all supersets.

    Time: O(n * 2^n)
    """
    dp = arr[:]

    for i in range(n):
        for mask in range(1 << n):
            # If bit i is NOT set
            if not (mask & (1 << i)):
                # Add contribution from mask with bit i turned on
                dp[mask] += dp[mask | (1 << i)]

    return dp

# Example
n = 3
arr = [i for i in range(1 << n)]
result = superset_dp(arr, n)

print("Sum over supersets:")
for mask in range(1 << n):
    print(f"{mask:03b}: sum = {result[mask]}")

Applications of SOS DP

# ============================================
# APPLICATION 1: COUNT SUBSETS WITH PROPERTY
# ============================================

def count_subsets_with_and_zero(arr, n):
    """
    Count pairs (i, j) where arr[i] & arr[j] == 0.

    Approach: For each arr[i], count how many arr[j] are subsets of ~arr[i]
    """
    max_val = max(arr)
    bits = max_val.bit_length()

    # Count frequency of each value
    freq = [0] * (1 << bits)
    for x in arr:
        freq[x] += 1

    # SOS DP to get count of all submasks
    sos = sos_dp(freq, bits)

    # For each element, count compatible elements
    count = 0
    for x in arr:
        complement = ((1 << bits) - 1) ^ x  # All bits flipped
        count += sos[complement]
        if x == 0:  # Don't count self-pair
            count -= 1

    return count // 2  # Each pair counted twice


# ============================================
# APPLICATION 2: MAXIMUM XOR SUBSET
# ============================================

def max_xor_subset(nums):
    """
    Find subset with maximum XOR value.
    Uses Gaussian elimination on bit vectors.

    Time: O(n * log(MAX))
    """
    basis = []

    for num in nums:
        cur = num
        # Try to reduce cur using existing basis vectors
        for b in basis:
            cur = min(cur, cur ^ b)

        if cur != 0:
            basis.append(cur)
            basis.sort(reverse=True)

    # Greedily build the max XOR: take a basis vector only if it
    # turns ON a bit that is still off (i.e. increases the result).
    # basis is sorted high-bit-first, so this locks in every high bit.
    result = 0
    for b in basis:
        result = max(result, result ^ b)

    return result

6. Common Problem Templates

Template 1: Subset Selection DP

def subset_selection_template(items, n):
    """
    Choose optimal subset of items.

    State: dp[mask] = optimal value for subset represented by mask
    """
    dp = [0] * (1 << n)

    # Base case
    dp[0] = 0  # Empty subset

    # Fill DP table
    for mask in range(1 << n):
        # Option 1: Try adding each item not in mask
        for i in range(n):
            if mask & (1 << i):
                continue

            next_mask = mask | (1 << i)
            dp[next_mask] = max(dp[next_mask],
                               dp[mask] + items[i])

        # Option 2: Transition from submasks
        # dp[mask] = max over all submasks

    return dp[(1 << n) - 1]

Template 2: Position-Based DP

def position_based_template(items, n):
    """
    Track both subset AND current position.

    State: dp[mask][pos] = optimal value for subset mask, currently at pos
    Common in TSP, Hamiltonian path problems.
    """
    INF = float('inf')
    dp = [[INF] * n for _ in range(1 << n)]

    # Base case: start at each position
    for i in range(n):
        dp[1 << i][i] = items[i]

    # Fill DP table
    for mask in range(1 << n):
        for last in range(n):
            if not (mask & (1 << last)):
                continue
            if dp[mask][last] == INF:
                continue

            # Try going to next position
            for next_pos in range(n):
                if mask & (1 << next_pos):
                    continue

                next_mask = mask | (1 << next_pos)
                transition_cost = cost(last, next_pos)
                dp[next_mask][next_pos] = min(
                    dp[next_mask][next_pos],
                    dp[mask][last] + transition_cost
                )

    # Return best ending position
    full_mask = (1 << n) - 1
    return min(dp[full_mask])

Template 3: Profile DP (Grid Problems)

def profile_dp_template(n, m):
    """
    Use bitmask to represent state of one dimension (column/row).
    Common in tiling problems.

    State: dp[col][profile] = ways to tile up to column col
           profile = bitmask representing which cells extend to next column

    Time: O(n * m * 2^m)
    """
    dp = [{} for _ in range(m + 1)]
    dp[0][0] = 1  # Base: empty grid

    for col in range(m):
        for profile, ways in dp[col].items():
            # Generate next profiles by filling current column
            fill_column(n, col, profile, 0, 0, ways, dp[col + 1])

    # Final state: no cells extending beyond last column
    return dp[m].get(0, 0)

def fill_column(rows, col, cur_profile, row, next_profile, ways, next_dp):
    """Recursively fill one column, generating next profile."""
    if row == rows:
        # Finished filling this column
        next_dp[next_profile] = next_dp.get(next_profile, 0) + ways
        return

    if cur_profile & (1 << row):
        # Cell already filled from previous column
        fill_column(rows, col, cur_profile, row + 1,
                   next_profile, ways, next_dp)
    else:
        # Option 1: Place vertical tile (occupies this and next row)
        if row + 1 < rows and not (cur_profile & (1 << (row + 1))):
            fill_column(rows, col, cur_profile | (1 << (row + 1)),
                       row + 2, next_profile, ways, next_dp)

        # Option 2: Place horizontal tile (extends to next column)
        fill_column(rows, col, cur_profile, row + 1,
                   next_profile | (1 << row), ways, next_dp)

7. Practice Problems

Beginner Level

LC # Problem Technique Key move
78 Subsets Subset enumeration for mask in range(1<<n)
461 Hamming Distance Bit counting popcount(a ^ b)
136 Single Number XOR properties a ^ a = 0, XOR all
231 Power of Two Bit check n & (n-1) == 0
338 Counting Bits DP on bits dp[i] = dp[i>>1] + (i&1)

Intermediate Level

LC # Problem Technique Key move
698 Partition to K Equal Sum Subsets Bitmask DP dp[mask] = bucket fill mod target
2305 Fair Distribution of Cookies Submask partition DP split mask among k, min-max
1125 Smallest Sufficient Team Subset-cover DP dp[skillmask] = min team
473 Matchsticks to Square Bitmask DP (k=4) 698 with fixed k=4

Advanced Level

LC # Problem Technique Key move
847 Shortest Path Visiting All Nodes TSP dp[mask][last] multi-source BFS over (mask,node)
943 Find the Shortest Superstring TSP + overlap weights dp[mask][last] on overlaps
1178 Number of Valid Words for Each Puzzle Submask / SOS query submasks of 7-letter puzzle
1349 Maximum Students Taking Exam Profile DP dp[row][seatmask], check adjacency
1707 Maximum XOR With an Element From Array XOR (linear) basis / trie greedy high-bit basis

Complexity Quick Reference

Technique Time Complexity Use Case
Iterate all subsets O(2^n) Basic enumeration
Iterate all submasks O(3^n) total Partition problems
SOS DP O(n × 2^n) Sum over subsets
Gosper’s hack O(C(n,k)) k-element subsets
TSP DP O(2^n × n²) Shortest Hamiltonian path
Assignment DP O(2^n × n) Matching problems
Profile DP O(n × m × 2^m) Grid tiling

Practical Limits:

  • n ≤ 20: Safe for most bitmask DP (2^20 ≈ 1M)
  • n ≤ 22: Feasible with optimization (2^22 ≈ 4M)
  • n ≤ 24: Cutting edge (2^24 ≈ 16M, need good constants)
  • n > 24: Consider meet-in-the-middle or other techniques

Which Bitmask Pattern Do I Need?

The task is… Pattern State / loop Cost
List every subset of n items Subset enumeration for mask in range(1<<n) O(2^n)
Pick a best subset under per-item values/constraints Subset-sum / value DP dp[mask], add one bit at a time O(2^n · n)
Order all items (visit/assign in sequence), cost depends on last TSP / permutation-over-mask dp[mask][last] O(2^n · n²)
Split a set into two groups and recurse on each Submask enumeration sub = (sub-1) & mask O(3^n) total
Aggregate a value over all submasks of every mask SOS DP bit-by-bit sweep O(n · 2^n)

Quick tell: if only which items matters → dp[mask]; if the order/last item matters → dp[mask][last]; if you must partition a set → submask loop.


Common Pitfalls

Pitfall Symptom Fix
1 << n vs 1 << (n-1) off-by-one full mask wrong / last item dropped full set is (1 << n) - 1; there are 1 << n masks (0 .. 2^n-1)
Missing parentheses in mask >> i & 1 wrong bit tested — & binds looser than >> here, but relational/in mixes bite you always write (mask >> i) & 1
Iterating submasks with while sub > 0 the empty submask 0 is skipped use the while True: ... if sub==0: break; sub=(sub-1)&mask form when 0 matters
Wrong submask step (e.g. sub-1 alone) visits masks that aren’t submasks the step is exactly sub = (sub - 1) & mask
Integer vs set confusion mask + (1<<i) when bit i may already be set corrupts state add with OR: mask | (1 << i); test with (mask >> i) & 1
Forgetting the base case dp[full] stays INF set dp[0]=0 (subset DP) or dp[1<<start][start]=0 (TSP)

Key Takeaways

  1. Bitmask = Subset: n-bit integer represents subset of n elements
  2. Submask enumeration is O(3^n) total: submask = (submask - 1) & mask
  3. SOS DP reduces O(3^n) to O(n × 2^n) for subset aggregation
  4. Common patterns:
    • Selection: dp[mask]
    • Ordering: dp[mask][pos]
    • Partitioning: enumerate submask pairs
  5. Bit tricks provide O(1) set operations
  6. Practical limit: n ≤ 20-24 elements

When to use:

  • Small n (≤ 20-24)
  • Need to track “which elements used”
  • Subset/combination problems
  • Optimization over all possible states

When NOT to use:

  • Large n (> 25)
  • Continuous/infinite state space
  • Problems better solved with greedy/graph algorithms

Additional Resources

  • CSES: Hamiltonian Flights, Elevator Rides
  • Codeforces: Bitmask DP tutorial by -is-this-fft-
  • CP Handbook: Chapter on Bitmask DP
  • AtCoder DP Contest: Problems M, O, X use bitmasks

THREE MOVES BEHIND EVERY BITMASK PROBLEM

1. ENCODE   — make the mask the state key
              "which items are chosen/visited" = the bits of one integer.
              n ≤ ~20, and identity (not just count) must matter.

2. TRANSITION — add one bit, or split into submasks
              dp[mask]        : add an unused item      mask | (1 << i)
              dp[mask][last]  : move to a new position   (order matters)
              submask loop    : hand a group away        s = (s-1) & mask

3. AGGREGATE — sum / min / max / count over masks
              feasibility (698), min-max (2305), shortest (847),
              count-over-submasks (1178, SOS).

Decision in one line: only which items matters → dp[mask]; order or current position matters → dp[mask][last]; must partition a set → submask loop; aggregate over all submasks → SOS DP.


When This Fails

Bitmask techniques are bounded by the size of the set, and by what a bit can represent:

  • n > 20 or so. 2^n states stops fitting. At n ≈ 40, meet-in-the-middle splits the set in half and searches 2^(n/2) on each side. Beyond that, look for different structure.
  • Submask enumeration at n > 16. 3^16 is about 43 million — borderline. 3^20 is 3.5 billion — not happening. Know which cost class you are in before you write it.
  • Order matters within the subset. A mask records membership only. If the sequence matters, you need a second dimension (dp[mask][last]), which multiplies the state count by n.
  • Elements are not distinguishable by position. A bitmask indexes items by a fixed position; multiset problems where only counts matter are usually better served by a different state.
  • Off-by-one on shifts. 1 << n is the full-mask bound, (1 << n) - 1 is the full mask. In C++, 1 << 31 overflows a signed int — use 1LL.

Self-Test

Answer these from memory, out loud or on paper, before looking. Recognition is not recall: rereading an explanation feels like knowing, and it is not. A question you cannot answer cold names the exact section to revisit — you do not need to reread the guide.

1. Give the five bit operations you cannot function without.

Answer

Test bit i: mask & (1 << i). Set it: mask | (1 << i). Clear it: mask & ~(1 << i). Toggle it: mask ^ (1 << i). Population count: bin(mask).count('1') or mask.bit_count() in Python 3.10+.

2. What does mask & (mask - 1) do, and mask & -mask?

Answer

mask & (mask - 1) clears the lowest set bit — loop on it to visit set bits in O(popcount) rather than O(n). mask & -mask isolates the lowest set bit, which is exactly the Fenwick tree’s lowbit.

3. Write the submask enumeration idiom and explain the termination.

Answer

sub = mask; while sub: ...; sub = (sub - 1) & mask. Subtracting one borrows through the low zeros and the mask-and re-restricts to mask’s bits, walking every submask in decreasing order. It stops at zero, so handle the empty submask separately if you need it.

4. Why is submask enumeration over all masks O(3^n) rather than O(4^n)?

Answer

Each of the n bit positions is independently in one of three states: in the submask, in the mask but not the submask, or in neither. That is 3^n (mask, submask) pairs total. Equivalently Σ_mask 2^popcount(mask) = 3^n by the binomial theorem.

5. What is SOS DP computing, and at what cost?

Answer

For every mask, an aggregate over all of its submasks — in O(2^n · n) instead of the naive O(3^n). It processes one bit position at a time, letting each mask absorb the value of the mask with that bit cleared, which is a subset-sum analogue of a prefix sum in n dimensions.

6. dp[mask] or dp[mask][last] — how do you decide?

Answer

Ask whether the future depends on which item you finished on. Assignment and partition: no, only membership matters, so dp[mask]. TSP or shortest superstring: yes, because the next cost depends on your current position, so add the last dimension and pay a factor of n.

7. LC 698 has two solutions with different complexities. What are they and when does each apply?

Answer

Mod-target dp[mask] tracking the current bucket’s fill level, O(2^n · n) — valid because all k buckets share one target. Peel-off via submask enumeration, O(3^n) — slower but works for unequal targets, capacities, and min-max objectives. Use the first on LC 698 itself, the second for the rest of the family.

8. You need to enumerate subsets and n = 40. Now what?

Answer

Meet in the middle: split into two halves of 20, enumerate 2^20 on each side, sort one half, and combine with binary search or hashing. 2 · 2^20 is trivial where 2^40 is not.


See Also

  • Bitmask DP — Subset Partition — the specialist guide for the partition/TSP family (LC 1986, 2305, 698, 943, 1494) that lives in the O(3^n) submask row.
  • Dynamic Programming §11 — where bitmask DP sits among the other 12 DP families, if you arrived from the DP side.
  • Backtracking §10 — the same mask used as a live visited set during search, before it becomes a table.
  • Contribution Counting §6 — the other way to use bits: decompose an answer per bit position and sum the columns.
  • Pattern Decision Map — the router: which technique does a cold problem call for?
  • Pattern Mastery Program — the spaced-repetition schedule, mastery checklist, and drill formats that turn reading into recall.

Pattern mastered — stop enumerating permutations; let the bits of one integer be the set, and fill the table once. 🚀