Subsets (Power Set) - Complete Backtracking Guide
Subsets (Power Set) - Complete Backtracking Guide
Introduction
The Subsets problem, often referred to as generating the Power Set, is a fundamental problem in computer science and algorithm design. It serves as an excellent gateway to understanding Backtracking.
Given a set of distinct integers, return all possible subsets (the power set). The solution set must not contain duplicate subsets.
What is a Power Set?
The power set of a set $S$ is the set of all subsets of $S$, including the empty set and $S$ itself. If a set has $N$ elements, its power set has $2^N$ elements.
For example, if $S = [1, 2, 3]$, the power set is:
[ [], [1], [2], [3], [1,2], [1,3], [2,3], [1,2,3] ]
The Backtracking Approach
Backtracking is a systematic way to iterate through all possible configurations of a problem. It is essentially a depth-first search (DFS) with pruning.
Core Logic: Include or Exclude
For every element in the array, we have exactly two choices:
- Include the element in the current subset.
- Exclude the element from the current subset.
By making these binary decisions for every element, we naturally generate all possible combinations.
Algorithm Steps
- Initialization: Create an empty array
pathto store the current subset being built and an arrayresultto store all valid subsets. - Recursive Function: Define a function
backtrack(startIndex).
- Base Case: If
startIndexreaches the length of the input array, we have explored all elements. Push a copy ofpathintoresult. - Recursive Step: Iterate
ifromstartIndexto the end of the array. - Action: Add
nums[i]topath. - Recurse: Call
backtrack(i + 1)to explore further elements. - Backtrack: Remove
nums[i]frompath(this is the crucial step). This resets the state for the next iteration of the loop.
- Start: Call
backtrack(0)to begin the process.
Visualizing the Recursion Tree
Imagine the array [1, 2, 3]:
Start (Index 0)
āāā Include 1
ā āāā Include 2
ā ā āāā Include 3 -> Push [1,2,3]
ā ā āāā Exclude 3 -> Push [1,2]
ā āāā Exclude 2
ā āāā Include 3 -> Push [1,3]
ā āāā Exclude 3 -> Push [1]
āāā Exclude 1
āāā Include 2
ā āāā Include 3 -> Push [2,3]
ā āāā Exclude 3 -> Push [2]
āāā Exclude 2
āāā Include 3 -> Push [3]
āāā Exclude 3 -> Push []Code Implementation
TypeScript/JavaScript
function subsets(nums: number[]): number[][] {
const result: number[][] = [];
const path: number[] = [];
function backtrack(startIndex: number) {
// Add a copy of the current path to the result
result.push([...path]);
// Iterate through the remaining elements
for (let i = startIndex; i < nums.length; i++) {
// Action: Include nums[i]
path.push(nums[i]);
// Recurse: Move to the next index
backtrack(i + 1);
// Backtrack: Remove nums[i] to explore the 'exclude' branch
path.pop();
}
}
backtrack(0);
return result;
}Python
def subsets(nums):
result = []
path = []
def backtrack(start):
result.append(path.copy())
for i in range(start, len(nums)):
path.append(nums[i])
backtrack(i + 1)
path.pop()
backtrack(0)
return resultComplexity Analysis
Time Complexity: $O(N imes 2^N)$
- Number of Subsets: There are $2^N$ subsets in the power set.
- Copying: For each subset, we create a new array (using
[...path]or.copy()) which takes $O(N)$ time. - Total: $2^N imes N$.
Space Complexity: $O(N)$
- Auxiliary Space: The recursion stack goes as deep as $N$ (when we include every element).
- Output Space: The space required to store the result is $O(N imes 2^N)$, which is unavoidable as we must store all subsets.
Handling Duplicates
If the input array contains duplicates (e.g., [1, 2, 2]), the naive approach will generate duplicate subsets like [1, 2] and [1, 2].
Solution Strategy
- Sort the input array first.
- In the loop, if
i > startIndexandnums[i] == nums[i-1], skip the current iteration.
function subsetsWithDup(nums: number[]): number[][] {
const result: number[][] = [];
const path: number[] = [];
// Sort to handle duplicates easily
nums.sort((a, b) => a - b);
function backtrack(startIndex: number) {
result.push([...path]);
for (let i = startIndex; i < nums.length; i++) {
// Skip duplicates: if current element is same as previous and not the first in this level
if (i > startIndex && nums[i] === nums[i - 1]) {
continue;
}
path.push(nums[i]);
backtrack(i + 1);
path.pop();
}
}
backtrack(0);
return result;
}Common Mistakes
- Not Copying the Path: If you push
pathdirectly intoresult, all entries inresultwill point to the same reference. You must push a copy of the array ([...path]orpath.slice()). - Incorrect Backtracking: Forgetting to
pop()the element after the recursive call causes thepathto grow indefinitely and leads to incorrect results. - Index Management: Using
iinstead ofi + 1in the recursive call will cause infinite recursion or incorrect subsets because it allows reusing the same element multiple times in one subset.
Real-World Applications
- Database Queries: Generating all possible combinations of filter criteria.
- Feature Flags: Evaluating all possible combinations of enabled/disabled features.
- Cryptography: Generating keys and permutations.
- Game Development: Exploring all possible moves or board states in a game tree.
Summary
The Subsets problem is the quintessential backtracking problem. It teaches the pattern of building a solution incrementally and undoing changes (backtracking) when a path doesn't lead to a valid solution. Mastering this pattern is essential for solving problems like Permutations, Combination Sum, and Subsets II.