Loading problems...
TL;DR: Use a recursive backtracking approach to build combinations by making a binary decision for each number from to : either include it in the current set or exclude it.
The LeetCode 77 problem asks us to generate all possible combinations of numbers chosen from the range . Unlike permutations, the order of numbers in a combination does not matter (e.g., is the same as ), and the problem asks for the result in any order. This is a classic combinatorial problem often used to test understanding of recursion and state-space tree traversal.
[1, 2][2, 1]The brute force strategy for generating combinations often involves generating every possible subset of the numbers and filtering them based on their size.
textfunction generateAllSubsets(index, currentSubset): if index > n: if currentSubset.size == k: addToResult(currentSubset) return // Include index currentSubset.add(index) generateAllSubsets(index + 1, currentSubset) currentSubset.removeLast() // Exclude index generateAllSubsets(index + 1, currentSubset)
While this logic produces the correct answer, it is computationally inefficient.
The transition from brute force to the optimal LeetCode 77 solution involves pruning. We can visualize the search space as a state-space tree where each level represents a number from to .
The core insight is to maintain the invariant that we only explore paths that can potentially lead to a valid solution.
Visual Description:
Imagine a binary tree. The root represents the decision for number 1. The left edge represents "Include 1", and the right edge represents "Exclude 1".
[1] and move to decide on 2.[] and we move to decide on 2.![]()
Let's trace n = 4, k = 2.
index = 1, combo = [].combo = [1]. Recurse to index = 2.
combo = [1, 2]. Size is (2). Add [1, 2] to result. Return.combo becomes [1].index = 3.
combo = [1, 3]. Size is 2. Add [1, 3]. Return.index = 4.
combo = [1, 4]. Size is 2. Add [1, 4]. Return.combo becomes [].index = 2.
combo = [2]. Recurse to index = 3...The algorithm is correct because it performs an exhaustive search of the valid state space defined by the constraints.
current.size() == k ensures we only store combinations of the correct length.[1, 2] but never [2, 1]), satisfying the definition of a combination.cppclass Solution { public: vector<vector<int>> combine(int n, int k) { vector<vector<int>> result; vector<int> current; backtrack(1, n, k, current, result); return result; } private: void backtrack(int index, int n, int k, vector<int>& current, vector<vector<int>>& result) { // Base case: combination is complete if (current.size() == k) { result.push_back(current); return; } // Base case: index out of bounds if (index > n) { return; } // Branch 1: Include current number current.push_back(index); backtrack(index + 1, n, k, current, result); current.pop_back(); // Backtrack // Branch 2: Exclude current number // Optimization: Only proceed if we have enough remaining numbers to fill k // Needed: k - current.size() // Available after skipping 'index': n - index if (n - index >= k - current.size()) { backtrack(index + 1, n, k, current, result); } } };
javaimport java.util.ArrayList; import java.util.List; class Solution { public List<List<Integer>> combine(int n, int k) { List<List<Integer>> result = new ArrayList<>(); backtrack(1, n, k, new ArrayList<>(), result); return result; } private void backtrack(int index, int n, int k, List<Integer> current, List<List<Integer>> result) { // Base case: combination is complete if (current.size() == k) { result.add(new ArrayList<>(current)); // Deep copy required return; } // Base case: index out of bounds if (index > n) { return; } // Branch 1: Include current number current.add(index); backtrack(index + 1, n, k, current, result); current.remove(current.size() - 1); // Backtrack // Branch 2: Exclude current number // Optimization: Pruning check // We need (k - current.size()) more elements // We have (n - index) elements left if we skip 'index' if (n - index >= k - current.size()) { backtrack(index + 1, n, k, current, result); } } }
pythonfrom typing import List class Solution: def combine(self, n: int, k: int) -> List[List[int]]: result = [] def backtrack(index: int, current: List[int]): # Base case: combination is complete if len(current) == k: result.append(current[:]) # Make a copy return # Base case: index out of bounds if index > n: return # Branch 1: Include current number current.append(index) backtrack(index + 1, current) current.pop() # Backtrack # Branch 2: Exclude current number # Optimization: Pruning check # If we skip 'index', we have (n - index) numbers left # We need (k - len(current)) numbers if n - index >= k - len(current): backtrack(index + 1, current) backtrack(1, []) return result
The time complexity is bounded by the number of valid combinations, which is the binomial coefficient . Technically, the complexity is because for each valid combination, we spend time copying the list to the result set. The pruning ensures we do not visit significantly more states than there are valid nodes in the decision tree.
current_combination which grows to at most size .The Backtracking - Subsets (Include/Exclude) pattern is a fundamental technique used in several other popular interview questions:
Mistake: Implementing pure recursion without pruning. Why: If you explore the "Exclude" branch even when it's mathematically impossible to finish the combination (e.g., you need 3 more numbers but only 2 remain), you waste massive computational resources exploring dead ends. Consequence: The algorithm processes significantly more nodes than necessary, leading to TLE on larger inputs.
Mistake: Storing a reference to the list instead of a copy.
Why: In languages like Java and Python, lists are mutable objects passed by reference. When you add current to result, you point to the same object that is later modified by pop()/remove().
Consequence: By the time the algorithm finishes, the current list is empty (due to backtracking), so your result contains references to an empty list.
[1,2] and [2,1]?Mistake: Resetting the search loop to start from 1 at every recursive step, or failing to enforce order.
Why: Combinations are unordered. If you allow picking 1 then 2, and later 2 then 1, you are generating permutations, not combinations.
Consequence: Incorrect output size and duplicates. The pattern enforces processing numbers strictly to avoid this.