78. Subsets - Explanation
Description
Given an array nums of unique integers, return all possible subsets of nums.
The solution set must not contain duplicate subsets. You may return the solution in any order.
Example 1:
Input: nums = [1,2,3]
Output: [[],[1],[2],[1,2],[3],[1,3],[2,3],[1,2,3]]Example 2:
Input: nums = [7]
Output: [[],[7]]Constraints:
1 <= nums.length <= 10-10 <= nums[i] <= 10
Topics
Recommended Time & Space Complexity
You should aim for a solution with O(n * (2^n)) time and O(n) space, where n is the size of the input array.
Hint 1
It is straightforward that if the array is empty we return an empty array. When we have an array [1] which is of size 1, we have two subsets, [[], [1]] as the output. Can you think why the output is so?
Hint 2
We can see that one subset includes a number, and another does not. From this, we can conclude that we need to find the subsets that include a number and those that do not. This results in 2^n subsets for an array of size n because there are many combinations for including and excluding the array of numbers. Since the elements are unique, duplicate subsets will not be formed if we ensure that we don't pick the element more than once in the current subset. Which algorithm is helpful to generate all subsets, and how would you implement it?
Hint 3
We can use backtracking to generate all possible subsets. We iterate through the given array with an index i and an initially empty temporary list representing the current subset. We recursively process each index, adding the corresponding element to the current subset and continuing, which results in a subset that includes that element. Alternatively, we skip the element by not adding it to the subset and proceed to the next index, forming a subset without including that element. What can be the base condition to end this recursion?
Hint 4
When the index i reaches the end of the array, we append a copy of the subset formed in that particular recursive path to the result list and return. All subsets of the given array are generated from these different recursive paths, which represent various combinations of "include" and "not include" steps for the elements of the array. As we are only iterating from left to right in the array, we don't pick an element more than once.
Prerequisites
Before attempting this problem, you should be comfortable with:
- Recursion - Used to explore the decision tree where each element is either included or excluded
- Backtracking - Needed to undo choices (remove elements) after exploring one branch of the decision tree
- Bit Manipulation - Alternative approach uses bitmasks to represent subset membership
1. Backtracking
Intuition
The idea is to build all possible subsets by making a choice at each step:
for every number, we have two options — include it or exclude it.
This naturally forms a decision tree.
Backtracking helps us explore both choices:
- Add the current number → explore further
- Remove it (undo) → explore without it
Whenever we reach the end of the array, the current list represents one
complete subset, so we store it.
This systematically generates all 2ⁿ subsets.
Algorithm
- Maintain:
res→ final list of all subsetssubset→ current subset being built
- Define a recursive function
dfs(i):- If
iequals the length of the input:- Add a copy of
subsettores - Return
- Add a copy of
- Choice 1: include
nums[i]- Append number to
subset - Recurse to next index
- Remove the number (backtrack)
- Append number to
- Choice 2: skip
nums[i]- Recurse to next index
- If
- Start recursion with
dfs(0) - Return
res
Time & Space Complexity
- Time complexity:
- Space complexity:
- extra space.
- for the output list.
2. Iteration
Intuition
Start with just one subset: the empty set [].
For every number in the array, we take all the subsets we have so far and
create new subsets by adding the current number to each of them.
Example:
- Start:
[[]] - Add
1→[[], [1]] - Add
2→[[], [1], [2], [1,2]] - Add
3→[[], [1], [2], [1,2], [3], [1,3], [2,3], [1,2,3]]
Each step doubles the number of subsets.
Algorithm
- Initialize
res = [[]](start with empty subset). - For each number
numin the input array:- For every subset already in
res:- Create a new subset that includes
num
- Create a new subset that includes
- Append all these newly created subsets to
res.
- For every subset already in
- Return
resafter processing all numbers.
Time & Space Complexity
- Time complexity:
- Space complexity:
- extra space.
- for the output list.
3. Bit Manipulation
Intuition
Every subset can be represented using bits.
For an array of length n, there are 2^n possible subsets.
Each subset corresponds to a number from 0 to 2^n - 1.
Example for nums = [a, b, c]:
000→ choose nothing →[]001→ choosec010→ chooseb011→ chooseb, c100→ choosea- ...and so on.
Each bit tells us whether to include the corresponding element.
So for every integer i from 0 to (1 << n) - 1:
- Check each bit
jofi - If bit
jis1, includenums[j]in the current subset.
Algorithm
- Let
nbe the length ofnums. - Loop
ifrom0to(1 << n) - 1(this generates all bitmasks). - For each
i, build a subset:- For each position
jfrom0ton - 1:- If the
j-th bit ofiis set, includenums[j]in the subset.
- If the
- For each position
- Add the subset to the result list.
- Return all subsets.
Time & Space Complexity
- Time complexity:
- Space complexity:
- extra space.
- for the output list.
Common Pitfalls
Modifying the Subset After Adding to Result
When adding a subset to the result list, you must add a copy of the current subset. Otherwise, backtracking modifications will alter subsets already in the result.
# Wrong: adds reference that gets modified later
res.append(subset)
# Correct: add a copy
res.append(subset[:]) # or list(subset)Forgetting the Empty Subset
The power set always includes the empty subset []. Forgetting to initialize with an empty subset or starting the iteration incorrectly will miss this case.
Incorrect Index Handling in Backtracking
When recursing, start from i + 1 to avoid reusing the same element and to prevent duplicate subsets. Starting from 0 or i generates incorrect results.
# Wrong: includes current element again
for j in range(i, len(nums)):
# Correct: start from next element
for j in range(i + 1, len(nums)):Integer Overflow in Bitmask Approach
For the bitmask solution, 1 << n can overflow for large n. In most languages, this limits the approach to around 30 elements, though problem constraints usually keep n small.
Sign in to join the discussion