Problem overview
The method CombinationSum receives candidates, an int[] of distinct numbers, and an int target. It returns every combination of candidates that adds up to target, as a List<List<int>>. A number can be used any number of times. Two combinations are the same when they use the same numbers the same number of times, so [2, 2, 3] and [3, 2, 2] count as one and must 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 list.
For candidates = [2, 3, 6, 7] and target = 7, the answer is [[2, 2, 3], [7]]: 2 + 2 + 3 = 7 and 7 alone is 7. For candidates = [5, 2, 3] and target = 8, the answer is [[2, 2, 2, 2], [2, 3, 3], [3, 5]], even though the input isn't sorted. For candidates = [2] and target = 1, the answer is [].
Approach: build a combination one number at a time
This is a search over choices. A partial combination has a total, and the amount remaining is target minus that total. At each step the search picks the next number to add, goes deeper with a smaller remaining amount, then removes the number again and tries the next one. Taking a choice back so another can be tried is the "back" in backtracking. When remaining reaches exactly 0, the combination is complete and gets saved.
Two ideas keep the output correct without any duplicate removal:
- Sort the candidates, and only move forward. A call may only choose candidates from index
ionward, and the next call starts at the index just chosen, not at 0. Starting at the same index allows a number to repeat. Never going back means a combination is built only in ascending order, so[2, 2, 3]can be found but[3, 2, 2]can't, and no combination is built twice. - The order comes out lexicographic. With the candidates sorted and the loop trying them in order, smaller first numbers are explored before larger ones, so the combinations are found in exactly the order the answer needs.
Sorting also allows a cut-off. Once a candidate is larger than what remains, every later candidate is larger still, so the loop can stop with break instead of testing them.
Step by step
CombinationSum: set up and start
- Sort
candidateswithArray.Sort, which sorts the array in place in ascending order. - Create
result, an emptyList<List<int>>. Each combination is itself a list. - Start the search with
Backtrack(candidates, target, 0, new List<int>(), result). Nothing is chosen yet, all oftargetremains, and every candidate from index 0 may be used. - Return
result, which the helper has filled.
Backtrack: add one number and recurse
- Declare
Backtrackas apublic voidmethod withcandidates,remaining,i(the first index it may use),current(the combination so far) andresult. - Test
remaining == 0. Then the numbers incurrentadd up to exactly the target. - Save a copy with
result.Add(new List<int>(current)), thenreturn. A copy is needed becausecurrentkeeps changing as the search goes on. Every candidate is at least 2, so adding more numbers would overshoot anyway. - Loop with
jfromiwhilej < candidates.Length. These are the numbers the combination may take next. - Stop at a number that is too big: if
candidates[j] > remaining,break. - Choose
candidates[j]by adding it to the end ofcurrent. - Recurse with
remaining - candidates[j]andjas the first index. Passingjrather thanj + 1lets the same number be taken again. - Undo the choice with
current.RemoveAt(current.Count - 1), socurrentis what it was before this turn of the loop.
The solution in C#
class Solution {
public List<List<int>> CombinationSum(int[] candidates, int target) {
Array.Sort(candidates);
List<List<int>> result = new List<List<int>>();
Backtrack(candidates, target, 0, new List<int>(), result);
return result;
}
public void Backtrack(int[] candidates, int remaining, int i, List<int> current, List<List<int>> result) {
if (remaining == 0) {
result.Add(new List<int>(current));
return;
}
for (int j = i; j < candidates.Length; j++) {
if (candidates[j] > remaining) {
break;
}
current.Add(candidates[j]);
Backtrack(candidates, remaining - candidates[j], j, current, result);
current.RemoveAt(current.Count - 1);
}
}
}
The C# details worth knowing:
- Lists are references. If the code wrote
result.Add(current), every entry ofresultwould be the same list object, and at the end all of them would be empty, because the undo steps emptycurrent.new List<int>(current)is the copy constructor: it builds a separate list with the same numbers. Array.Sortchanges the caller's array. It sortscandidatesin place and returns nothing. After the call, the caller's array is sorted too.RemoveAttakes an index.current.RemoveAt(current.Count - 1)removes the last element.current.Remove(x)would remove the first element equal tox, which could be a different one when numbers repeat.- Nested generics.
List<List<int>>is a list whose elements are lists ofint. Each inner list can have a different length. - The helper is shared state. One
currentlist and oneresultlist are passed through all the calls. That is why the undo step matters: all the calls sharecurrent.
Complexity
Let n be the number of candidates, t the target and m the smallest candidate.
Time: exponential, about O(n^(t/m)) in the worst case. A combination has at most t / m numbers, because each is at least m, and each position is one of n candidates. The break and the forward-only index prune much of that, but the bound for the worst case stays exponential. Copying each finished combination also costs time proportional to its length.
Space: O(t / m) besides the answer. The combination being built and the call stack are both at most t / m long.
Edge cases and common mistakes
- No combination. With
candidates = [2]andtarget = 1, the only candidate is already too big, the loop breaks at once, and the result is[]. - Unsorted input. Without the sort, the
breakwould skip valid candidates that come after a big one, and the combinations wouldn't be in ascending order. - Starting the loop at 0 in every call. This builds
[2, 2, 3],[2, 3, 2]and[3, 2, 2]as separate answers. - Passing
j + 1. This forbids using a number twice, which solves a different problem. For[2, 3, 6, 7]and target 7, it would lose[2, 2, 3]. - Forgetting the undo.
currentgrows with every choice, and later combinations contain leftovers from earlier ones. - Using
continueinstead ofbreak. It gives the same answer, but the loop keeps testing numbers that can't fit.
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, 3, 5],target = 8gives[[2,2,2,2],[2,3,3],[3,5]].candidates = [2],target = 1gives[].
