Phase 19 - Lesson 19.8

Bit Manipulation and State Compression

Subsets as integers: enumerating \(2^n\) states, popcount and lowbit tricks, subset-sum DP, and the \(O(2^n n^2)\) travelling-salesman DP.

⏱ 60 min● Advanced🔗 Prereqs: 19.7 dynamic programming
↖ Phase 19 hub
Builds on: 19.7 needed a compact state; a bitmask IS the compact state when the state is ‘which subset have I used?’.
Leads to: 19.10 uses masks in meet-in-the-middle; 19.14 tells you when \(2^n\) is affordable. The Euler Lab’s Dynamic Programming and Combinatorics paths both hide bitmask problems.

Learning Objectives

Click a status chip to cycle: Not started → In progress → Studied → Practiced → Needs review → Mastered.

Key Vocabulary

Bitmask
An integer whose \(i\)-th bit records membership of element \(i\). Union = OR, intersection = AND, symmetric difference = XOR, complement = XOR with the full mask.
Popcount
The number of set bits. Python: bin(x).count('1') or x.bit_count() (3.10+).
Lowbit
\(x\ \&\ (-x)\) isolates the lowest set bit - the standard way to iterate over the elements of a mask in \(O(\text{popcount})\).
Submask enumeration
s = (s-1) & mask walks all submasks of mask in decreasing order; over all masks this totals \(3^n\) steps, not \(4^n\).
Held–Karp
The bitmask DP for TSP: state \((\text{visited set},\ \text{current city})\), cost \(O(2^n n^2)\) time and \(O(2^n n)\) memory.
State compression
Encoding an exponential-but-small state (a subset, a profile of a grid row) as an integer so it can index a DP array.

Intuition & Motivation

Intuition
A subset of \(\{0,\dots,n-1\}\) is a list of yes/no answers - and a list of yes/no answers is a binary number. Once you see that, the set operations become single CPU instructions and the DP table becomes a flat array indexed by the subset itself. The catch is honest and permanent: there are \(2^n\) subsets. Bitmask DP does not defeat exponential growth; it makes the exponent small enough to survive (roughly \(n\le20\) for \(2^n n^2\)).

Sets as integers

Set operationBit operationNote
Is \(i\in S\)?(S >> i) & 11 if present
Add \(i\)S | (1 << i)idempotent
Remove \(i\)S & ~(1 << i)or S ^ (1<<i) if you know it is present
Toggle \(i\)S ^ (1 << i)XOR is its own inverse
\(S\cup T,\ S\cap T,\ S\triangle T\)S|T, S&T, S^Tone instruction each
Complement in \([n]\)S ^ ((1<<n) - 1)XOR with the full mask
\(|S|\)S.bit_count()popcount
Lowest element(S & -S).bit_length() - 1\(x\ \&\ {-x}\) isolates the lowest set bit
Is \(T\subseteq S\)?(S & T) == Tnothing outside \(S\)

The identity behind lowbit: in two’s complement, \(-x=\lnot x+1\), which flips every bit above the lowest set bit and leaves that bit set. Hence \(x\ \&\ (-x)\) keeps precisely the lowest 1. Example: \(x=12=1100_2\), \(-x=\ldots10100_2\), \(x\ \&\ (-x)=100_2=4\).

Enumerating subsets and submasks

\[\text{all subsets: } S=0,1,\dots,2^n-1.\qquad \text{all submasks of }M:\quad s=M;\ \ \text{while } s:\ \ \{\text{use }s\};\ \ s=(s-1)\ \&\ M.\] (19.18)
Theorem - The \(3^n\) bound
Summing the number of submasks over all masks gives \(\sum_{M\subseteq[n]}2^{|M|}=\sum_{k=0}^n\binom nk 2^k=(1+2)^n=3^n\) by the binomial theorem. So the nested ‘for each mask, for each submask’ loop is \(O(3^n)\), not \(O(4^n)\) - a distinction that decides feasibility around \(n=20\) (\(3^{20}\approx3.5\times10^9\) vs \(4^{20}\approx1.1\times10^{12}\)).

Bitmask DP

The signature use: the state contains ‘which items/cities have I already used?’, which is a subset, plus a small extra coordinate.

