Master subset generation with backtracking — generating all 2^n subsets, subsets with duplicates, combinations of size k, combination sum, partitions, power set using bitmask approach, and understanding which problems require subsets versus combinations versus permutations.
What Is a Subset?
A subset of a set S is any collection of elements from S (including the empty set and S itself). For a set of n elements, there are exactly 2^n subsets.
S = {1, 2, 3}
All 2³ = 8 subsets:
{}
{1}
{2}
{3}
{1, 2}
{1, 3}
{2, 3}
{1, 2, 3}
Approach 2 — Bitmask Method
Use an integer from 0 to 2^n - 1. Each bit position represents whether the corresponding element is included.
def all_subsets_bitmask(nums):
"""
Generate all subsets using bitmask.
Bit i of mask = 1 means nums[i] is included.
Time: O(2^n × n), Space: O(1) extra
"""
n = len(nums)
result = []
for mask in range(1 << n): # 0 to 2^n - 1
subset = []
for i in range(n):
if mask & (1 << i): # If bit i is set
subset.append(nums[i])
result.append(subset)
return result
print(all_subsets_bitmask([1, 2, 3]))
# [[], [1], [2], [1,2], [3], [1,3], [2,3], [1,2,3]]
| Bitmask | Subset: |
| 000 (0) | [] |
| 001 (1) | [1] (bit 0 set) |
| 010 (2) | [2] (bit 1 set) |
| 011 (3) | [1,2] (bits 0,1 set) |
| 100 (4) | [3] (bit 2 set) |
| 101 (5) | [1,3] (bits 0,2 set) |
| 110 (6) | [2,3] (bits 1,2 set) |
| 111 (7) | [1,2,3] (all bits set) |
Subsets with Duplicates
If the input has duplicates, we need to avoid generating duplicate subsets:
def subsets_with_dups(nums):
"""
Generate unique subsets when input contains duplicates.
Key: Sort first, then skip duplicate elements at same recursion level.
"""
nums.sort() # MUST sort first
result = []
def backtrack(start, current):
result.append(list(current))
for i in range(start, len(nums)):
# Skip duplicates at the same recursion level
if i > start and nums[i] == nums[i-1]:
continue # ← Skip duplicate
current.append(nums[i])
backtrack(i + 1, current)
current.pop()
backtrack(0, [])
return result
print(subsets_with_dups([1, 2, 2]))
# [[], [1], [1,2], [1,2,2], [2], [2,2]]
# Without dedup: [[], [1], [1,2], [1,2,2], [1,2], [2], [2,2], [2]] ← duplicates!
Combinations of Size K
Generate all subsets of exactly size k:
def combinations(nums, k):
"""
Generate all subsets of exactly size k (C(n,k) combinations).
Time: O(C(n,k) × k), Space: O(k)
"""
result = []
def backtrack(start, current):
if len(current) == k:
result.append(list(current))
return
# PRUNING: Not enough elements left to reach size k
remaining_needed = k - len(current)
remaining_available = len(nums) - start
if remaining_available < remaining_needed:
return # Can't possibly reach size k — prune
for i in range(start, len(nums)):
current.append(nums[i])
backtrack(i + 1, current)
current.pop()
backtrack(0, [])
return result
print(combinations([1, 2, 3, 4], 2))
# [[1,2],[1,3],[1,4],[2,3],[2,4],[3,4]]
print(len(combinations(list(range(1, 11)), 5))) # 252 = C(10,5)
Combination Sum (Can Reuse Elements)
def combination_sum(candidates, target):
"""
Find all combinations that sum to target.
Each number can be used multiple times.
"""
candidates.sort()
result = []
def backtrack(start, current, remaining):
if remaining == 0:
result.append(list(current))
return
for i in range(start, len(candidates)):
if candidates[i] > remaining:
break # Sorted: all further candidates are larger too
current.append(candidates[i])
backtrack(i, current, remaining - candidates[i]) # i (not i+1) = reuse allowed
current.pop()
backtrack(0, [], target)
return result
print(combination_sum([2, 3, 6, 7], 7)) # [[2,2,3],[7]]
print(combination_sum([2, 3, 5], 8)) # [[2,2,2,2],[2,3,3],[3,5]]
Partition Set into Two Equal Sum Subsets
def can_partition(nums):
"""
Can the array be partitioned into two subsets with equal sum?
"""
total = sum(nums)
if total % 2 != 0:
return False
target = total // 2
def backtrack(start, current_sum):
if current_sum == target:
return True
if current_sum > target or start == len(nums):
return False
# Try including nums[start]
if backtrack(start + 1, current_sum + nums[start]):
return True
# Skip including nums[start]
return backtrack(start + 1, current_sum)
return backtrack(0, 0)
print(can_partition([1, 5, 11, 5])) # True — {1,5,5} and {11}
print(can_partition([1, 2, 3, 5])) # False
All Partitions of a Set
def set_partitions(nums):
"""
Generate all ways to partition nums into non-empty groups.
Bell number B(n): B(1)=1, B(2)=2, B(3)=5, B(4)=15, B(5)=52...
"""
result = []
def backtrack(idx, groups):
if idx == len(nums):
result.append([list(g) for g in groups])
return
# Option 1: Add nums[idx] to an existing group
for group in groups:
group.append(nums[idx])
backtrack(idx + 1, groups)
group.pop()
# Option 2: Start a new group with nums[idx]
groups.append([nums[idx]])
backtrack(idx + 1, groups)
groups.pop()
backtrack(0, [])
return result
partitions = set_partitions([1, 2, 3])
print(f"Number of partitions of 3-element set: {len(partitions)}") # 5 (Bell number B(3))
Subsets vs Combinations vs Permutations
| Problem | Order matters? | Repetition? | Count | Algorithm |
|---|
| Subsets | No | No | 2^n | Backtrack with start index |
| Combinations C(n,k) | No | No | C(n,k) | Backtrack, size limit |
| Permutations | Yes | No | n! | Backtrack, swap |
| Combinations w/ rep. | No | Yes | C(n+k-1,k) | Backtrack, same start |
| Permutations w/ rep. | Yes | Yes | n^k | Backtrack, no start |
Summary
Subset generation is foundational to many backtracking problems:
- All subsets: Backtrack with include/exclude at each position — O(2^n × n)
- Bitmask: Iterate 0 to 2^n-1, check bits — same complexity, simpler code
- With duplicates: Sort first, skip adjacent duplicates at same level
- Combinations of size k: Add length check as base case + pruning
- Combination sum: Add target check, allow element reuse with same start index
The pattern of "make a choice, recurse, undo" applies to all these variants. The only thing that changes is when the base case fires and which elements are available at each step.
*Next Lesson: Backtracking Problems*