39. Combination Sum
Problem Description
You are given an array of distinct integers called candidates
and a target integer target
. Your task is to find all unique combinations of numbers from candidates
that sum up to target
.
Key points about this problem:
-
Numbers can be reused: You can use the same number from
candidates
multiple times in a single combination. For example, ifcandidates = [2, 3, 5]
andtarget = 8
, then[2, 2, 2, 2]
is a valid combination. -
All numbers are distinct: The
candidates
array contains no duplicate values. -
Unique combinations: Two combinations are considered different if at least one number appears a different number of times. For example,
[2, 2, 3]
and[2, 3, 3]
are different combinations. -
Order doesn't matter:
[2, 3]
and[3, 2]
are considered the same combination, so you should only include one of them in your result. -
Return format: Return a list of lists, where each inner list represents one valid combination. The combinations can be returned in any order.
For example:
- If
candidates = [2, 3, 6, 7]
andtarget = 7
, the output would be[[2, 2, 3], [7]]
[2, 2, 3]
sums to 7 (using 2 twice and 3 once)[7]
sums to 7 (using 7 once)
The problem guarantees that the number of unique combinations will be less than 150 for any given input.
Flowchart Walkthrough
First, let's pin down the algorithm using the Flowchart. Here's a step-by-step walkthrough:
Is it a graph?
- No: The problem involves finding combinations from an array that sum to a target value. This is not about nodes and edges or graph traversal.
Need to solve for kth smallest/largest?
- No: We're not looking for the kth smallest or largest element. We need to find all valid combinations that sum to the target.
Involves Linked Lists?
- No: The problem works with an array of integers, not linked lists.
Does the problem have small constraints?
- Yes: The problem statement explicitly mentions that the number of unique combinations is less than 150 for any given input. This is a relatively small constraint, suggesting that exploring all possibilities is feasible.
Brute force / Backtracking?
- Yes: Since we need to find all possible combinations and the constraints are small, backtracking is the appropriate approach. We need to explore different combinations by:
- Including an element (potentially multiple times)
- Backtracking when the sum exceeds the target
- Recording valid combinations when the sum equals the target
- Trying different elements at each decision point
Conclusion: The flowchart correctly leads us to use a Backtracking approach. This makes sense because we need to systematically explore all possible combinations of numbers that can sum to the target, with the ability to reuse numbers and backtrack when a path doesn't lead to a valid solution.
Intuition
Think of this problem like making change with coins, but you have unlimited coins of each denomination and need to find all possible ways to make the exact amount.
The key insight is that at each step, we face a decision: which number should we try next? Since we can reuse numbers, after picking a number, we have the same set of choices available again. This naturally leads to a recursive approach where we:
- Pick a number from the available candidates
- Subtract it from our remaining target
- Recursively solve for the new remaining target
The beauty of backtracking here is that it allows us to explore all paths systematically. When we pick a number like 2
from [2, 3, 6, 7]
to reach target 7
, we continue using 2
until either:
- We reach exactly
7
(found a valid combination!) - We exceed
7
(this path won't work, backtrack) - We realize we can't reach
7
with the current path
To avoid duplicate combinations like [2, 3]
and [3, 2]
, we use a clever trick: always pick numbers in non-decreasing order. We do this by maintaining an index i
that tells us the minimum position in the candidates array we can pick from. If we picked candidates[3]
, our next pick must be from index 3
or later, never from indices 0
, 1
, or 2
.
Sorting the candidates array first gives us an additional optimization. If the smallest available candidate is already larger than our remaining target, we know that all subsequent candidates will also be too large (since the array is sorted). This allows us to prune entire branches of our search tree early, significantly improving performance.
The recursive nature of the problem becomes clear when you realize that after picking a number and subtracting it from the target, you're left with the exact same type of problem - just with a smaller target value. This self-similar structure is what makes backtracking with recursion such an elegant solution.
Learn more about Backtracking patterns.
Solution Approach
The implementation uses a Depth-First Search (DFS) with backtracking approach. Let's walk through the key components:
1. Sorting for Optimization
candidates.sort()
We first sort the candidates array. This enables early termination when we encounter a candidate larger than the remaining target, as all subsequent candidates will also be too large.
2. DFS Function Design
The core logic is in the dfs(i, s)
function where:
i
: The starting index in the candidates array (ensures we don't create duplicate combinations)s
: The remaining sum we need to reach
3. Base Cases
if s == 0: ans.append(t[:]) return if s < candidates[i]: return
- Success case: When
s == 0
, we've found a valid combination. We add a copy of the current patht[:]
to our answer. - Pruning case: When
s < candidates[i]
, the smallest available candidate is too large, so we can stop exploring this branch.
4. Backtracking Logic
for j in range(i, len(candidates)):
t.append(candidates[j])
dfs(j, s - candidates[j])
t.pop()
This is the heart of the backtracking algorithm:
- Iterate from index
i
: This ensures we only consider candidates at positioni
or later, preventing duplicates - Make a choice: Add
candidates[j]
to our current patht
- Explore: Recursively call
dfs(j, s - candidates[j])
. Note we passj
(notj+1
) because we can reuse the same number - Backtrack: Remove the last element with
t.pop()
to try other possibilities
5. Data Structures
t = []
: A temporary list that maintains the current combination being builtans = []
: Stores all valid combinations found
Why This Works
The algorithm systematically explores all possible combinations:
- For each candidate, it tries using it 0, 1, 2, ... times until the sum exceeds the target
- By starting each recursive call from index
i
or later, we ensure combinations like[2,3]
and[3,2]
aren't both generated - The sorting optimization allows us to stop early when we know no valid combinations can be formed
Time Complexity Analysis
- In the worst case, we might explore
O(2^n)
combinations (where each element can be included or not) - For each valid combination, we need
O(n)
time to copy it to the answer - Overall:
O(2^n × n)
, though pruning significantly reduces the actual runtime
Space Complexity
- The recursion depth can go up to
target/min(candidates)
in the worst case - We also store the temporary path
t
which can have at mosttarget/min(candidates)
elements - Overall:
O(n)
wheren
is the maximum depth of recursion
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's trace through the algorithm with candidates = [2, 3, 5]
and target = 8
.
Initial Setup:
- Sort candidates:
[2, 3, 5]
(already sorted) - Start with empty path
t = []
and calldfs(0, 8)
Execution Tree:
dfs(0, 8) - Can use candidates from index 0 onwards ├─ Choose 2: t = [2], dfs(0, 6) │ ├─ Choose 2: t = [2,2], dfs(0, 4) │ │ ├─ Choose 2: t = [2,2,2], dfs(0, 2) │ │ │ ├─ Choose 2: t = [2,2,2,2], dfs(0, 0) ✓ Found [2,2,2,2] │ │ │ ├─ Choose 3: t = [2,2,2,3], dfs(1, -1) ✗ (negative sum) │ │ │ └─ Choose 5: t = [2,2,2,5], dfs(2, -3) ✗ (negative sum) │ │ ├─ Choose 3: t = [2,2,3], dfs(1, 1) │ │ │ └─ 1 < 3, return ✗ (remaining sum too small) │ │ └─ Choose 5: t = [2,2,5], dfs(2, -1) ✗ (negative sum) │ ├─ Choose 3: t = [2,3], dfs(1, 3) │ │ ├─ Choose 3: t = [2,3,3], dfs(1, 0) ✓ Found [2,3,3] │ │ └─ Choose 5: t = [2,3,5], dfs(2, -2) ✗ (negative sum) │ └─ Choose 5: t = [2,5], dfs(2, 1) │ └─ 1 < 5, return ✗ (remaining sum too small) ├─ Choose 3: t = [3], dfs(1, 5) │ ├─ Choose 3: t = [3,3], dfs(1, 2) │ │ └─ 2 < 3, return ✗ (remaining sum too small) │ └─ Choose 5: t = [3,5], dfs(2, 0) ✓ Found [3,5] └─ Choose 5: t = [5], dfs(2, 3) └─ 3 < 5, return ✗ (remaining sum too small)
Step-by-step for finding [2,2,2,2]:
- Start with
dfs(0, 8)
, choosecandidates[0] = 2
, add to path:t = [2]
- Call
dfs(0, 6)
, choosecandidates[0] = 2
again, add to path:t = [2,2]
- Call
dfs(0, 4)
, choosecandidates[0] = 2
again, add to path:t = [2,2,2]
- Call
dfs(0, 2)
, choosecandidates[0] = 2
again, add to path:t = [2,2,2,2]
- Call
dfs(0, 0)
, sum is 0 - we found a valid combination! Add[2,2,2,2]
to answer - Backtrack by popping elements and trying other branches
Key Observations:
- When we choose index
j
, the next recursive call usesj
(notj+1
) as the starting index, allowing number reuse - The algorithm avoids duplicates like
[3,2,3]
because once we choose3
at index 1, we can't go back to choose2
at index 0 - Early termination happens when remaining sum is less than the smallest available candidate
Final Result: [[2,2,2,2], [2,3,3], [3,5]]
Solution Implementation
1class Solution:
2 def combinationSum(self, candidates: List[int], target: int) -> List[List[int]]:
3 """
4 Find all unique combinations of candidates that sum to target.
5 Each candidate can be used unlimited times.
6
7 Args:
8 candidates: List of distinct positive integers
9 target: Target sum to achieve
10
11 Returns:
12 List of all unique combinations that sum to target
13 """
14
15 def backtrack(start_index: int, remaining_sum: int) -> None:
16 """
17 Recursive helper function to explore all valid combinations.
18
19 Args:
20 start_index: Current index in candidates array to avoid duplicates
21 remaining_sum: Remaining sum needed to reach target
22 """
23 # Base case: found a valid combination
24 if remaining_sum == 0:
25 result.append(current_combination[:])
26 return
27
28 # Pruning: if remaining sum is less than smallest available candidate
29 if remaining_sum < candidates[start_index]:
30 return
31
32 # Try each candidate from start_index onwards
33 for index in range(start_index, len(candidates)):
34 # Include current candidate in the combination
35 current_combination.append(candidates[index])
36
37 # Recursively explore with same index (allowing reuse)
38 backtrack(index, remaining_sum - candidates[index])
39
40 # Backtrack: remove the last added candidate
41 current_combination.pop()
42
43 # Sort candidates for optimization (early termination)
44 candidates.sort()
45
46 # Initialize variables
47 current_combination = [] # Tracks current combination being built
48 result = [] # Stores all valid combinations
49
50 # Start the backtracking process
51 backtrack(0, target)
52
53 return result
54
1class Solution {
2 // Store all valid combinations that sum to target
3 private List<List<Integer>> result = new ArrayList<>();
4 // Store current combination being built
5 private List<Integer> currentCombination = new ArrayList<>();
6 // Store the candidate numbers array
7 private int[] candidates;
8
9 /**
10 * Find all unique combinations where the candidate numbers sum to target.
11 * The same number may be chosen from candidates an unlimited number of times.
12 *
13 * @param candidates Array of distinct integers
14 * @param target Target sum to achieve
15 * @return List of all unique combinations that sum to target
16 */
17 public List<List<Integer>> combinationSum(int[] candidates, int target) {
18 // Sort candidates to enable early termination in DFS
19 Arrays.sort(candidates);
20 this.candidates = candidates;
21
22 // Start DFS from index 0 with the initial target
23 dfs(0, target);
24
25 return result;
26 }
27
28 /**
29 * Depth-first search to find all combinations starting from index startIndex
30 *
31 * @param startIndex Starting index in candidates array to avoid duplicates
32 * @param remainingTarget Remaining sum needed to reach the target
33 */
34 private void dfs(int startIndex, int remainingTarget) {
35 // Base case: found a valid combination
36 if (remainingTarget == 0) {
37 result.add(new ArrayList<>(currentCombination));
38 return;
39 }
40
41 // Early termination: remaining target is less than smallest available candidate
42 if (remainingTarget < candidates[startIndex]) {
43 return;
44 }
45
46 // Try each candidate starting from startIndex
47 for (int i = startIndex; i < candidates.length; ++i) {
48 // Add current candidate to the combination
49 currentCombination.add(candidates[i]);
50
51 // Recursively search with the same starting index (allowing reuse)
52 // and reduced target
53 dfs(i, remainingTarget - candidates[i]);
54
55 // Backtrack: remove the last added candidate
56 currentCombination.remove(currentCombination.size() - 1);
57 }
58 }
59}
60
1class Solution {
2public:
3 vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
4 // Sort candidates in ascending order for optimization
5 sort(candidates.begin(), candidates.end());
6
7 // Store all valid combinations
8 vector<vector<int>> result;
9
10 // Current combination being built
11 vector<int> currentCombination;
12
13 // Define recursive backtracking function using lambda
14 // Parameters: startIndex - current position in candidates array
15 // remainingSum - remaining target value to achieve
16 function<void(int, int)> backtrack = [&](int startIndex, int remainingSum) {
17 // Base case: found a valid combination
18 if (remainingSum == 0) {
19 result.emplace_back(currentCombination);
20 return;
21 }
22
23 // Pruning: remaining sum is less than smallest available candidate
24 if (remainingSum < candidates[startIndex]) {
25 return;
26 }
27
28 // Try each candidate starting from current index
29 // Allow reuse by starting from same index (j) in recursive call
30 for (int j = startIndex; j < candidates.size(); ++j) {
31 // Choose current candidate
32 currentCombination.push_back(candidates[j]);
33
34 // Explore with this choice (allow reuse by passing j, not j+1)
35 backtrack(j, remainingSum - candidates[j]);
36
37 // Backtrack: remove current candidate for next iteration
38 currentCombination.pop_back();
39 }
40 };
41
42 // Start backtracking from index 0 with full target sum
43 backtrack(0, target);
44
45 return result;
46 }
47};
48
1/**
2 * Finds all unique combinations in candidates where the numbers sum to target.
3 * Each number in candidates may be used unlimited times in the combination.
4 * @param candidates - Array of distinct positive integers
5 * @param target - Target sum to achieve
6 * @returns Array of arrays containing all unique combinations that sum to target
7 */
8function combinationSum(candidates: number[], target: number): number[][] {
9 // Sort candidates in ascending order for optimization
10 candidates.sort((a: number, b: number) => a - b);
11
12 // Store all valid combinations
13 const result: number[][] = [];
14
15 // Current combination being built
16 const currentCombination: number[] = [];
17
18 /**
19 * Depth-first search to explore all possible combinations
20 * @param startIndex - Starting index in candidates array to avoid duplicates
21 * @param remainingSum - Remaining sum needed to reach target
22 */
23 const depthFirstSearch = (startIndex: number, remainingSum: number): void => {
24 // Base case: found a valid combination
25 if (remainingSum === 0) {
26 // Add a copy of current combination to results
27 result.push([...currentCombination]);
28 return;
29 }
30
31 // Pruning: if remaining sum is less than smallest available candidate
32 if (remainingSum < candidates[startIndex]) {
33 return;
34 }
35
36 // Try each candidate starting from startIndex
37 for (let index: number = startIndex; index < candidates.length; index++) {
38 // Add current candidate to combination
39 currentCombination.push(candidates[index]);
40
41 // Recursively search with same index (allowing reuse) and reduced sum
42 depthFirstSearch(index, remainingSum - candidates[index]);
43
44 // Backtrack: remove the last added candidate
45 currentCombination.pop();
46 }
47 };
48
49 // Start the search from index 0 with full target sum
50 depthFirstSearch(0, target);
51
52 return result;
53}
54
Time and Space Complexity
Time Complexity: O(N^(T/M))
where N
is the number of candidates, T
is the target value, and M
is the minimal value among the candidates.
The analysis breaks down as follows:
- In the worst case, the recursion tree can have a maximum depth of
T/M
(when we keep choosing the smallest element repeatedly until we reach the target). - At each level of recursion, we can branch out to at most
N
different candidates (the for loop iterates through all candidates from indexi
to the end). - The number of nodes in the recursion tree represents the number of recursive calls, which is bounded by
N^(T/M)
. - At each leaf node where we find a valid combination (
s == 0
), we append a copy of the current path to the answer, which takesO(T/M)
time in the worst case. - However, the number of valid combinations is typically much smaller than the total number of nodes, so the dominant factor remains
O(N^(T/M))
.
Space Complexity: O(T/M)
The space complexity consists of:
- Recursion stack depth: The maximum depth of the recursion is
T/M
(when we repeatedly choose the minimum value), requiringO(T/M)
space for the call stack. - Temporary list
t
: This list stores the current combination being built and can have at mostT/M
elements, requiringO(T/M)
space. - Output space: The space for storing the final answer
ans
is not counted as part of the auxiliary space complexity as it's required for the output.
Note that sorting the candidates takes O(N log N)
time but only O(1)
or O(log N)
auxiliary space depending on the sorting algorithm used, which doesn't affect the overall space complexity dominated by the recursion depth.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Creating Duplicate Combinations by Not Maintaining Start Index
The Pitfall: A common mistake is to iterate through ALL candidates in each recursive call instead of starting from the current index. This leads to duplicate combinations in different orders.
Incorrect Implementation:
def backtrack(remaining_sum):
if remaining_sum == 0:
result.append(current_combination[:])
return
# WRONG: Starting from 0 every time creates duplicates
for i in range(len(candidates)):
if candidates[i] <= remaining_sum:
current_combination.append(candidates[i])
backtrack(remaining_sum - candidates[i])
current_combination.pop()
Why it's wrong: This would generate both [2, 3]
and [3, 2]
as separate combinations when target = 5
and candidates = [2, 3]
.
The Fix:
Always pass and use a start_index
parameter to ensure you only consider candidates from the current position onwards:
def backtrack(start_index, remaining_sum):
# ... base cases ...
# Correct: Start from start_index to avoid duplicates
for i in range(start_index, len(candidates)):
current_combination.append(candidates[i])
backtrack(i, remaining_sum - candidates[i]) # Pass i, not i+1
current_combination.pop()
2. Forgetting to Copy the List When Adding to Results
The Pitfall:
Appending the reference to current_combination
instead of a copy means all results will point to the same list object, which gets modified throughout the recursion.
Incorrect Implementation:
if remaining_sum == 0: result.append(current_combination) # WRONG: Appends reference return
Why it's wrong: Since current_combination
is modified during backtracking, all entries in result
will end up being empty lists or containing the same final state.
The Fix: Always create a copy of the current combination when adding it to results:
if remaining_sum == 0: result.append(current_combination[:]) # Correct: Creates a copy # Or use: result.append(list(current_combination)) return
3. Incorrect Pruning Logic After Sorting
The Pitfall:
When implementing the optimization with sorted candidates, accessing candidates[start_index]
without checking bounds can cause an IndexError.
Incorrect Implementation:
def backtrack(start_index, remaining_sum):
if remaining_sum == 0:
result.append(current_combination[:])
return
# WRONG: No bounds checking before accessing candidates[start_index]
if remaining_sum < candidates[start_index]:
return
Why it's wrong: If start_index >= len(candidates)
, this will throw an IndexError.
The Fix: Add proper bounds checking:
def backtrack(start_index, remaining_sum):
if remaining_sum == 0:
result.append(current_combination[:])
return
# Correct: Check bounds first
if start_index >= len(candidates) or remaining_sum < candidates[start_index]:
return
4. Using i+1 Instead of i in Recursive Call
The Pitfall:
Passing i+1
instead of i
to the recursive call prevents reusing the same number multiple times.
Incorrect Implementation:
for i in range(start_index, len(candidates)):
current_combination.append(candidates[i])
backtrack(i + 1, remaining_sum - candidates[i]) # WRONG: Can't reuse numbers
current_combination.pop()
Why it's wrong: This would make it impossible to generate combinations like [2, 2, 3]
where a number appears multiple times.
The Fix:
Pass i
(not i+1
) to allow reusing the same candidate:
for i in range(start_index, len(candidates)):
current_combination.append(candidates[i])
backtrack(i, remaining_sum - candidates[i]) # Correct: Allows reuse
current_combination.pop()
Which of these pictures shows the visit order of a depth-first search?
Recommended Readings
Backtracking Template Prereq DFS with States problems dfs_with_states Combinatorial search problems Combinatorial search problems involve finding groupings and assignments of objects that satisfy certain conditions Finding all permutations combinations subsets and solving Sudoku are classic combinatorial problems The time complexity of combinatorial problems often grows rapidly with the size of
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask
Want a Structured Path to Master System Design Too? Don’t Miss This!