Here is something that will bother you if you let it. You now have a
beautiful recursive template — a seven-line function that generates
subsets, combinations, or permutations depending on a single expression.
You have the radio dial. You know how to tune it. That template will
carry you through most interview problems in this family.
But it has a weakness, and the weakness is structural.
Every time you call subsets(nums, i + 1, path, result), you are pushing
a frame onto the call stack. For a set of 20 elements, that is a recursion
depth of 20. For 30 elements? 30 frames deep. The call stack is doing
real work at each level — storing the idx pointer, the path reference,
the return address — and all of that occupies memory proportional to n.
Push frames onto the stack below and watch the overhead accumulate:
There is also a subtler cost. Recursion asks you to hold multiple
levels of the call tree in your head simultaneously. When you are
debugging a subset bug at depth 5, you have to trace backwards
through 5 frames to understand how the current path was assembled.
Each frame is a context switch in your mental model. That is fine
when the problem requires it — when you need pruning, backtracking,
or conditional branching. But when the problem is simply “list every
subset,” all of that machinery is overhead. You are paying for
flexibility you do not need.
And then there is the mutable state problem. The path array is
shared across all recursive calls. You push an element before
recursing, then pop it after returning. If you forget the pop, every
subsequent subset includes a phantom element that should have been
removed. If you forget to clone the path when adding it to the
result (result.push([...path]) instead of result.push(path)),
all entries in your result array point to the same array object —
and they all end up as the empty set after the recursion unwinds.
These bugs are not conceptually difficult. They are mechanically
easy to introduce and surprisingly hard to spot in a stressful
interview when you are writing code on a whiteboard.
What if you could generate every subset without a single recursive call?
No call stack, no backtracking, no choose-unchoose dance. No mutable
path array to push and pop. Just a for loop and some arithmetic.
Below is a row of three toggle switches. Each switch represents one
element of the set {A, B, C}. Try it — flip each switch and notice
how the subset changes:
Each switch is a binary decision: include or exclude. Three switches, three independent decisions. Your job now: flip the switches to build specific subsets. No code. No recursion. No function calls. Just physical toggles on a panel.
Toggle the switches and see if you can spot the pattern.
Build: {B, C}
Turn ON the bits for elements B and C
You just discovered that counting from 0 to 2^n - 1 in binary IS
subset enumeration. Every integer maps to exactly one subset. 000 is
the empty set. 001 is {C}. 010 is {B}. 011 is {B, C}.
Continue the sequence: 100, 101, 110, 111. That is 0 through
7 in binary. Eight integers. Eight subsets.
Watch it happen. Step through the counter below and see each mask produce its corresponding subset:
There is something almost unsettling about how clean this is. You did
not need to construct a tree. You did not need to make include/exclude
decisions one at a time, backtracking when you reached a leaf. You
just counted. And counting in binary IS the sequence of all possible
on/off configurations for n switches — which is exactly what subset
membership is.
Notice what happened between mask 3 (011) and mask 4 (100). In
decimal, that is just “3 plus 1 equals 4.” In binary, every bit
flipped — the rightmost bits rolled over like an odometer turning
from 099 to 100. And the subset changed from {B, C} to {A} — a
completely different combination of elements. The binary counter does
not care about “which elements go together.” It systematically walks
through every possible configuration by the simple act of adding one.
The combinatorics are embedded in the number system itself.
Why exactly 2^n? Each element gets one bit position. Bit ON means
“include this element.” Bit OFF means “exclude it.” The n bits of an
integer encode exactly one include/exclude decision per element. Since
each bit is independently on or off, and there are n bits, the total
number of possible configurations is 2 * 2 * 2 * ... * 2 (n times) —
which is 2^n. This is not a coincidence — it is the deep structural
reason why a set of n elements has exactly 2^n subsets.
Let me lay out the full correspondence for n = 3 so you can see
the pattern at a glance:
| Decimal | Binary | Subset |
|---|---|---|
| 0 | 000 | { } |
| 1 | 001 | { C } |
| 2 | 010 | { B } |
| 3 | 011 | { B, C } |
| 4 | 100 | { A } |
| 5 | 101 | { A, C } |
| 6 | 110 | { A, B } |
| 7 | 111 | { A, B, C } |
Look at the binary column. Notice the patterns within the patterns.
The rightmost bit (C's column) alternates every row: 0, 1, 0, 1, 0,
1, 0, 1. The middle bit (B's column) alternates every two rows: 0,
0, 1, 1, 0, 0, 1, 1. The leftmost bit (A's column) alternates every
four rows: 0, 0, 0, 0, 1, 1, 1, 1. This is just how binary counting
works — the least significant bit flips fastest, and each higher bit
flips at half the frequency. But look at what it means for subsets:
C appears in exactly half the subsets (the odd rows). B appears in
exactly half the subsets (rows 2-3 and 6-7). A appears in exactly
half the subsets (rows 4-7). Every element is in exactly 2^(n-1)
of the 2^n subsets. That is not a coincidence either — it is a
direct consequence of the binary structure.
And the subset column is just a decoding of which bits are set.
Row 5 has binary 101 — bit 2 is on (A), bit 1 is off, bit 0 is
on (C) — so the subset is {A, C}. That mapping is mechanical.
There is no interpretation. No ambiguity. Just a direct correspondence
between the binary representation of an integer and a subset of the
original set.
Think about what this means for the algorithm. If you iterate from 0 to
2^n - 1 and decode each integer as a subset, you have generated every
subset without a single recursive call. No call stack. No backtracking.
No path.push() / path.pop() dance. Just a for loop and some
bitwise arithmetic.
This insight has a name: bitmask enumeration. And it works because the binary number system is literally a system for encoding independent on/off decisions — which is exactly what subset membership is. There is no translation layer. The encoding is direct.
The recursive template builds subsets by making n sequential
include/exclude decisions via function calls. The bitmask approach builds
subsets by representing all n include/exclude decisions simultaneously
as the bits of a single integer. Same decisions. Different encoding. The
recursive approach unfolds them in time. The bitmask approach encodes
them in space.
Now that you understand WHY bitmask enumeration works — that counting in
binary IS subset generation — let us build the HOW. The algorithm has
two nested loops, and understanding each one precisely will save you from
the two most common off-by-one bugs in bitmask code.
The outer loop iterates mask from 0 to (1 << n) - 1. Each
value of mask represents one subset. The expression 1 << n is
2^n — the total number of subsets. We use 1 << n instead of
Math.pow(2, n) for three reasons: it is faster (a single CPU
instruction vs. a floating-point computation), it is more idiomatic in
bit-manipulation code (other developers reading your solution will
expect it), and — once you are used to it — it is more readable because
it makes the “powers of two” connection explicit. 1 << 3 literally
means “shift a 1-bit three positions left,” giving you binary 1000
which is 8 — the number of subsets for a 3-element set.
Watch the 1-bit shift leftward, position by position:
The outer loop condition is mask < (1 << n), not mask <= (1 << n).
This is because we want masks from 0 to 2^n - 1, which is exactly
2^n values. The mask 2^n itself (all bits beyond position n-1 set)
does not represent a valid subset of n elements.
The inner loop runs for each mask, iterating i from 0 to
n - 1. At each position i, we check whether bit i is set in
mask using the expression mask & (1 << i). Let me unpack this
operation step by step with a concrete example.
Say mask = 5, which is binary 101. And i = 1. Here is what
happens inside the expression mask & (1 << i):
1 << 1 shifts the number 1 one position to the left. In binary,
1 is 001, and shifting left by 1 gives 010. That is the
decimal number 2. This creates a “probe” — a number with exactly
one bit set at position i.
mask & (1 << i) performs bitwise AND between 101 and 010.
Bitwise AND compares each bit position independently: both bits
must be 1 for the result to be 1 at that position.
101 (mask = 5)& 010 (1 << 1 = 2)----- 000 (result = 0)The result is 0, which is falsy in JavaScript. Bit 1 is off —
element B (at index 1) is not in this subset.
Try it yourself. Pick any mask and any bit position — watch the AND operation unfold digit by digit:
Now try i = 0 with the same mask:
1 << 0 gives 001 (just 1).101 & 001 = 001 (result is 1, truthy).And i = 2:
1 << 2 gives 100 (which is 4).101 & 100 = 100 (result is 4, truthy).So mask 5 (101) gives us elements at positions 0 and 2: that is
{A, C}, which matches the table from the previous screen. The
bitwise AND acts as a single-bit spotlight. It isolates exactly one
bit position and tells you whether it is on or off. Every other bit in
the mask is zeroed out — only position i survives.
Two common mistakes trip people up. Both come from confusion about what
n means in the context of bit operations, and both will produce
subtly wrong output that might pass small test cases but fail on
anything non-trivial:
Off-by-one on the outer bound: writing mask < n instead of
mask < (1 << n). The variable n is the number of elements, but the
number of subsets is 2^n. When n = 3, you need mask to count
from 0 to 7 (eight values), not 0 to 2 (three values). This
mistake generates only n subsets instead of 2^n. Three subsets of
[1, 2, 3]? That is [1], [2], [1, 2] — clearly missing the
majority of subsets. The error is subtle because small inputs might
still produce some valid subsets, tricking you into thinking the code
mostly works.
See the difference side by side — three subsets on the left, eight on the right:
Wrong bit check: writing mask >> i instead of mask & (1 << i).
The expression mask >> i right-shifts the mask by i positions. It
does not test a single bit — it returns a multi-bit value. For example,
if mask = 5 (binary 101) and i = 0, then mask >> 0 = 5, which
is truthy. And mask >> 1 = 2 (binary 10), also truthy. And
mask >> 2 = 1, also truthy. So every position reads as “included,”
producing {A, B, C} for mask 5 instead of the correct {A, C}. The
correct test isolates exactly one bit: mask & (1 << i) returns either
0 (bit off) or a single non-zero power of two (bit on).
There is a technically correct variation: (mask >> i) & 1. This
right-shifts the mask so that bit i becomes bit 0, then ANDs with 1
to isolate that single bit. The result is always 0 or 1 (never a
larger power of two). Some programmers prefer this form because the
result is a clean boolean-like value. Both mask & (1 << i) and
(mask >> i) & 1 work. The mistake is using mask >> i without
the & 1 — that leaves all the higher bits in place.
Here is the failure in a truth table — notice how mask >> i is truthy
for ALL positions, even bit 1 which is actually off:
mask >> 1 = 2 (truthy!) but bit 1 is actually OFF. Without & 1, higher bits leak through.
There is a third mistake that is less common but worth mentioning:
starting the inner loop at 1 instead of 0. Bit positions are
zero-indexed. Position 0 is the rightmost (least significant) bit.
If you start at i = 1, you skip element 0 entirely — every subset
will be missing nums[0]. This is hard to catch because the subsets
still look valid; they are just all missing one element.
Below, fill in the three critical expressions that make the bitmask loop work. Each blank has four options — one correct, three plausible traps. If you get caught by a trap, read the feedback carefully — it explains the exact failure mode.
Look at what you just built. Six lines of code. No recursion. No path
array being pushed and popped. No base case check. No
choose-explore-unchoose dance. The outer loop counts subsets by
counting integers. The inner loop decodes each integer into a subset by
reading its bits. That is the entire algorithm.
Play with the mapping yourself. Toggle any bit and see the subset update in real time:
This sandbox makes the mapping visceral. Every bit position is one element. Flip bit 2 and A appears; flip it again and A disappears. The integer is just a compact encoding of which elements are “in.” There is no indirection, no array index math, no recursive call to trace. The mask is the subset, written in the language of binary.
Take a moment to appreciate what the two loops are doing together.
The outer loop is the generator — it produces every mask from 0
to 2^n - 1, which means it visits every possible subset exactly
once, in order. The inner loop is the decoder — for each mask,
it reads each bit position and translates “bit on” into “include
this element.” Generator and decoder. Count and translate. That is
the entire architecture of bitmask enumeration, and once you see it,
you will never forget it.
There is an elegance to this separation that the recursive version
does not have. In the recursive version, generation and decoding are
interleaved — you build the subset one element at a time through
push/pop operations that happen at different levels of the call
tree. The subset is never fully specified until you reach a leaf
node. In the bitmask version, the subset is fully specified the
moment you have the mask. The inner loop is just reading it out.
This means each iteration of the outer loop is completely
independent — you could process masks 0 through 7 in any order, or
even in parallel, and still get the correct answer. The recursive
version does not have this property. Its correctness depends on the
depth-first traversal order and the mutation of the shared path
array.
The structural difference is visible at a glance:
Same output. Different shape. The tree backtracks; the loop just counts.
Now compare the bitmask template to the recursive version you built in Screen 1:
function subsets(nums, idx = 0, path = [], result = []) { result.push([...path]) for (let i = idx; i < nums.length; i++) { path.push(nums[i]) subsets(nums, i + 1, path, result) path.pop() } return result}Both produce the same 2^n subsets. Both run in O(n * 2^n) time —
you have to build each subset, and each subset can be up to size n, so
each one costs up to O(n) to construct. But they have fundamentally
different shapes.
The recursive version is a tree traversal. It explores a binary
decision tree of include/exclude choices, backtracking at each leaf. The
tree has 2^n leaves and 2^(n+1) - 1 total nodes. The traversal order
is depth-first, and the path array mutates as you go — push on the way
down, pop on the way up.
The iterative version is a flat count. It walks a number line from
0 to 2^n - 1 and decodes each number into a subset. There is no tree.
There is no stack. Each iteration is independent — you build a fresh
subset array from scratch, which means you never need to clone or
backtrack.
The recursive version is more powerful. It can prune mid-tree — useful
for combinations, where you stop exploring when the path is already too
long. It can handle duplicates by sorting and skipping (the
i > idx && nums[i] === nums[i-1] pattern from Screen 2). And it can
pivot between subsets, combinations, and permutations by changing one
expression — the loop knob you learned in Screen 1.
The iterative version is more concise. Six lines, no recursion to
explain, no call stack to reason about. No mutable path variable being
shared across recursive calls. In an interview where the question is
simply “generate all subsets” — no pruning, no dedup, no variant
switching — it is the faster solution to write and the easier solution to
verify.
When should you reach for bitmask enumeration in an interview? The rule
is simple: if the problem is pure subset enumeration with n <= 20
and no constraints that require pruning, use the bitmask loop. It is
six lines, it is flat, and it avoids the entire category of recursion
bugs — forgotten base cases, off-by-one idx values, missing
path.pop() calls. The ceiling of n <= 20 comes from the fact that
2^20 = 1,048,576, which is already a million subsets. Beyond n = 20,
the output itself is too large for any interview question to expect.
For anything more complex — combinations, permutations, dedup, pruning with constraints — reach for the recursive template. It is the Swiss Army knife. The bitmask loop is the pocket knife: perfect for one specific job.
Both belong in your toolkit. The question is: when do you reach for which one? We will answer that precisely in the final screen. But first, there is a bigger problem to solve.
You now have two ways to generate subsets. You also know — from Screens 1 and 2 — how to generate combinations and permutations, how to handle duplicates, and how to prune with constraints. That is a lot of tools. Six variants, if you count the dedup versions separately. Six templates in your head.
The failure mode at this point is not lack of knowledge. It is retrieval. You sit down in an interview, read a problem, and think: “This looks like... subsets? No, wait, combinations? Or is it permutations with dedup?” You know all the templates. You just cannot quickly match the right one to the problem in front of you. It is like standing at a fork in the road with no signs.
This is a real problem, not a theoretical one. Consider these four problems and notice how easy it is to confuse them:
Tap each card below to reveal which algorithm it maps to. Notice how similar the wording is — and how different the algorithms are:
Four problems, same structure, different algorithms. The triage compass tells them apart.
All four use the same recursive structure. All four are backtracking
problems. The only difference is one expression in the for-loop and
an optional sort-and-skip condition. But that one expression changes
everything about the output. Using the wrong variant does not throw an
error — it produces wrong results that look plausible. You get subsets
when you wanted combinations. You get duplicates when you wanted unique
results. And you do not notice until you trace through the output by
hand, which costs precious interview minutes.
You do not need to memorize a decision table. You do not need to
pattern-match problem wording to algorithm names. You need a compass —
and the compass is built from questions, not answers.
Below, route real problems through a decision tree. The problems come
from real-world scenarios — burritos, card games, tournament brackets,
paint palettes. You will not see the algorithm names until your routing
decisions produce them. The questions will teach themselves.
A food delivery app lets you customize a burrito by picking any combination of toppings. Salsa, guac, cheese, sour cream, jalapenos — any subset, including “plain” (no toppings). How many customization options are there for 5 toppings?
easyDoes the ORDER of elements matter?
Notice what just happened. You did not pick from a list of six algorithms. You derived the right algorithm by answering structured questions about the problem's constraints. The questions are the same every single time:
This is a repeatable, mechanical process. Not pattern matching. Not intuition. Not “this problem feels like combinations.” It is three binary decisions that deterministically produce the right answer.
Here is the compass you just built:
Hover a leaf to trace its path. Three questions, six algorithms.
Let me walk through why each question is the right question, in the right order.
"Does order matter?" is the first question because it is the biggest
fork. Permutations and subsets/combinations have fundamentally
different tree structures. In a permutation tree, every element can
appear at every level — you use a used[] array to track what has been
placed so far. In a subset/combination tree, elements only appear at
their level or deeper — you use an idx or start parameter to prevent
looking backwards. This is not a small tweak. It changes the shape of
the recursion tree entirely. Getting this question wrong means you are
exploring the wrong tree, and no amount of fixing downstream will save
you.
The heuristic for this question: if swapping two elements in the
output changes the meaning, order matters. Seating charts, passwords,
tournament brackets — order. Playlists, ingredient selections,
committee membership — no order. When you are unsure, ask yourself:
"Is [A, B] a different answer from [B, A]?" If yes, permutations.
If no, subsets or combinations.
"Is there a fixed size constraint?" is the second question because
it separates two siblings. Both subsets and combinations are unordered
collections. The only difference: combinations have a fixed size k,
subsets do not. This matters because the combination template prunes
aggressively at path.length === k — it stops exploring deeper once
the combination is full. Using the subset template for a combinations
problem means you generate all 2^n subsets and then filter by length,
which is wasteful. Using the combination template for a subset problem
means you terminate too early and miss shorter subsets.
Watch for “at most k” or “at least k” — those are subsets with a
post-filter, not combinations. The combination template is for
“exactly k” only. If you see “choose up to 3 toppings,” that is
subsets filtered by path.length <= 3, not C(n, 3).
"Are there duplicates in the input?" is the final modifier. It does
not change the tree structure — it adds a condition to skip redundant
branches. The pattern is the same regardless of variant: sort the input
first, then add if (i > threshold && nums[i] === nums[i-1]) continue.
The threshold varies: idx for subsets, start for combinations,
0 for permutations (with the additional !used[i-1] check). This is
the cheapest question — if the input has no duplicates, you skip it
entirely.
In an interview, narrate the triage out loud: "Does order matter here?
The problem says any selection of toppings works, and salsa-then-guac
equals guac-then-salsa, so no. Size constraint? No, any number of
toppings including none. So this is subsets. Duplicates? The toppings
are all distinct, so I do not need the skip condition. Standard subset
template with i = idx."
"Does order matter here?"
Any selection of toppings works, salsa-then-guac equals guac-then-salsa.
So no — this is not permutations.
"Size constraint?"
Any number of toppings including none.
So no — this is subsets, not combinations.
"Duplicates?"
All toppings are distinct. Standard subset template.
That kind of structured reasoning is precisely what interviewers want to hear. It shows that you are not guessing. It shows that you have a framework. And when the problem is ambiguous, the triage tells you exactly which clarification question to ask: “Does the order of assignment matter, or only which workers are assigned to which tasks?”
Here are some real interview problems and how the triage routes them. Practice running the questions in your head:
LC 78 — Subsets: “Given an integer array of unique elements, return
all possible subsets.” Order matters? No (subsets are sets). Fixed
size? No (any size including empty). Duplicates? No (unique elements).
Route: Subsets, i = idx. Or bitmask — your choice.
LC 39 — Combination Sum: “Find all unique combinations in
candidates where the chosen numbers sum to target. The same number
may be chosen unlimited times.” Order matters? No. Fixed size? No
(but there is a sum constraint for pruning). Duplicates? Depends on
variant. Route: Subsets with pruning (sum exceeds target -> prune).
Recursive only — bitmask cannot prune.
LC 46 — Permutations: “Given an array of distinct integers, return
all possible permutations.” Order matters? Yes ([1,2,3] differs
from [3,2,1]). All elements used? Yes. Duplicates? No. Route:
Permutations, i = 0 with used[] array.
LC 90 — Subsets II: “Given an integer array that may contain
duplicates, return all possible unique subsets.” Order matters? No.
Fixed size? No. Duplicates? Yes. Route: Subsets + Dedup, sort
first, if (i > idx && nums[i] === nums[i-1]) continue.
In every case, three questions. In every case, the right template falls out mechanically. The triage is not a shortcut — it is a guarantee that you will not pick the wrong variant.
One more thing. The triage also tells you which approach to use — recursive or bitmask. If the triage leads to “subsets” (no dedup, no fixed size), and the problem has no additional constraints that require pruning, you have a choice: the recursive template or the bitmask loop. If the triage leads to anything else — combinations, permutations, or any dedup variant — the recursive template is the only option. The bitmask loop does not generalize beyond pure subset enumeration.
Let us bring everything together. You now have two fundamentally different approaches to subset generation, and a triage system for routing any enumeration problem to the right algorithm. Before we close this module, I want to establish the practical tradeoffs with precision — not just “both are O(2^n)” but the specific, concrete situations where each approach shines.
The tempting conclusion is “the recursive version is always better because it does more.” That conclusion is wrong. The bitmask approach has real advantages that matter in specific interview and production contexts.
Interview speed and simplicity: In a pure “generate all subsets”
question — no pruning, no dedup, no variant switching — the bitmask
loop is genuinely faster to write. Six lines, two simple for-loops, no
recursion to trace. You write it, you explain it in one sentence
("count from 0 to 2^n - 1, decode each integer as a subset by checking
its bits"), and you move on to the follow-up question. The interviewer
reads your code in a single pass, top to bottom. There is no “which
call returns first?” question to answer, no “what does the path look
like at this point in the recursion?” to trace.
Compare that to explaining the recursive version: "We explore a binary
decision tree where each level represents one element. At each node, we
snapshot the current path. The for-loop iterates over remaining
candidates, pushing each one, recursing deeper, then popping to
backtrack. The idx parameter prevents us from considering earlier
elements, which avoids duplicate subsets." That is a paragraph. The
bitmask explanation is one sentence.
Stack safety: For n up to about 20, the recursive call stack is
no concern. But n = 20 already gives you 2^20 = 1,048,576 subsets.
For n = 25, you have 33 million subsets and a recursion depth of 25.
In languages with limited default stack sizes (Python defaults to
sys.getrecursionlimit() = 1000, though that is plenty for 25), or
in environments where deep recursion triggers slow garbage collection,
the flat bitmask loop has zero overhead. No frames to allocate, no
frames to unwind.
Mental model clarity: “Count from 0 to 2^n - 1 and decode each
number” is a complete, self-contained mental model. You do not need to
understand recursion, backtracking, or tree traversal to implement or
reason about the bitmask approach. This matters when debugging: if your
bitmask code produces wrong output, you can check a specific mask value,
convert it to binary, and verify the subset by hand. The debugging is
arithmetic, not tree tracing.
But the recursive version wins decisively when the problem is anything beyond pure subset enumeration:
Pruning: Combinations need path.length === k as a termination
condition. Constrained subsets (like "subset sum < target") can prune
entire subtrees when the running sum exceeds the threshold. The
recursive template naturally supports early termination — you simply
return before the for-loop. The bitmask approach generates ALL 2^n
masks and must filter afterward. You cannot skip subtrees because there
are no subtrees — just a flat sequence of integers.
Duplicates: The recursive sort + skip pattern
(if (i > idx && nums[i] === nums[i-1]) continue) is clean, efficient,
and well-understood. The bitmask equivalent? Generate all 2^n masks,
build every subset, convert each to a canonical form (like a sorted
comma-separated string), and deduplicate with a hash set. That is O(n * 2^n) extra work and O(2^n) extra memory just for deduplication.
Variant switching: This is the biggest win for the recursive
template. The loop knob from Screen 1: change idx to 0 and add a
used-set and you get permutations. Change idx to start and add a
depth check and you get combinations. The bitmask loop does not
generalize to permutations at all — you cannot encode “order matters”
in a bitmask where each bit is just on/off.
Extensions: Many real interview problems build on the basic subset template. “Generate all valid parentheses combinations” is a modified subset enumeration. “Partition a set into two subsets with equal sum” uses subset enumeration with pruning. The recursive template adapts to these extensions naturally. The bitmask loop is a dead end once the problem moves beyond pure enumeration.
To give you a visceral sense of why the recursive template matters for constrained problems, consider the growth rates:
Toggle to n = 6 first. The permutation bar (720) is already more
than 10x the subset bar (64). Now toggle to n = 8. The chart
switches to log scale because the numbers are so different: 40,320
permutations vs. 256 subsets — a ratio of 157:1. And at n = 10
(imagine it), that ratio becomes 3,628,800 to 1,024 — over
3,500:1. The recursive template can prune the permutation tree early
when constraints are violated — sometimes cutting 90% of the search
space. The bitmask approach has no tree to prune.
This chart also reveals why subset problems are the “gentlest”
introduction to backtracking. Even at n = 20, you have about a
million subsets — large but tractable. Permutations of 20 elements?
That is 20! = 2.4 * 10^18. No computer will enumerate all of them.
Permutation problems require pruning. Subset problems merely
benefit from it.
Below, predict which approach wins on three specific practical dimensions. Your predictions will unlock a full comparison table that maps every tradeoff.
Both approaches generate the same 2^n subsets. Which one uses LESS memory beyond the output array?
The synthesis is this: the recursive template is your primary tool. It handles subsets, combinations, and permutations through one expression. It prunes. It deduplicates cleanly. It extends to constrained variants. It is the Swiss Army knife of enumeration algorithms.
The bitmask loop is your pocket knife. Smaller, faster for one specific
job, easier to explain in thirty seconds. When the question is purely
“generate all subsets” and nothing more — no constraints, no dedup, no
follow-up — reach for the bitmask. It is the concisest, flattest, most
debuggable solution for that specific task.
And the triage? That is your compass. Three questions that route any enumeration problem to the right tool in the right configuration. Does order matter? Is there a fixed size? Are there duplicates? You will never stare at a problem wondering “which template is this?” again.
When to use recursive vs. iterative — the decision checklist:
n <= 20 and no pruning is needed.Try it yourself — toggle the constraints and see which approach the checklist recommends:
Remember Screen 1: one template, one expression, three algorithms. Remember Screen 2: the fine adjustments — pruning, dedup, swaps. Now add Screen 3: a second approach entirely (bitmask enumeration) and a compass that tells you which tool to pick (the triage machine).
This is the end of the subsets module. Let me trace the arc of what you built across all three screens, because the whole is more than the sum of its parts.
Screen 1 gave you the recursive template — a seven-line function
where a single expression (the loop initialization) determines
whether you generate subsets, combinations, or permutations. That
was the “radio dial” metaphor: one knob, three stations. You learned
that backtracking is not three separate algorithms — it is one
algorithm with a tunable parameter. The insight was structural: the
decision tree is the same tree regardless of which variant you are
generating. Only the branching rule changes.
Screen 2 taught you the fine adjustments. Pruning: stop exploring
when a constraint is violated, saving exponential work. Dedup: sort
the input and skip elements that would produce duplicate branches.
Swap-based permutations: instead of a used[] set, partition the
array into “placed” and “unplaced” regions by swapping. These are
not new algorithms — they are modifications to the template from
Screen 1. A sort here, a condition there, a swap instead of
push/pop. The template stayed the same. The tuning got finer.
Screen 3 — this screen — added two tools that complement the
recursive template. The bitmask loop: a flat, non-recursive way to
generate all subsets by counting in binary. Six lines, no call
stack, no mutable state. And the triage compass: three questions
that route any enumeration problem to the right variant. Does order
matter? Is there a fixed size? Are there duplicates? Three binary
decisions that deterministically select the algorithm.
Together, these three screens give you a complete enumeration toolkit. The recursive template handles the general case — any variant, any constraint, any pruning strategy. The bitmask loop handles the special case — pure subset enumeration, fast and flat. And the compass tells you which tool to reach for before you write a single line of code.
The dial. The counter. The compass. Three tools. One family of problems.
Every enumeration question you will see in an interview routes through this framework. You will never stare at a problem wondering “which template is this?” again — because you will not be matching templates. You will be asking questions. And the answers will lead you to the right code, every time.
0 through 7 map perfectly to all 8 subsets of a 3-element set. Binary counting and subset generation are the same operation.
The three triage questions — order, size, duplicates — are your compass. Some routes tripped you up, but the decision tree is a tool you can internalize with practice.
Remember Screen 1's radio dial? One expression, three algorithms. Now you have two radios: the recursive template with its tunable loop knob, and the bitmask loop that counts its way to every subset. Different tools for different moments.Recursive
Iterative