Editorial
Core insight
Combinations · Backtracking Patterns
Core Insight for Combinations
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.
- Capacity Constraint: If the current combination size reaches , we record the result and backtrack immediately. We do not process further numbers.
- Feasibility Constraint: If the number of elements currently selected plus the number of remaining candidates is less than , we cannot possibly form a valid combination. We prune this branch (stop recursing).
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".
- Moving left (Include 1): We update our state to
[1]and move to decide on2. - Moving right (Exclude 1): Our state remains
[]and we move to decide on2. - At any node, if our state size is , it is a leaf. If we run out of numbers to pick, it is a leaf.
Deep dive
Full breakdown
Full reasoning, pitfalls, and implementation flow
LeetCode 77: Combinations Solution & Explanation
Problem Overview
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., [1, 2] is the same as [2, 1]), 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.
Brute Force Approach for Combinations
The brute force strategy for generating combinations often involves generating every possible subset of the numbers and filtering them based on their size.
Naive Logic
- Generate the power set (all subsets) of the range .
- Iterate through every generated subset.
- Check if the size of the subset is exactly equal to .
- If yes, add it to the result list.
Pseudo-code
function 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)Why It Fails
While this logic produces the correct answer, it is computationally inefficient.
- Time Complexity: . For , operations, which might pass, but if , we still generate huge subsets (size 20) only to discard them.
- Inefficiency: The algorithm explores branches that are already invalid. For example, if we have already selected items, continuing to add more items to the subset is wasted effort. Similarly, if we have excluded so many items that it is impossible to reach size with the remaining numbers, we should stop immediately.
Algorithm Strategy: Backtracking Patterns
We implement the Subsets (Include/Exclude) subpattern using recursion.
- State Management: We track the
current_number(1 to ) we are considering and thecurrent_combination(list of selected numbers). - Base Case (Success): If
current_combinationhas size , add a copy of it to the results and return. - Base Case (Failure/Boundary): If
current_numberexceeds , return. - Recursive Step (Include):
- Add
current_numbertocurrent_combination. - Recursively call the function for
current_number + 1. - Backtrack: Remove
current_numberfromcurrent_combinationto restore the state.
- Add
- Recursive Step (Exclude):
- Skip
current_number. - Recursively call the function for
current_number + 1. - Optimization: Only take this branch if there are enough remaining numbers to fill the combination to size .
- Skip
Execution Flow
Let's trace n = 4, k = 2.
- Start:
index = 1,combo = []. - Decision at 1:
- Include 1:
combo = [1]. Recurse toindex = 2.- Decision at 2:
- Include 2:
combo = [1, 2]. Size is (2). Add [1, 2] to result. Return. - Backtrack:
combobecomes[1]. - Exclude 2: Recurse to
index = 3.- Decision at 3:
- Include 3:
combo = [1, 3]. Size is 2. Add [1, 3]. Return. - Exclude 3: Recurse to
index = 4.- Include 4:
combo = [1, 4]. Size is 2. Add [1, 4]. Return. - Exclude 4: Not enough numbers left to reach size 2. (Pruned).
- Include 4:
- Include 3:
- Decision at 3:
- Include 2:
- Decision at 2:
- Backtrack:
combobecomes[]. - Exclude 1: Recurse to
index = 2.- Decision at 2:
- Include 2:
combo = [2]. Recurse toindex = 3... - (Process continues similarly for remaining numbers).
- Include 2:
- Decision at 2:
- Include 1:
Proof of Correctness
The algorithm is correct because it performs an exhaustive search of the valid state space defined by the constraints.
- Completeness: The binary decision tree covers every possible subset of . By traversing both "include" and "exclude" branches, we ensure no subset is missed.
- Validity: The base case
current.size() == kensures we only store combinations of the correct length. - Uniqueness: Since we iterate numbers from to strictly in increasing order, we never generate permutations (e.g., we generate
[1, 2]but never[2, 1]), satisfying the definition of a combination.
Pattern Reuse Notes
The Backtracking - Subsets (Include/Exclude) pattern is a fundamental technique used in several other popular interview questions:
- LeetCode 17: Letter Combinations of a Phone Number - While typically an N-ary tree, it can be conceptualized similarly as making choices at each digit.
- LeetCode 78: Subsets - This is the parent problem. It uses the exact same Include/Exclude logic but collects all nodes in the tree, not just those at depth .
- LeetCode 90: Subsets II - Applies the same pattern but adds logic to handle duplicate input numbers by skipping adjacent duplicates during the "Exclude" phase or via sorting.
Feynman coach · Before you peek at the code
Can you explain the core insight in your own words?
If you can explain it before reading code, you’re far more likely to recall it under interview pressure.
Reference Implementation
C++ Solution for LeetCode 77
1class Solution {
2public:
3 vector<vector<int>> combine(int n, int k) {
4 vector<vector<int>> result;
5 vector<int> current;
6 backtrack(1, n, k, current, result);
7 return result;
8 }
9
10private:
11 void backtrack(int index, int n, int k, vector<int>& current, vector<vector<int>>& result) {
12 // Base case: combination is complete
13 if (current.size() == k) {
14 result.push_back(current);
15 return;
16 }
17
18 // Base case: index out of bounds
19 if (index > n) {
20 return;
21 }
22
23 // Branch 1: Include current number
24 current.push_back(index);
25 backtrack(index + 1, n, k, current, result);
26 current.pop_back(); // Backtrack
27
28 // Branch 2: Exclude current number
29 // Optimization: Only proceed if we have enough remaining numbers to fill k
30 // Needed: k - current.size()
31 // Available after skipping 'index': n - index
32 if (n - index >= k - current.size()) {
33 backtrack(index + 1, n, k, current, result);
34 }
35 }
36};Java Solution for LeetCode 77
1import java.util.ArrayList;
2import java.util.List;
3
4class Solution {
5 public List<List<Integer>> combine(int n, int k) {
6 List<List<Integer>> result = new ArrayList<>();
7 backtrack(1, n, k, new ArrayList<>(), result);
8 return result;
9 }
10
11 private void backtrack(int index, int n, int k, List<Integer> current, List<List<Integer>> result) {
12 // Base case: combination is complete
13 if (current.size() == k) {
14 result.add(new ArrayList<>(current)); // Deep copy required
15 return;
16 }
17
18 // Base case: index out of bounds
19 if (index > n) {
20 return;
21 }
22
23 // Branch 1: Include current number
24 current.add(index);
25 backtrack(index + 1, n, k, current, result);
26 current.remove(current.size() - 1); // Backtrack
27
28 // Branch 2: Exclude current number
29 // Optimization: Pruning check
30 // We need (k - current.size()) more elements
31 // We have (n - index) elements left if we skip 'index'
32 if (n - index >= k - current.size()) {
33 backtrack(index + 1, n, k, current, result);
34 }
35 }
36}Python Solution for LeetCode 77
1from typing import List
2
3class Solution:
4 def combine(self, n: int, k: int) -> List[List[int]]:
5 result = []
6
7 def backtrack(index: int, current: List[int]):
8 # Base case: combination is complete
9 if len(current) == k:
10 result.append(current[:]) # Make a copy
11 return
12
13 # Base case: index out of bounds
14 if index > n:
15 return
16
17 # Branch 1: Include current number
18 current.append(index)
19 backtrack(index + 1, current)
20 current.pop() # Backtrack
21
22 # Branch 2: Exclude current number
23 # Optimization: Pruning check
24 # If we skip 'index', we have (n - index) numbers left
25 # We need (k - len(current)) numbers
26 if n - index >= k - len(current):
27 backtrack(index + 1, current)
28
29 backtrack(1, [])
30 return result