function subsets(nums) { const result = []; const current = []; function backtrack(index) { // Base case: add current subset result.push([...current]); // Try adding each remaining element for (let i = index; i < nums.length; i++) { current.push(nums[i]); // Include backtrack(i + 1); // Recurse current.pop(); // Backtrack } } backtrack(0); return result;}// Example: [1,2,3]// Result: [[],[1],[1,2],[1,2,3],[1,3],[2],[2,3],[3]]// Decision tree:// []// / | \// [1] [2] [3]// / \ |// [1,2][1,3][2,3]// |// [1,2,3]
Example: Iterative build approach
function subsetsIterative(nums) { const result = [[]]; // Start with empty subset for (const num of nums) { const n = result.length; // Add num to all existing subsets for (let i = 0; i < n; i++) { result.push([...result[i], num]); } } return result;}// Example: [1,2,3]// Step by step:// Initial: [[]]// Add 1: [[], [1]]// Add 2: [[], [1], [2], [1,2]]// Add 3: [[], [1], [2], [1,2], [3], [1,3], [2,3], [1,2,3]]// Total: 2^n subsets for n elements
Example: Bit manipulation approach
function subsetsBitmask(nums) { const n = nums.length; const result = []; const totalSubsets = 1 << n; // 2^n // Generate all numbers from 0 to 2^n - 1 for (let mask = 0; mask < totalSubsets; mask++) { const subset = []; // Check each bit in the mask for (let i = 0; i < n; i++) { if (mask & (1 << i)) { subset.push(nums[i]); } } result.push(subset); } return result;}// Example: [1,2,3] (n=3, so 2^3 = 8 subsets)// mask=0 (000): []// mask=1 (001): [1]// mask=2 (010): [2]// mask=3 (011): [1,2]// mask=4 (100): [3]// mask=5 (101): [1,3]// mask=6 (110): [2,3]// mask=7 (111): [1,2,3]// Each bit represents whether element at that index is included
2. Subsets with Duplicates
Challenge
Solution
Key Technique
Complexity
Duplicate Elements
Sort array first, skip duplicates in same level
Continue only if i==start or nums[i]!=nums[i-1]
O(n log n) sort
Level Skipping
Skip duplicates at same recursion level
Prevents generating duplicate subsets
Same branch OK, different branch skip
Counting Approach
Count frequency, add 0 to freq[num] copies
For each unique element, choose 0..count copies
More complex but elegant
Set-based Dedup
Use Set with serialized subsets
Simple but inefficient - O(2^n) space
Not recommended for interviews
Example: Subsets with duplicates
function subsetsWithDup(nums) { const result = []; const current = []; // CRITICAL: Sort to group duplicates together nums.sort((a, b) => a - b); function backtrack(start) { result.push([...current]); for (let i = start; i < nums.length; i++) { // Skip duplicates at the same recursion level // i > start ensures we take first occurrence if (i > start && nums[i] === nums[i - 1]) { continue; } current.push(nums[i]); backtrack(i + 1); current.pop(); } } backtrack(0); return result;}// Example: [1,2,2]// After sort: [1,2,2]// Result: [[],[1],[1,2],[1,2,2],[2],[2,2]]// Without skip: would generate [1,2] twice// With skip: second 2 at same level is skipped// Why i > start?// At start, we want to include first duplicate// At start+1, we skip subsequent duplicates
Note: The condition i > start is critical - it allows us to use the first occurrence of a duplicate (when i == start) but skip subsequent ones at the same recursion level. This ensures we don't generate duplicate subsets while still exploring all valid combinations.
3. Combinations K Elements
Pattern
Implementation
Pruning Optimization
Time
Basic Backtracking
Choose k elements from n, order doesn't matter
Stop when current.length == k
O(C(n,k) * k)
Early Termination
Stop if not enough elements remaining
if (i + (k - current.length) > n) break
Significantly faster
Pascal's Triangle
C(n,k) = C(n-1,k-1) + C(n-1,k)
Include first or skip first recursively
Alternative approach
Iterative
Use k nested loops or bitmask with k bits set
Complex for variable k
Only for fixed small k
Example: Combinations C(n, k)
function combine(n, k) { const result = []; const current = []; function backtrack(start) { // Base case: found k elements if (current.length === k) { result.push([...current]); return; } // Pruning: need k - current.length more elements // Remaining elements: n - start + 1 // If remaining < needed, no point continuing const needed = k - current.length; const remaining = n - start + 1; for (let i = start; i <= n; i++) { // Pruning optimization if (remaining < needed) break; current.push(i); backtrack(i + 1); current.pop(); remaining--; // One less element available } } backtrack(1); return result;}// Example: n=4, k=2// Result: [[1,2],[1,3],[1,4],[2,3],[2,4],[3,4]]// Without pruning: explores [4] even though needs 2 elements// With pruning: stops early when not enough elements left
Example: Pascal's triangle approach
function combinePascal(n, k) { const result = []; function backtrack(start, current) { if (current.length === k) { result.push([...current]); return; } if (start > n) return; // Two choices: include start or skip start // Include start current.push(start); backtrack(start + 1, current); current.pop(); // Skip start backtrack(start + 1, current); } backtrack(1, []); return result;}// This explicitly shows the C(n,k) = C(n-1,k-1) + C(n-1,k) relation// Include element: reduce to C(n-1, k-1)// Exclude element: reduce to C(n-1, k)
4. Combination Sum Variations
Variation
Rule
Implementation Detail
Complexity
Combination Sum I
Unlimited reuse of elements
Recurse with same index: backtrack(i)
O(2^target)
Combination Sum II
Each element used once, has duplicates
Sort + skip duplicates, recurse i+1
O(2^n)
Combination Sum III
k numbers that sum to n, use 1-9 once
Combinations with sum constraint
O(C(9,k))
Combination Sum IV
Order matters (permutations)
Dynamic Programming, not backtracking
O(target * n)
Example: Combination Sum I (unlimited reuse)
function combinationSum(candidates, target) { const result = []; const current = []; candidates.sort((a, b) => a - b); // Optional optimization function backtrack(start, remaining) { if (remaining === 0) { result.push([...current]); return; } if (remaining < 0) return; // Exceeded target for (let i = start; i < candidates.length; i++) { // Pruning: if sorted, can break early if (candidates[i] > remaining) break; current.push(candidates[i]); // Can reuse same element: pass i not i+1 backtrack(i, remaining - candidates[i]); current.pop(); } } backtrack(0, target); return result;}// Example: [2,3,6,7], target=7// Result: [[2,2,3],[7]]// Note: [2,2,3] valid because can reuse 2
Example: Combination Sum II (each once, has duplicates)
function combinationSum2(candidates, target) { const result = []; const current = []; candidates.sort((a, b) => a - b); // Required for dedup function backtrack(start, remaining) { if (remaining === 0) { result.push([...current]); return; } for (let i = start; i < candidates.length; i++) { // Skip duplicates at same level if (i > start && candidates[i] === candidates[i-1]) { continue; } if (candidates[i] > remaining) break; current.push(candidates[i]); // Each element used once: pass i+1 backtrack(i + 1, remaining - candidates[i]); current.pop(); } } backtrack(0, target); return result;}// Example: [10,1,2,7,6,1,5], target=8// After sort: [1,1,2,5,6,7,10]// Result: [[1,1,6],[1,2,5],[1,7],[2,6]]
Example: Combination Sum IV (order matters - DP)
function combinationSum4(nums, target) { const dp = new Array(target + 1).fill(0); dp[0] = 1; // One way to make 0: use nothing // For each sum from 1 to target for (let sum = 1; sum <= target; sum++) { // Try each number as last number for (const num of nums) { if (sum - num >= 0) { dp[sum] += dp[sum - num]; } } } return dp[target];}// Example: [1,2,3], target=4// Result: 7 (count of combinations)// [1,1,1,1], [1,1,2], [1,2,1], [2,1,1], [2,2], [1,3], [3,1]// This is NOT backtracking - it's DP because order matters// dp[i] = sum of dp[i-num] for all num in nums where i-num >= 0
Warning: Combination Sum IV is fundamentally different from I, II, III. It's a permutation problem (order matters), not combination. Use dynamic programming, not backtracking. The name is misleading!
5. Letter Combinations Phone
Approach
Technique
Time
Space
Backtracking
Build string digit by digit, try all letters
O(4^n)
O(n) depth
BFS Queue
Build all combinations level by level
O(4^n)
O(4^n)
Iterative Build
For each digit, append all its letters to existing combos
O(4^n)
O(4^n)
Cartesian Product
Mathematical view: product of letter sets
O(4^n)
Conceptual approach
Example: Backtracking approach
function letterCombinations(digits) { if (!digits) return []; const phone = { '2': 'abc', '3': 'def', '4': 'ghi', '5': 'jkl', '6': 'mno', '7': 'pqrs', '8': 'tuv', '9': 'wxyz' }; const result = []; function backtrack(index, current) { // Base case: built complete combination if (index === digits.length) { result.push(current); return; } // Get letters for current digit const letters = phone[digits[index]]; // Try each letter for (const letter of letters) { backtrack(index + 1, current + letter); } } backtrack(0, ''); return result;}// Example: "23"// Result: ["ad","ae","af","bd","be","bf","cd","ce","cf"]// Decision tree:// ""// / | \// a b c (digit 2)// / | \ /|\ /|\// d e f d e f d e f (digit 3)
Example: Iterative build approach
function letterCombinationsIterative(digits) { if (!digits) return []; const phone = { '2': 'abc', '3': 'def', '4': 'ghi', '5': 'jkl', '6': 'mno', '7': 'pqrs', '8': 'tuv', '9': 'wxyz' }; let result = ['']; for (const digit of digits) { const letters = phone[digit]; const temp = []; // For each existing combination for (const combo of result) { // Append each letter of current digit for (const letter of letters) { temp.push(combo + letter); } } result = temp; } return result;}// Example: "23"// Initial: ['']// After '2': ['a', 'b', 'c']// After '3': ['ad','ae','af','bd','be','bf','cd','ce','cf']
Example: BFS queue approach
function letterCombinationsBFS(digits) { if (!digits) return []; const phone = { '2': 'abc', '3': 'def', '4': 'ghi', '5': 'jkl', '6': 'mno', '7': 'pqrs', '8': 'tuv', '9': 'wxyz' }; const queue = ['']; for (const digit of digits) { const letters = phone[digit]; const size = queue.length; // Process all combinations at current level for (let i = 0; i < size; i++) { const combo = queue.shift(); // Add all possible extensions for (const letter of letters) { queue.push(combo + letter); } } } return queue;}// BFS level-by-level construction// Each level corresponds to processing one digit// Queue contains all partial combinations at current level
Note: For phone number combinations, all approaches have the same time complexity O(4^n) where n is the number of digits (worst case when digits are 7 or 9 which map to 4 letters). The backtracking approach is most memory efficient with O(n) call stack vs O(4^n) for iterative approaches that store all intermediate results.
Summary: Subsets and Combinations Patterns
Powerset (All Subsets): 2^n subsets for n elements - backtracking (include/exclude), iterative build (double result each step), or bitmask (each number 0 to 2^n-1)
Backtracking Template: Add current state to result, loop from start to end, recurse with i+1 (or i for reuse), backtrack by removing last element
Subsets with Duplicates: Sort array first, skip duplicates at same recursion level with if (i > start && nums[i] === nums[i-1]) continue
Level vs Branch Duplicates: i > start allows first duplicate (when i==start) but skips subsequent ones at same level - prevents duplicate subsets
Combinations C(n,k): Choose k from n - base case when current.length == k, prune when remaining elements < needed elements
Pruning Optimization: Early termination: if (n - start + 1 < k - current.length) break - skip when not enough elements left
Combination Sum I: Unlimited reuse - recurse with same index backtrack(i, remaining), can use same element multiple times
Combination Sum II: Each element once, has duplicates - sort + skip duplicates + recurse i+1, combines deduplication with single-use
Combination Sum IV: Order matters (permutations) - NOT backtracking, use DP: dp[sum] += dp[sum-num] for all nums
Letter Combinations: Cartesian product of digit mappings - backtracking digit by digit, or iterative build extending all combos
Complexity Analysis: Subsets O(2^n), Combinations O(C(n,k)), Combination Sum O(2^target), Phone O(4^n for 7/9 digits)
Key Pattern: Most use backtracking with start index to avoid duplicates/permutations, except Combination Sum IV which needs DP