Subsets and Combinations

1. All Subsets Powerset

Approach Method Time Space
Backtracking Include/exclude each element recursively O(2^n) O(n) depth
Iterative Build Add each element to all existing subsets O(n * 2^n) O(2^n)
Bit Manipulation Use binary representation for subset membership O(n * 2^n) O(1) extra
Lexicographic Generate in sorted order using cascading O(n * 2^n) O(n)

Example: Backtracking approach

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