Definition - Held–Karp (TSP)
Let \(D[S][j]\) be the length of the shortest path that starts at city 0, visits exactly the set \(S\) (with \(0,j\in S\)), and ends at \(j\). Then
\[D[S][j]=\min_{i\in S\setminus\{j\}}\Big( D[S\setminus\{j\}][i] + w(i,j)\Big),\qquad \text{answer}=\min_j \big(D[\text{full}][j]+w(j,0)\big).\] (19.19)

There are \(2^n n\) states and each takes \(O(n)\) work, so the total is \(O(2^n n^2)\) time and \(O(2^n n)\) memory - versus \((n-1)!\) for brute force. For \(n=20\): \(2^{20}\cdot400\approx4\times10^8\) (a few minutes in Python, seconds in C) against \(19!\approx1.2\times10^{17}\) (impossible). Exponential, but a survivable exponential - the exact distinction 19.14 teaches you to make.

Worked Example - Subset-sum reachability with a single integer
1
Question: from \(\{3,34,4,12,5,2\}\), which totals \(\le 20\) are achievable?
2
Represent the set of reachable sums as ONE big integer \(R\), where bit \(t\) is set iff total \(t\) is reachable. Start \(R=1\) (only 0 is reachable).
3
Adding an item of value \(v\) to the pool: every previously-reachable \(t\) now also gives \(t+v\). Shifting all bits up by \(v\) does that in one operation: R |= R << v.
4
Process 3, 34, 4, 12, 5, 2 in turn. After all six items, bit \(t\) of \(R\) is set iff some sub-multiset sums to \(t\).
5
Check \(t=9\): \(3+4+2=9\) ✓, so bit 9 is set. Check \(t=11\): \(4+5+2=11\) ✓. Check \(t=1\): no subset sums to 1, so bit 1 is clear.
6
Cost: \(O(n)\) big-integer shifts on a \(\Sigma v\)-bit integer - in CPython this beats an explicit boolean DP array by an order of magnitude, because the bit-parallelism happens inside the big-int machinery. Same \(O(n\Sigma)\) work; a far better constant.

Coding workbench

In the Euler Lab, bitmasks show up wherever the phrase ‘choose a subset’ or ‘which have I used?’ appears - digit-permutation problems, exact-cover-flavoured puzzles, and the smaller travelling-salesman variants in the Dynamic Programming path.

Common Mistakes to Avoid
  • Confusing & precedence in Python: S >> k & 1 works, but S & 1 << k == 0 does NOT mean what you think - parenthesise.
  • Iterating \(k\in S\) by looping \(k=0..n-1\) and testing each bit when the mask is sparse. Use the lowbit loop instead.
  • Believing bitmask DP makes an exponential problem polynomial. \(O(2^n n^2)\) is still exponential; it just moves the wall from \(n\approx12\) to \(n\approx20\).
  • Allocating \(2^n\times n\) Python floats for \(n=25\) - that is 800 million cells. Check the memory before you write the loop (19.14).
  • Forgetting that Python ints are signed and unbounded, so ~x is negative. Mask explicitly: x ^ ((1<<n)-1).
Quant Practitioner Tips
  • int.bit_count() (Python 3.10+) is a single instruction on modern CPUs; below that, bin(x).count('1') is still surprisingly fast.
  • Big-integer bitsets are CPython’s secret weapon: R |= R << v does thousands of DP updates in one operation. Reach for it whenever a boolean DP array is indexed by a sum.
  • Order your subset loop by increasing mask value: since removing a bit always decreases the integer, \(S\setminus\{j\}\lt S\), so ascending mask order is automatically a valid topological order for the DP.
  • The \(3^n\) submask sum is worth memorising: it is what makes subset-convolution and SOS (sum-over-subsets) DP practical at \(n=20\).

Practice this in the Euler Lab

Computational problems that exercise exactly this technique. Each opens in the Euler Lab with a Python workbench, a progressive hint ladder, and answer checking. Tier A/B run at full scale in the browser.

Warm-up:
#103 Special Subset Sums: Optimum (14%, tier C)

Applied:
#105 Special Subset Sums: Testing (21%, tier B) #106 Special Subset Sums: Meta-testing (21%, tier B) #944 Sum of Elevisors (22%, tier C)

Challenge:
#766 Sliding Block Puzzle (41%, tier C) #189 Tri-colouring a Triangular Grid (44%, tier C)

49 Project Euler problems in total are mapped to this lesson. Open the Euler Lab to filter them all.

