Subsets Using Bits
Generating all possible subsets without using recursion or backtracking!
Introduction
In the Recursion module, we learned how to generate all subsets of an array using Backtracking.
However, there is an incredibly elegant, non-recursive way to generate subsets using Bit Masks. This method is often cleaner to write and avoids recursion limit overhead.
The Concept
An array of length \(N\) has exactly \(2^N\) possible subsets.
If \(N = 3\), there are \(2^3 = 8\) subsets. Let's count from 0 to 7 in binary:
0: 000 (Empty Subset)
1: 001 (Include only nums[0])
2: 010 (Include only nums[1])
3: 011 (Include nums[1], nums[0])
4: 100 (Include only nums[2])
5: 101 (Include nums[2], nums[0])
6: 110 (Include nums[2], nums[1])
7: 111 (Include nums[2], nums[1], nums[0])
Notice a pattern? The binary representation of every number from \(0\) to \(2^N - 1\) PERFECTLY maps to exactly one subset combination!
A 1 means "include this element", and a 0 means "exclude this element".
Implementation
We can loop from \(0\) to \(2^N - 1\). For each number (mask), we check which bits are set to 1 and include the corresponding elements from the array.
def subsets(nums: list[int]) -> list[list[int]]:
n = len(nums)
res = []
# 1 << n is exactly 2^n
for mask in range(1 << n):
current_subset = []
# Check every bit position from 0 to n-1
for i in range(n):
# If the i-th bit is set to 1, include nums[i]
if mask & (1 << i):
current_subset.append(nums[i])
res.append(current_subset)
return res
| Time Complexity | Space Complexity |
|---|---|
| \(O(N \cdot 2^N)\) | \(O(N \cdot 2^N)\) |
The time complexity is identical to Backtracking (\(2^N\) subsets, and copying a subset takes \(O(N)\)).
Why use this over Backtracking?
- Pros: It's entirely iterative, meaning no Call Stack overhead and no risk of hitting a Recursion Limit. It's also slightly easier to reason about if you are comfortable with bitmasks.
- Cons: It only works for up to \(\approx 31\) elements (though in Python, integers have infinite precision, so you could technically do more, but a \(2^{31}\) loop would take minutes to run anyway). Backtracking is often easier to adapt to complex rules (like filtering out duplicates).
Key Takeaways
- The numbers \(0\) to \(2^N - 1\) in binary directly represent all possible subsets of an array of size \(N\).
- Use
1 << Nto calculate \(2^N\). - Use
mask & (1 << i)to check if the \(i\)-th element should be included.