Subsets
Generate every subset of a list, and meet the decision tree that all backtracking problems share
Problem Statement
Given an array of distinct integers, return all possible subsets (the power set). The answer may be in any order, and must not contain duplicates.
Input: nums = [1, 2, 3]
Output: [[], [1], [2], [3], [1,2], [1,3], [2,3], [1,2,3]]
Input: nums = [0]
Output: [[], [0]]Intuition
There are 2³ = 8 subsets of a 3-element list, and that 2ⁿ is the clue.
Why exactly 2ⁿ? Because building a subset means walking the list once and making one yes/no decision per element: is this element in, or out? Three elements, two options each, 2 × 2 × 2 = 8.
The key insight
A subset is not something you search for. It is the record of a series of decisions — one per element.
So instead of hunting for subsets, walk through the elements making decisions, and every complete run of decisions is a subset.
The decision tree
[]
/ \
take 1 skip 1
/ \ / \
take 2 skip 2 take 2 skip 2
/ \ / \ / \ / \
t3 s3 t3 s3 t3 s3 t3 s3
| | | | | | | |
[1,2,3][1,2][1,3][1] [2,3][2] [3] []Eight leaves, eight subsets. Backtracking walks this tree depth-first.
Why every node is an answer, not just the leaves
In many backtracking problems only complete paths count. Here, every node on the way down is already a valid subset.
[1] is a subset. So is [1, 2]. So is the empty path at the root.
That is why the recording happens at the top of the function rather than inside an "is it complete?" check — a detail that makes this the simplest backtracking problem there is.
The start index, and why it exists
Without care you would produce [1, 2] and [2, 1] — the same subset twice, since order does not matter in a set.
The fix is a start index: when choosing the next element, only look at positions after the one just taken. That forces every subset into increasing order, so each one can be built exactly one way.
With start: [1,2] produced once
Without start: [1,2] and [2,1] — duplicatesApproach
Record the current path immediately
Every path is a valid subset, including the empty one. Record a copy — not the array itself.
Loop over the remaining candidates
Start at start, not at 0. Everything before start was already decided by an ancestor call.
Choose, explore, undo
Append nums[i], recurse with start = i + 1, then pop.
The i + 1 is what prevents reusing an element and what keeps the ordering fixed.
Watch it run
Solution
def subsets(nums: list[int]) -> list[list[int]]:
"""Return every subset of a list of distinct integers."""
results: list[list[int]] = []
path: list[int] = []
def backtrack(start: int) -> None:
# Every path is a valid subset — record a COPY, not the live array.
results.append(path[:])
for i in range(start, len(nums)):
path.append(nums[i]) # choose
backtrack(i + 1) # explore, never looking back
path.pop() # undo
backtrack(0)
return results
def subsets_bitmask(nums: list[int]) -> list[list[int]]:
"""Alternative: the bits of 0..2^n - 1 spell out the include/exclude choices."""
n = len(nums)
results: list[list[int]] = []
for mask in range(1 << n): # 1 << n is 2^n
# Bit i set means "include nums[i]".
results.append([nums[i] for i in range(n) if mask & (1 << i)])
return resultsThe bitmask version is worth knowing
Count from 0 to 2ⁿ - 1 and read each number's bits as the include/exclude decisions. For n = 3, the number 5 is binary 101, meaning "take index 0, skip index 1, take index 2" — the subset [1, 3].
No recursion and no undo. It only works when n ≤ 32 or so, and it is harder to adapt when a problem adds constraints — but for plain subsets it is short and fast.
Complexity Analysis
Time Complexity
O(n · 2ⁿ)
Space Complexity
O(n)
- Time — there are
2ⁿsubsets, and copying each one into the results costs up toO(n). The recursion itself doesO(2ⁿ)work; the copying is what adds the factor ofn. - Space — excluding the output, only the
patharray and the recursion stack, both at mostndeep. Including the output, it isO(n · 2ⁿ)because that is simply how big the answer is.
Why n is always small here
At n = 20 there are about a million subsets. At n = 30, a billion. Constraints for this problem are typically n ≤ 10, which is the author signalling that exponential is the intended answer.
Edge Cases
Related Problems
- Permutations — order matters, so every element stays available
- Combination Sum — the same start index, plus reuse and pruning
- Backtracking — the choose / explore / undo template