Subsets and Combinations Patterns
1. Generate 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 Duplicate Elements
| 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 of 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 Problem 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 of Phone Number
| 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