Problem overview
The method combinationSum receives candidates, a vector of distinct integers, and a target. It returns every combination of candidates that adds up to target. A candidate may be used any number of times. Two combinations are the same if they use the same numbers the same number of times, in any order, so [2, 2, 3] and [3, 2, 2] are one combination and appear once.
Each combination is written in ascending order, and the combinations are returned in lexicographic order: compare number by number, and the first difference decides. If nothing adds up to target, the result is an empty vector.
With candidates = [2, 3, 6, 7] and target = 7 the answer is [[2, 2, 3], [7]]. With the unsorted [5, 2, 3] and target 8 it is [[2, 2, 2, 2], [2, 3, 3], [3, 5]]. With [2] and target 1 it is empty, because the only candidate is bigger than the target.
Approach: build combinations one number at a time
This is a search over choices, and backtracking fits it. Build a combination by adding one number at a time. After each addition, ask the same question again with a smaller target, which is the amount still needed. When that amount reaches exactly 0, the numbers chosen so far are a valid combination. After exploring a choice, remove the number and try the next candidate in its place.
Two ideas keep the output correct and small:
- Sort the candidates, and only look forward. A call receives an index
iand may use only candidates fromion. Choosingcandidates[j]passesj, notj + 1, to the next call, so the same number can be picked again, but a smaller one never can. Every combination is therefore built in ascending order, exactly once:[2, 2, 3]is found and[3, 2, 2]never is. - Stop early. Because the candidates are sorted, as soon as one is bigger than the remaining amount, all the later ones are too, so the loop can
break.
Trying candidates in increasing order and going deeper before moving on also produces the combinations in lexicographic order, which is the order the answer needs.
The search is exponential. With n candidates, target t and smallest candidate m, a combination has at most t / m numbers, each one of n candidates, so the time is O(n^(t/m)) in the worst case. The stopping rule prunes much of that.
Step by step
- Sort
candidatesin place withsort(candidates.begin(), candidates.end()). This gives the ascending combinations, the right order of results, and the early stop. - Create
result, avector<vector<int>>, empty, to collect the combinations. - Create
current, avector<int>, empty: the combination being built. - Start the search by calling
backtrackwithtargetas the amount still needed and 0 as the first usable index. - Return
result.backtrackfilled the same vector. - Write
backtrackwith the parametersremainingandinext to the vectors, all vectors taken by reference. It returns nothing and adds toresultinstead. - Detect a full combination: if
remaining == 0, the numbers incurrentadd up totarget. - Record it with
push_back(current)andreturn. Every candidate is at least 2, so adding more could only overshoot. - Loop over the candidates from
j = ito the end: those the combination may take next. - Break when too big: if
candidates[j] > remaining, no later candidate fits either. - Choose:
push_back(candidates[j])ontocurrent. - Explore: call
backtrackwithremaining - candidates[j]andjas the first index. - Undo:
pop_back()removes the number just chosen, socurrentis what it was before this turn of the loop.
The solution in C++
class Solution {
public:
vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
sort(candidates.begin(), candidates.end());
vector<vector<int>> result;
vector<int> current;
backtrack(candidates, target, 0, current, result);
return result;
}
void backtrack(vector<int>& candidates, int remaining, int i, vector<int>& current, vector<vector<int>>& result) {
if (remaining == 0) {
result.push_back(current);
return;
}
for (int j = i; j < candidates.size(); j++) {
if (candidates[j] > remaining) {
break;
}
current.push_back(candidates[j]);
backtrack(candidates, remaining - candidates[j], j, current, result);
current.pop_back();
}
}
};
C++ details worth a closer look:
- References.
backtracktakescurrentandresultby reference, so every call works on the same two vectors. The caller sees the changes, and no vector is copied per call. push_back(current)copies. The vector insideresultis a copy ofcurrentat that moment. That is why laterpush_backandpop_backcalls oncurrentdo not change the stored combinations.sortchanges the caller's vector.candidatesis a non-const reference, so the vector passed in comes back sorted. For this lesson that is fine.- Unsigned bound.
j < candidates.size()compares anintwith an unsigned value. It is safe becausejnever goes below 0. - Pop after every push. Each loop turn pushes one number and pops it again, so
currentis restored even after a deep search.
Complexity
Time: O(n^(t/m)) in the worst case, where n is the number of candidates, t the target and m the smallest candidate. A combination holds at most t / m numbers, and each position has up to n choices. The break and the forward-only index cut this down, but the bound is exponential. Copying each answer into result costs extra time proportional to its length. The sort adds O(n log n), which is small next to the search.
Space: O(t / m) besides the output. current holds at most t / m numbers, and the recursion is at most that many calls deep.
Edge cases and common mistakes
- No combination.
[2]with target 1 returns[]: the loop breaks at once and nothing is recorded.[2]with target 3 gives[]too, because the search reaches a remaining amount of 1 and breaks. - Using the same number more than once.
[2]with target 4 returns[[2, 2]]. Passingj + 1instead ofjwould forbid that. - Starting every loop at 0. The search would produce
[2, 3]and[3, 2]as separate answers. Starting atilists each combination once. - Unsorted input. Without the sort,
[5, 2, 3]would give combinations in the wrong order, and the earlybreakwould stop at the 5 and miss answers. The sort is part of the algorithm. - Skipping the undo. Without
pop_back(),currentkeeps growing and every later answer contains leftovers from earlier branches. - Checking the sum late. Passing the remaining amount down, instead of summing
currentat every call, keeps each step a constant-time comparison. - Target equal to a candidate. The single-number combination counts: for
[2, 3, 6, 7]and 7,[7]is an answer.
Testing
candidates = [2, 3, 6, 7],target = 7gives[[2, 2, 3], [7]].candidates = [5, 2, 3],target = 8gives[[2, 2, 2, 2], [2, 3, 3], [3, 5]].candidates = [2],target = 1gives[].candidates = [2, 3, 5],target = 8gives[[2, 2, 2, 2], [2, 3, 3], [3, 5]].candidates = [7, 3, 2],target = 3gives[[3]].candidates = [2],target = 4gives[[2, 2]].
