[]
path
Every subset is a node in the include/exclude decision tree — record at every level, not just the leaves. A clean backtracking template (push → recurse → pop) generates all 2^n subsets in one DFS pass.
Given an integer array nums with distinct elements, return all possible subsets (the power set). The solution may be in any order and must contain no duplicate subsets.
Concrete example: nums = [1, 2, 3] → [[], [1], [1,2], [1,2,3], [1,3], [2], [2,3], [3]] — eight subsets total (2^3 = 8).
2^n subsets with zero extra bookkeeping.bt(0, []) — start from index 0 with an empty path. Push an empty result immediately (the empty set is always a subset).result.push([...path]). This captures the subset formed by every element chosen so far, from the empty set to the full set.i from start to nums.length - 1. Push nums[i]onto the path (the “include” branch).bt(i + 1, path) — pass i + 1 (not start + 1) so we never revisit earlier elements and avoid producing duplicate subsets.path.pop()— restore the path for the next iteration (the “exclude” branch). This is the backtrack step.start + 1 instead of i + 1 in the recursive call. When the loop variable is i (not start), you must pass i + 1 so each deeper level only considers elements to the right of the one just chosen. Using start + 1 produces duplicate subsets because two different iterations could recurse into overlapping index ranges.Both approaches are optimal — you must output all 2^n subsets and each takes O(n) to copy, so O(n · 2^n) is tight. Space is the same for the call stack (depth n) plus the output.
[1, 2, 3]. We record every node in the decision tree — include or exclude each element.1▶function subsets(nums: number[]): number[][] {2▶ const result: number[][] = [];34 function bt(start: number, path: number[]): void {5 result.push([...path]); // record at every node67 for (let i = start; i < nums.length; i++) {8 path.push(nums[i]); // choose: include nums[i]9 bt(i + 1, path); // recurse — next index only (no duplicates)10 path.pop(); // un-choose: exclude nums[i]11 }12 }1314▶ bt(0, []);15 return result;16}
function subsets(nums: number[]): number[][] {
const result: number[][] = [];
function bt(start: number, path: number[]): void {
result.push([...path]); // record at every node
for (let i = start; i < nums.length; i++) {
path.push(nums[i]); // choose: include nums[i]
bt(i + 1, path); // recurse — next index only (no duplicates)
path.pop(); // un-choose: exclude nums[i]
}
}
bt(0, []);
return result;
}bt(start, path). start is the index from which we may still pick elements; path is the subset built so far (mutated in-place for efficiency).result.push([...path]) snapshots the path at the top of every call — before any element is chosen for this level. This is what makes the approach collect every node in the decision tree, not just leaf nodes.i, push nums[i], recurse with i + 1(forward only), then pop. The pop is the backtrack — it undoes the choice so the loop's next iteration starts from a clean state.bt(0, []) — empty path, start from index 0. The first record call immediately adds the empty subset [].[[]], then for each number append it to every existing subset and push the new subsets back in. Same O(n · 2^n), no recursion.path.length === k (Combinations, LC 77).used[] array; loop from 0 every time but skip used indices.2^n - 1; bit j set means include nums[j]. Same complexity, simpler to code but harder to adapt when subsets have constraints.| "return all possible subsets / power set" | backtrack, record at every node |
| enumerate all include/exclude combos | bt(start, path): push→recurse(i+1)→pop |
| "subsets with duplicates" / sort + skip | Subsets II pattern |
| "all combinations of size k" | same skeleton, record only when path.length===k |
function subsets(nums: number[]): number[][] {
const result: number[][] = [];
function bt(start: number, path: number[]): void {
result.push([...path]); // record here (every node = valid subset)
for (let i = start; i < nums.length; i++) {
path.push(nums[i]);
bt(i + 1, path);
path.pop();
}
}
bt(0, []);
return result;
}result.push([...path]). Every node in the decision tree is a valid subset.n?nums = [1, 2]. How many subsets are returned?start + 1 instead of i + 1 in the recursive call?