skip to content

Why does counting an integer from 0 to 2^n - 1 enumerate every subset of an n-element set?

level: middleimportance: must knowfreq 58%

answer

  1. how many yes-or-no choices is a subset
  2. how many bits does the counter have
  3. count the values in the range
  4. the counter is not an index into anything
  5. one number per subset, no lists needed

basics

~20 s

Every n-bit integer is a distinct in-or-out choice for each of the n elements, and counting from 0 to 2^n - 1 visits every n-bit pattern exactly once. The number itself is the subset; no list is built.

solid answer

~50 s

A subset of n elements is n independent yes/no decisions, and an n-bit integer holds exactly n such bits — so the integers 0 through 2^n - 1 are in one-to-one correspondence with the subsets, and a counting loop walks all of them exactly once with no bookkeeping. Bit i of the loop variable means "element i is in this subset", tested with `(mask & (1 << i)) != 0`. The key mental shift is that the mask *is* the subset: you only materialize a list of elements if the surrounding problem needs one, and often you can accumulate directly from the bits instead. Cost is 2^n masks, times O(n) if you scan the bits inside, so O(2^n * n) overall; 0 is the empty set and 2^n - 1 is the full set, both included. Iteration order is numeric, not by subset size.

code

pseudocode · 13 lines
pseudocode
n = length(people)
best = INFINITY
for mask in 0..(1 << n) - 1
    // bit i set means people[i] is on this candidate shift team
    coverage = 0
    size = 0
    for i in 0..n-1
        if (mask & (1 << i)) != 0
            coverage = coverage | availability[i]
            size = size + 1
    if coverage == FULL_WEEK and size < best
        best = size
...

go deeper

for a junior

Be able to say that each bit of the counter stands for one element being in or out, and that the loop body runs 2^n times. Recognising the mask-counting loop for what it is counts as the answer here.

for a middle

State the bijection out loud, read a single bit with the isolate-and-compare test, and quote the cost as the mask count times whatever the body does. Know that mask zero is the empty subset and that the order is numeric.

for a senior

Show that you do not materialize what you do not need: fold each candidate's contribution straight out of the bits and keep only the running best. Be explicit that doubling per added element is what decides whether the technique is usable.

for a principal

Own the size boundary. Decide when an exhaustive scan is the honest, cheap and reviewable answer, and when its growth is a trap that a later change in data volume turns into an incident.

## The bijection Choosing a subset of an n-element collection means answering n independent yes/no questions: is element 0 in, is element 1 in, and so on. There are 2 answers for each of n questions, hence 2^n possible subsets. An n-bit binary integer is precisely n independent yes/no digits. So the map "bit i is 1 means element i is in the subset" is a bijection between the integers 0..2^n - 1 and the subsets — every subset gets exactly one integer, every integer names exactly one subset. A counting loop visits every value in that range exactly once, in increasing numeric order. That is the entire enumeration: no stack, no visited set, no recursion, no partial results to copy. The loop counter *is* the state. ## Reading a mask To ask whether element i is in the subset named by `mask`, isolate its bit: `(mask & (1 << i)) != 0`. `1 << i` is a value with a single 1 at position i; ANDing keeps that bit only if the mask has it and clears everything else, so the result is either that power of two or zero. Scanning i from 0 to n-1 walks the subset's members. Two boundaries are worth saying out loud in an interview because they are where off-by-one errors live. Mask 0 has no bits set, so it is the empty subset — it is included, and an algorithm that assumes a non-empty selection must skip or guard it. Mask `(1 << n) - 1` has all n low bits set, so it is the full set, and it is the last value the loop visits. The loop bound is `< (1 << n)`, inclusive of `(1 << n) - 1`; writing `< (1 << n) - 1` silently drops the full set, and writing `<= (1 << n)` produces one mask with a bit outside the universe. ## The wrong answer this aims at Many candidates hear "enumerate all subsets" and immediately describe building a collection of collections — for each of 2^n possibilities, construct a fresh list of the chosen elements and store it. That is a legitimate output format when the caller genuinely wants the subsets themselves, but it is not the enumeration, and it is usually the expensive part. Materializing all subsets costs O(2^n * n) space; the counting loop costs O(1) extra space and only touches the elements it needs. On a scoring problem — pick the cheapest on-call team that covers every day of the week — you never need a list at all: you fold each candidate's contribution directly out of the bits and keep the best answer seen. Say that explicitly; it is what separates "I memorized the trick" from "I understand the encoding". ## Cost The outer loop is 2^n iterations. If the body scans all n bits, the total is O(2^n * n); if the body does constant work per mask, it is O(2^n). Both are exponential, and the base is what matters: for a twelve-person roster, 2^12 = 4096 masks, and with a 12-bit inner scan that is under 50,000 operations — free. Add ten more people and it is 4 million masks; add ten more again and it is 4 billion. The technique's shape is what makes it teachable, but its cost is what decides whether it is usable, and doubling every added element is not a gentle curve. A common refinement: if the inner scan is the bottleneck, precompute per-element contributions so the body is constant time, or derive each mask's value from a mask with one fewer bit, since removing the lowest set bit (`mask & (mask - 1)`) yields a smaller mask you have already processed. ## Order, and what the order does not give you The loop visits masks in increasing numeric order, which is *not* order by subset size. Mask 3 (two elements) comes before mask 4 (one element). If you need subsets grouped by cardinality — all singletons, then all pairs — numeric order will not deliver it; you either sort masks by the number of set bits or filter by that count on each pass. Candidates frequently assume the counting loop hands them a size-ordered enumeration and build a greedy algorithm on that false premise. One genuinely useful ordering property does hold: every mask that fits inside the low k bits is visited before any mask using bit k, because setting a higher bit makes the number larger. That is why the enumeration naturally processes subsets of a prefix before extending into new elements. ## Where the encoding pays beyond enumeration Because a subset is one integer, it is also a cheap key. A search over configurations — which of the switches are currently on — can record visited states as integers rather than as sets of sets, which turns an expensive structural hash and equality check into a machine-word comparison, and shrinks the memory footprint of the visited collection by an order of magnitude. That compactness is the same property the enumeration relies on: the set has been reduced to a number.

  • Which subsets do mask 0 and mask (1 << n) - 1 name, and why does that matter?
    Mask 0 has no bits set, so it is the empty subset, and `(1 << n) - 1` has all n low bits set, so it is the full set. Both are inside the loop's range, so an algorithm that requires a non-empty selection must guard against the zero mask, and a loop bound written as `< (1 << n) - 1` silently skips the full set.
  • Does the counting loop hand you subsets ordered by size?
    No — it hands them back in numeric order, so a two-element subset like `011` precedes the one-element subset `100`. If you need cardinality order you must sort masks by their number of set bits or make one filtering pass per size. Assuming size order and building a greedy rule on top of it is a common and quiet bug.
  • The inner bit scan dominates your loop. How do you cut it?
    Either make the body constant time by precomputing each element's contribution and combining it with a table indexed by mask, or build each mask's answer from a smaller one: clearing the lowest set bit with `mask & (mask - 1)` gives a strictly smaller mask that the loop has already processed, so one combine step replaces the n-step scan.

saying these in an interview costs you the question

  • Says enumerating subsets means building a collection of lists
  • Loops to 2^n - 1 exclusive and drops the full set
  • Forgets that mask zero is the empty subset
  • Assumes masks arrive ordered by subset size
  • Quotes the cost as O(2^n) while scanning all bits inside
  • Confuses the mask with an index into the element array

context