266. Combination Sum
๐ LeetCode Problem: 39. Combination Sum
๐ Difficulty: Medium
๐ท๏ธ Topics: Array, Backtracking
Problem Statement
Given an array of distinct integers candidates and a target integer target, return a list of all unique combinations of candidates where the chosen numbers sum to target. You may return the combinations in any order.
The same number may be chosen from candidates an unlimited number of times. Two combinations are unique if the frequency of at least one of the chosen numbers is different.
Example 1:
Input: candidates = [2,3,6,7], target = 7
Output: [[2,2,3],[7]]
Explanation:
2 and 3 are candidates, and 2 + 2 + 3 = 7. Note that 2 can be used multiple times.
7 is a candidate, and 7 = 7.
These are the only two combinations.
Example 2:
Input: candidates = [2,3,5], target = 8
Output: [[2,2,2,2],[2,3,3],[3,5]]
Example 3:
Input: candidates = [2], target = 1
Output: []
Constraints:
- 1 <= candidates.length <= 30
- 2 <= candidates[i] <= 40
- All elements of candidates are distinct
- 1 <= target <= 40
๐ ELI5: The Simple Idea!
Think of it like making change for money:
You have coins: [2, 3, 6, 7]
You want to make: 7
Ways to make 7:
2 + 2 + 3 = 7 โ
7 = 7 โ
Can't make 7:
2 + 3 = 5 (not enough)
2 + 6 = 8 (too much)
6 + 3 = 9 (too much)
Key insight: You can use the SAME coin multiple times!
2 + 2 + 3 uses coin "2" twice
The Building Process:
Start with: [] (empty combination)
Target remaining: 7
Try coin 2:
[2], remaining: 7-2 = 5
Try coin 2 again:
[2, 2], remaining: 5-2 = 3
Try coin 2 again:
[2, 2, 2], remaining: 3-2 = 1
Can't use 2 (too big), can't use 3,6,7 (all too big)
Dead end!
Try coin 3:
[2, 2, 3], remaining: 3-3 = 0
Found one! โ
Continue exploring all possibilities...
๐จ Visual Understanding
Decision Tree for candidates=[2,3,6,7], target=7
[]
target=7
/ | | \
2 3 6 7
/ | | \
[2] [3] [6] [7]
rem=5 rem=4 rem=1 rem=0 โ
/ | | \ / | \ โ
2 3 6 7 3 6 7
/ | โ โ | โ โ
[2,2] [2,3] [3,3]
rem=3 rem=2 rem=-2 โ
/ | \ / \
2 3 6 3 6
/ | โ | โ
[2,2,2] [2,2,3] [2,3,3]
rem=1 rem=0 โ rem=-1 โ
โ
Valid combinations: [[2,2,3], [7]]
Why No Duplicates?
Without ordering constraint:
[2, 3, 2] and [2, 2, 3] are same combination!
[3, 2, 2] also same!
Solution: Only pick candidates >= last picked
If we picked 2, next can be: 2, 3, 6, 7
If we picked 3, next can be: 3, 6, 7 (not 2!)
If we picked 6, next can be: 6, 7 (not 2 or 3!)
This ensures: [2,2,3] found, but [2,3,2] and [3,2,2] never generated!
๐ฏ Approach 1: Backtracking (Recursion) โญโญ
The Most Common Solution
Algorithm:
Use backtracking with:
- Current combination being built
- Remaining target to achieve
- Start index (to avoid duplicates)
Rules:
1. If remaining == 0 โ Found valid combination!
2. If remaining < 0 โ Invalid, backtrack
3. Try each candidate from start index onwards
4. Can use same candidate multiple times
Implementation
import java.util.*;
/**
* Backtracking approach
* Time: O(N^(T/M)) where N=candidates.length, T=target, M=min(candidates)
* Space: O(T/M) - Recursion depth
*/
class Solution {
private List<List<Integer>> result;
public List<List<Integer>> combinationSum(int[] candidates, int target) {
result = new ArrayList<>();
backtrack(candidates, target, 0, new ArrayList<>());
return result;
}
// start: index to start picking from (avoids duplicates)
// remaining: how much more we need to reach target
private void backtrack(int[] candidates, int remaining, int start,
List<Integer> current) {
// Base case: found valid combination
if (remaining == 0) {
result.add(new ArrayList<>(current));
return;
}
// Base case: exceeded target
if (remaining < 0) {
return;
}
// Try each candidate from start index
for (int i = start; i < candidates.length; i++) {
// Choose
current.add(candidates[i]);
// Explore (can reuse same candidate, so pass i not i+1)
backtrack(candidates, remaining - candidates[i], i, current);
// Unchoose (backtrack)
current.remove(current.size() - 1);
}
}
public static void main(String[] args) {
Solution sol = new Solution();
System.out.println(sol.combinationSum(new int[]{2,3,6,7}, 7));
System.out.println(sol.combinationSum(new int[]{2,3,5}, 8));
}
}
โฐ Time: O(N^(T/M)) where N=len(candidates), T=target, M=min(candidates)
๐พ Space: O(T/M) - Maximum recursion depth
๐ Super Detailed Dry Run
Example: candidates = [2,3,6,7], target = 7
Goal: Find all combinations that sum to 7
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Backtrack Call Tree
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Call 1: backtrack([2,3,6,7], remaining=7, start=0, current=[])
remaining=7 (continue)
Loop i=0 (candidate=2):
current = [2]
โ backtrack([2,3,6,7], 5, 0, [2])
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Call 2: backtrack([2,3,6,7], remaining=5, start=0, current=[2])
remaining=5 (continue)
Loop i=0 (candidate=2):
current = [2, 2]
โ backtrack([2,3,6,7], 3, 0, [2,2])
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Call 3: backtrack([2,3,6,7], remaining=3, start=0, current=[2,2])
remaining=3 (continue)
Loop i=0 (candidate=2):
current = [2, 2, 2]
โ backtrack([2,3,6,7], 1, 0, [2,2,2])
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Call 4: backtrack([2,3,6,7], remaining=1, start=0, current=[2,2,2])
remaining=1 (continue)
Loop i=0 (candidate=2):
current = [2, 2, 2, 2]
โ backtrack([2,3,6,7], -1, 0, [2,2,2,2])
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Call 5: backtrack([2,3,6,7], remaining=-1, start=0, current=[2,2,2,2])
remaining=-1 < 0 โ Return (exceeded target)
[Back to Call 4]
Remove last element: current = [2,2,2]
Loop i=1 (candidate=3):
3 > 1 โ Will exceed, skip
Loop i=2,3: All too big, skip
Return
[Back to Call 3]
Remove last element: current = [2,2]
Loop i=1 (candidate=3):
current = [2, 2, 3]
โ backtrack([2,3,6,7], 0, 1, [2,2,3])
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Call 6: backtrack([2,3,6,7], remaining=0, start=1, current=[2,2,3])
remaining=0 โ Found valid combination!
Add [2,2,3] to result โ
Return
Result: [[2,2,3]]
[Back to Call 3]
Remove last element: current = [2,2]
Loop i=2 (candidate=6):
6 > 3 โ Skip
Loop i=3 (candidate=7):
7 > 3 โ Skip
Return
[Back to Call 2]
Remove last element: current = [2]
Loop i=1 (candidate=3):
current = [2, 3]
โ backtrack([2,3,6,7], 2, 1, [2,3])
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Call 7: backtrack([2,3,6,7], remaining=2, start=1, current=[2,3])
remaining=2 (continue)
Loop i=1 (candidate=3):
3 > 2 โ Skip
Loop i=2,3: All too big, skip
Return
[Back to Call 2]
Remove last element: current = [2]
Loop i=2,3: Candidates 6,7 too big, skip
Return
[Back to Call 1]
Remove last element: current = []
Loop i=1 (candidate=3):
current = [3]
โ backtrack([2,3,6,7], 4, 1, [3])
... (Similar exploration for candidates 3, 6, 7)
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Eventually reach:
Call N: backtrack([2,3,6,7], remaining=7, start=3, current=[])
Loop i=3 (candidate=7):
current = [7]
โ backtrack([2,3,6,7], 0, 3, [7])
remaining=0 โ Found valid combination!
Add [7] to result โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Final Result: [[2,2,3], [7]]
Visual Decision Tree:
([], 7)
/ | \
2 3 7
/ | \
([2],5) ([3],4) ([7],0) โ
/ \ |
2 3 3,6,7
/ \ (all exceed)
([2,2],3) ([2,3],2)
/ \ |
2 3 3,6,7
/ \ (all exceed)
([2,2,2],1) ([2,2,3],0) โ
|
2,3,6,7
(all exceed)
๐ฏ Approach 2: Backtracking with Pruning โญ
Optimized Version
Key optimization: Sort candidates first, then break early when candidate exceeds remaining.
import java.util.*;
class Solution {
private List<List<Integer>> result;
public List<List<Integer>> combinationSum(int[] candidates, int target) {
result = new ArrayList<>();
Arrays.sort(candidates); // Sort for pruning
backtrack(candidates, target, 0, new ArrayList<>());
return result;
}
private void backtrack(int[] candidates, int remaining, int start,
List<Integer> current) {
if (remaining == 0) {
result.add(new ArrayList<>(current));
return;
}
for (int i = start; i < candidates.length; i++) {
// Pruning: if current candidate exceeds remaining,
// all later candidates will too (array is sorted)
if (candidates[i] > remaining) {
break; // No need to continue
}
current.add(candidates[i]);
backtrack(candidates, remaining - candidates[i], i, current);
current.remove(current.size() - 1);
}
}
}
Benefits: - Early termination when candidate > remaining - Fewer recursive calls - Same time complexity worst case, but faster in practice
๐ฏ Approach 3: Iterative (Using Stack) โญ
Alternative Solution
import java.util.*;
class Solution {
static class State {
List<Integer> combination;
int remaining;
int start;
State(List<Integer> combination, int remaining, int start) {
this.combination = new ArrayList<>(combination);
this.remaining = remaining;
this.start = start;
}
}
public List<List<Integer>> combinationSum(int[] candidates, int target) {
List<List<Integer>> result = new ArrayList<>();
Stack<State> stack = new Stack<>();
stack.push(new State(new ArrayList<>(), target, 0));
while (!stack.isEmpty()) {
State current = stack.pop();
// Found valid combination
if (current.remaining == 0) {
result.add(current.combination);
continue;
}
// Try each candidate from start index
for (int i = current.start; i < candidates.length; i++) {
int candidate = candidates[i];
// Skip if exceeds remaining
if (candidate > current.remaining) {
continue;
}
// Create new state with this candidate
List<Integer> newCombination = new ArrayList<>(current.combination);
newCombination.add(candidate);
stack.push(new State(
newCombination,
current.remaining - candidate,
i // Can reuse same candidate
));
}
}
return result;
}
}
โฐ Time: O(N^(T/M))
๐พ Space: O(N^(T/M)) - Stack can hold many states
โ ๏ธ Common Mistakes
Mistake 1: Not avoiding duplicates
// โ WRONG - Generates duplicates like [2,3] and [3,2]
backtrack(candidates, remaining - candidates[i], 0, current);
// Always starts from index 0!
// โ CORRECT - Pass current index to avoid going back
backtrack(candidates, remaining - candidates[i], i, current);
// Can reuse current candidate, but won't pick earlier ones
Mistake 2: Forgetting to copy list when adding to result
// โ WRONG - Adds reference, gets modified later!
if (remaining == 0) {
result.add(current); // Reference to same list!
return;
}
// โ CORRECT - Add a copy
if (remaining == 0) {
result.add(new ArrayList<>(current));
return;
}
Mistake 3: Not backtracking properly
// โ WRONG - Doesn't remove after exploring
current.add(candidates[i]);
backtrack(candidates, remaining - candidates[i], i, current);
// current still has the added element!
// โ CORRECT - Remove after exploring
current.add(candidates[i]);
backtrack(candidates, remaining - candidates[i], i, current);
current.remove(current.size() - 1); // Backtrack!
Mistake 4: Wrong index increment
// โ WRONG - Can't reuse same number
backtrack(candidates, remaining - candidates[i], i + 1, current);
// Moves to next candidate, can't reuse current one
// โ CORRECT - Pass i to allow reuse
backtrack(candidates, remaining - candidates[i], i, current);
Mistake 5: Not handling base case properly
// โ WRONG - Doesn't check if exceeded
if (remaining == 0) {
result.add(new ArrayList<>(current));
return;
}
// Will recurse with negative remaining!
// โ CORRECT - Check both conditions
if (remaining == 0) {
result.add(new ArrayList<>(current));
return;
}
if (remaining < 0) {
return; // Exceeded target
}
๐ฏ Pattern Recognition
Problem Type: Combination Generation with Reuse
Core Pattern: Backtracking + Index Tracking
When to Apply:
โ Find all combinations that meet criteria
โ Can reuse elements unlimited times
โ Need to avoid duplicate combinations
โ Target sum or goal to achieve
Recognition Keywords:
- "all combinations"
- "sum to target"
- "unlimited number of times"
- "unique combinations"
- "distinct integers"
Similar Problems:
- Combination Sum II (LC 40) - Can't reuse, has duplicates
- Combination Sum III (LC 216) - Fixed number of elements
- Combination Sum IV (LC 377) - Count combinations (DP)
- Coin Change (LC 322) - Minimum coins (DP)
Key Components:
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Backtracking: Explore all valid paths โ
โ Start index: Avoid duplicate combinations โ
โ Reuse: Pass same index (i not i+1) โ
โ Pruning: Break if candidate > remaining โ
โ Base case: remaining == 0 or < 0 โ
โ Time: O(N^(T/M)), Space: O(T/M) โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
๐ง Interview Strategy
Step 1: "This is a backtracking problem with unlimited reuse"
Step 2: "Need to find all combinations that sum to target"
Step 3: "Use start index to avoid duplicates like [2,3] vs [3,2]"
Key Points to Mention:
- Backtracking explores all valid combinations
- Start index ensures we don't generate duplicates
- Pass same index i (not i+1) to allow reuse
- Base cases: remaining == 0 (success), < 0 (fail)
- Can optimize with sorting and early break
Walk Through Example:
"For candidates=[2,3,6,7], target=7:
Start with empty []
Try 2: [2], remaining=5
Try 2 again: [2,2], remaining=3
Try 3: [2,2,3], remaining=0 โ Found! โ
Try 7: [7], remaining=0 โ Found! โ
Result: [[2,2,3], [7]]"
Why Start Index:
"Without start index, we'd generate:
[2,3] and [3,2] - these are same combination!
With start index i:
If we picked candidate[1]=3, next pick from index 1 onwards
This ensures we only get [2,3], never [3,2]"
Complexity:
"Time is O(N^(T/M)) where:
N = number of candidates
T = target value
M = minimum candidate
Worst case: all 1s summing to T
Space is O(T/M) for recursion depth"
Edge Cases to Mention:
โ Target = 0 โ [[]]
โ No valid combination โ []
โ Single candidate equals target โ [[target]]
โ Can reuse same number multiple times
๐ Quick Revision Notes
๐ฏ Core Concept:
Combination Sum: Use backtracking to find all combinations summing to target. Can reuse same number unlimited times. Use start index to avoid duplicates like [2,3] vs [3,2]. Pass same index i (not i+1) to allow reuse. Base case: remaining == 0.
โก Quick Implementation:
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Stack;
public class Solution {
static class State {
ArrayList<Integer> currList;
int remaining;
int currIndex;
public State(ArrayList<Integer> currList, int remaining, int currIndex) {
this.currList = currList;
this.remaining = remaining;
this.currIndex = currIndex;
}
}
public List<List<Integer>> combinationSum(int[] a, int k) {
// return recursive(a, k);
// return recursiveWithSort(a, k);
return iterative(a, k);
}
private List<List<Integer>> iterative(int[] a, int k) {
List<List<Integer>> res = new ArrayList<>();
// step 1: sort the array
Arrays.sort(a);
// step 2: initialize the current state of the stack
Stack<State> stack = new Stack<>();
stack.push(new State(new ArrayList<>(), k, 0));
// step 3: process till stack becomes empty
while (!stack.isEmpty()) {
State currState = stack.pop();
ArrayList<Integer> currList = currState.currList;
int remaining = currState.remaining;
int currIndex = currState.currIndex;
if (remaining == 0) {
res.add(new ArrayList<>(currList));
continue;
}
if (remaining < 0) {
continue;
}
for (int i = currIndex; i < a.length; i++) {
if (remaining - a[i] >= 0) {
currList.add(a[i]);
// GOTCHA: you need to create new list while pushing to stack (same as string)
stack.push(new State(new ArrayList<>(currList), remaining - a[i], i));
currList.remove(currList.size() - 1);
} else {
continue;
}
}
}
return res;
}
private List<List<Integer>> recursiveWithSort(int[] a, int k) {
List<List<Integer>> res = new ArrayList<>();
// step 1: sort the numbers
// why? refer step 3 of prev method
// You do not need to check every number once k < 0 if the
// numbers are sorted. Refer step 2.
// Note: all other steps are exactly same.
Arrays.sort(a);
recursiveWithSortUtil(a, k, 0, new ArrayList<>(), res);
return res;
}
private void recursiveWithSortUtil(int[] a, int k, int currIndex, ArrayList<Integer> currList,
List<List<Integer>> res) {
if (k == 0) {
res.add(new ArrayList<>(currList));
return;
}
if (k < 0) {
return;
}
// step 2: no need to proceed further to try other numbers
// remaining in the array if k - a[i] >= 0
for (int i = currIndex; i < a.length; i++) {
if (k - a[i] >= 0) {
currList.add(a[i]);
recursiveWithSortUtil(a, k - a[i], i, currList, res);
currList.remove(currList.size() - 1);
} else {
return;
}
}
}
private List<List<Integer>> recursive(int[] a, int k) {
List<List<Integer>> res = new ArrayList<>();
recursiveUtil(a, k, 0, new ArrayList<>(), res);
return res;
}
private void recursiveUtil(int[] a, int k, int currIndex, ArrayList<Integer> currList, List<List<Integer>> res) {
// step 5: base case 1
if (k == 0) {
res.add(new ArrayList<>(currList));
return;
}
// step 6: base case 2
if (k < 0) {
return;
}
// step 1: try the element at index currIndex
for (int i = currIndex; i < a.length; i++) {
// step 2: add it to the curr list which may becomes one of the result
currList.add(a[i]);
// step 3: try adding 1 more time the same element (a[i]) with k reduced
// if k becomes negative, you should not try this number at index i
// and you should try for next index which is i+1. See step 5 base case 2
recursiveUtil(a, k - a[i], i, currList, res);
// step 4: remove the added number from the curr list
currList.remove(currList.size() - 1);
}
}
public static void main(String[] args) {
Solution s = new Solution();
System.out.println(s.combinationSum(new int[] { 2, 3, 6, 7 }, 7)); // [[2,2,3],[7]]
}
}
๐ Key Insights:
- Pattern: Backtracking + Start Index
- Reuse: Pass i not i+1 (allows same number again)
- No Duplicates: Start index ensures [2,3] found, not [3,2]
- Base Case: remaining == 0 (success), < 0 (fail)
- Optimization: Sort + break when candidate > remaining
- Time: O(N^(T/M)), Space: O(T/M) โ
๐ช Memory Aid:
"Try each coin, can reuse same, track from where you came!"
"Pass i not i+1, avoid going back, sum to zero done!" โจ
โ ๏ธ Critical Difference:
ALLOWING REUSE:
backtrack(candidates, remaining - candidates[i], i, current)
Pass i โ Can pick candidates[i] again
Example: [2] โ can try 2 again โ [2,2]
NOT ALLOWING REUSE:
backtrack(candidates, remaining - candidates[i], i+1, current)
Pass i+1 โ Must pick next candidate
Example: [2] โ must try 3,6,7 next, not 2
This problem: ALLOWS REUSE โ Pass i โ
๐งช Edge Cases
Case 1: Single candidate equals target
Input: candidates = [7], target = 7
Output: [[7]]
Direct match
Case 2: No valid combination
Input: candidates = [2], target = 1
Output: []
Can't make 1 with 2
Case 3: Multiple reuse needed
Input: candidates = [2,3,5], target = 8
Output: [[2,2,2,2],[2,3,3],[3,5]]
[2,2,2,2] uses 2 four times!
Case 4: Target = smallest candidate
Input: candidates = [2,3,6,7], target = 2
Output: [[2]]
Only one way
All handled correctly! โ
๐ Complexity Analysis
Time Complexity: O(N^(T/M))
Where:
N = number of candidates
T = target value
M = minimum candidate value
Why?
Maximum depth of recursion tree: T/M
(If we always pick smallest candidate)
At each level, we branch N ways
Total nodes: N^(T/M)
Example:
candidates = [2,3], target = 8
Min candidate M = 2
Max depth = 8/2 = 4
Branches = 2 (candidates count)
Nodes โ 2^4 = 16
Worst case:
candidates = [1], target = 100
Depth = 100
Extremely slow!
Space Complexity: O(T/M)
Recursion stack depth:
Maximum depth = T/M
(Building combination one element at a time)
Each call: O(1) space
Total: O(T/M)
Example:
target = 8, min candidate = 2
Max depth = 4
Space = O(4)
๐ Related Problems
Same Core Pattern: - Combination Sum II (LC 40) - Each number used once, has duplicates - Combination Sum III (LC 216) - Fixed k numbers, use 1-9 - Combination Sum IV (LC 377) - Count ways (DP, order matters)
Similar Backtracking: - Permutations (LC 46) - All permutations - Subsets (LC 78) - All subsets - Letter Combinations (LC 17) - Phone number mapping
Related DP: - Coin Change (LC 322) - Minimum coins to make amount - Coin Change 2 (LC 518) - Number of ways to make amount
Happy practicing! ๐ฏ
Note: This is a classic backtracking problem! Master this and you understand: (1) combination generation, (2) avoiding duplicates with start index, (3) allowing element reuse, (4) pruning for optimization. Very common at FAANG! The "start index" technique is crucial - it appears in many problems! ๐ชโจ