Knowledge Check

Q1 Medium
x & (x-1) computes:
x with the lowest set bit cleared
The lowest set bit
The popcount
x divided by 2
Q2 Hard
Looping over all masks and, for each, all of its submasks costs:
\(O(2^n)\)
\(O(4^n)\)
\(O(3^n)\), by \(\sum_k\binom nk 2^k=3^n\)
\(O(n2^n)\)
Q3 Medium
Held–Karp solves TSP in \(O(2^n n^2)\). At roughly what \(n\) does this stop being feasible on a normal machine?
Around n = 8
Around n = 20
Around n = 100
It is polynomial, so any n

Practical Exercise

(a) Prove that \(x\ \&\ (-x)\) isolates the lowest set bit, using the two’s-complement identity \(-x=\lnot x+1\). (b) You must decide whether any subset of 40 given integers sums to a target \(T\). Brute force is \(2^{40}\approx10^{12}\). Give a \(O(2^{n/2}n)\) algorithm. (c) State the exact memory cost of Held–Karp for \(n=22\) with 8-byte cells, and say whether it fits in 8 GB.

▶ Show full solution

(a) Let the lowest set bit of \(x\) be at position \(k\), so \(x=\ldots b\,1\,\underbrace{0\cdots0}_{k}\). Then \(\lnot x=\ldots \bar b\,0\,\underbrace{1\cdots1}_{k}\) and adding 1 carries through those \(k\) ones, producing \(-x=\ldots\bar b\,1\,\underbrace{0\cdots0}_{k}\). Comparing \(x\) and \(-x\): below position \(k\) both are 0; at \(k\) both are 1; above \(k\) the bits are complementary. The AND therefore keeps only bit \(k\), i.e. \(x\ \&\ (-x)=2^k\).

(b) Meet in the middle (developed further in 19.10). Split the 40 numbers into halves \(A\) and \(B\) of 20. Enumerate all \(2^{20}\approx10^6\) subset sums of each half (a bitmask loop). Sort \(B\)’s sums. For each sum \(a\) of \(A\), binary-search \(T-a\) in \(B\)’s sorted list. Cost: \(O(2^{n/2}\cdot n)\) time (about \(2\times10^6\) sums plus \(10^6\log(10^6)\approx2\times10^7\) comparisons) and \(O(2^{n/2})\) memory. Feasible in seconds, versus \(10^{12}\) for brute force.

(c) States: \(2^{22}\times22=4{,}194{,}304\times22=92{,}274{,}688\) cells. At 8 bytes each that is \(738\ \text{MB}\) - which does fit in 8 GB as a C array, but in CPython, where each list element is a pointer to a boxed float (8 bytes pointer + ≥24 bytes object), the true footprint is several gigabytes and the allocation alone will take minutes. Use a flat array / NumPy buffer of float32 or int32 (369 MB) if you must run it at all.

After the reveal, answer for yourself: Part (c) is the lesson: the asymptotic bound said ‘fits’, the language’s constant factor said ‘no’. Which of the two would you have checked first before this lesson?

Lesson Summary

A subset is an integer, so set operations become single bit operations and a DP indexed by subsets becomes a flat array. Popcount, x & (x-1) (clear lowest bit) and x & -x (isolate lowest bit) give tight inner loops; submask enumeration over all masks costs \(3^n\), not \(4^n\). Held–Karp solves TSP in \(O(2^n n^2)\), and the big-integer trick R |= R << v performs subset-sum reachability in bit-parallel. None of this defeats exponential growth - it just makes \(n\le20\) survivable.

Retrieval Practice

Close the lesson and answer from memory before checking. This is deliberate, effortful recall - the single highest-yield study action.

▶ Show retrieval prompts & answers
Q: What do x & (x-1) and x & -x do, and why?
A: The first CLEARS the lowest set bit (\(x-1\) flips it and sets everything below); the second ISOLATES it (two's complement makes all higher bits complementary). Together they give an \(O(\text{popcount})\) element loop.
Q: Why is ‘for each mask, for each submask’ \(O(3^n)\)?
A: A mask with \(k\) bits has \(2^k\) submasks and there are \(\binom{n}{k}\) of them; \(\sum_k\binom nk 2^k=(1+2)^n=3^n\).

Completion Checklist

Confidence / mastery rating
Personal notes