The Problem With Recursion

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:

Frames: 0/4

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:

A
B
C
{ }

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.

Flip the bits to build each subset. No recursion. No backtracking. Just switches.
1 / 5

Build: {B, C}

Turn ON the bits for elements B and C

switches
=0
Subset:{}

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:

mask = 0 of 7
0
A
0
B
0
C
{}

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:

DecimalBinarySubset
0000{ }
1001{ C }
2010{ B }
3011{ B, C }
4100{ A }
5101{ A, C }
6110{ A, B }
7111{ 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.

Building the Bitmask Loop

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 << 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.

  2. 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.

    1
      101   (mask = 5)
    2
    & 010   (1 << 1 = 2)
    3
    -----
    4
      000   (result = 0)
  3. 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:

mask (0-7):
bit position i:
mask=5
101
1<<2=4
100
&
= 4
100
Bit 2 is ON — A is included

Now try i = 0 with the same mask:

  1. 1 << 0 gives 001 (just 1).
  2. 101 & 001 = 001 (result is 1, truthy).
  3. Bit 0 is on — element C is included.

And i = 2:

  1. 1 << 2 gives 100 (which is 4).
  2. 101 & 100 = 100 (result is 4, truthy).
  3. Bit 2 is on — element A is included.

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:

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.

Now build it. Fill in the three expressions that make the bitmask loop work.
function subsets(nums) { const result = [] for (let mask = 0; mask < ; mask++) { const subset = [] for (let i = 0; i < ; i++) { if () subset.push(nums[i]) } result.push(subset) } return result }

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:

A
B
C
Subset:{ }
Decimal:0(0002)

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:

Now compare the bitmask template to the recursive version you built in Screen 1:

1
function subsets(nums, idx = 0, path = [], result = []) {
2
  result.push([...path])
3
  for (let i = idx; i < nums.length; i++) {
4
    path.push(nums[i])
5
    subsets(nums, i + 1, path, result)
6
    path.pop()
7
  }
8
  return result
9
}

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.

The Three-Question Compass

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:

  • "Given a list of distinct numbers, return all possible subsets."
  • "Given a list of distinct numbers, return all combinations of size k."
  • "Given a list of distinct numbers, return all permutations."
  • "Given a list of numbers (with duplicates), return all unique subsets."

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.

Route each problem through the decision tree. Answer the questions to find the right algorithm.
1 / 5

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?

easy

Does 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:

  1. Does order matter? (yes -> permutations family, no -> next question)
  2. Fixed size? (yes -> combinations, no -> subsets)
  3. Duplicates in input? (yes -> add sort + skip condition)

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:

YesNoNoYesYesNoNoYesOrder matters?Duplicates?Permutationsi=0 + used[]Perms + Dedupsort + skipFixed size k?Combinationsi=start, prune at kDuplicates?Subsetsi=idxSubsets + Dedupsort + skip

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.

A
B
B
A
Tap to switch context

"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."

Interview narration

"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 78Subsets
Order?No
Size k?No
Dupes?No
Subsets

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 39Combination Sum
Order?No
Size k?No
Subsets + Pruning

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 46Permutations
Order?Yes
Dupes?No
Permutations

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.

LC 90Subsets II
Order?No
Size k?No
Dupes?Yes
Subsets + Dedup

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.

Two Tools, One Toolkit

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 produce the same subsets. Predict where each one wins.
1 / 3

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:

  • The problem asks for subsets only, no combinations or permutations? Use bitmask if n <= 20 and no pruning is needed.
  • The problem has constraints that allow early termination (sum limits, size limits, validity checks)? Use recursive — you need the tree to prune.
  • The problem involves duplicates? Use recursive — the sort-and-skip pattern is clean and efficient.
  • The problem might have a follow-up that changes the variant (e.g., "now make it combinations of size 3")? Start with recursive — you will not have to rewrite.
  • You are in a timed interview and the problem is purely "list all subsets"? Bitmask — write six lines and move on.

Try it yourself — toggle the constraints and see which approach the checklist recommends:

Toggle constraints to see

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.

Module Complete

Bitmask Discovery0/3
Code Construction0/3 blanks
Algorithm Triageno wrong routes0/5 problems
Approach Comparison0/3 predictions
You built subsets by flipping bits and discovered that integers 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.
Recursivei = idx, i = start, i = 0Need pruning, dedup, or variant switching
Iterativemask = 0..2^n - 1Pure subset generation, conciseness matters
DecisionRoute to the right template3 questions: order? size? duplicates?