Backtracking · Choose, explore, undo
candidates and a positive integer target. Return every unique combination of candidates that sums to target. You may use the same number as many times as you like. Two combinations are the same if they use each number the same number of times, so [2, 3] and [3, 2] count once. The combinations can come back in any order.
You have a list of numbers and a target. Find every way to add numbers from the list to reach the target. You may use the same number as many times as you like. For [2, 3, 5] and a target of 7, the answers are 2 + 2 + 3 and 2 + 5.
[2, 2, 3] and [3, 2, 2] are the same combination, and must appear once.Because we must return every answer, we cannot avoid trying every option, so this is a search. At each step we make one decision: which number do we add next? Each decision shrinks the amount we still need. Here are the first decisions for [2, 3, 5] and target 7. The small number under each circle is what is still needed.
Each branch is one decision, and a path from the top is a partly built answer. It is finished when nothing is needed any more (the remaining amount reaches 0), and it is dead when the remaining amount would go below 0.
The direct idea: at every step try every number, stop when the sum reaches the target, and remove the repeats afterwards by sorting each answer and keeping it in a set.
It is correct, but it does the same work again and again. The combination {2, 2, 3} is reached as [2, 2, 3], as [2, 3, 2] and as [3, 2, 2], three different paths for one answer. With more numbers, the same answer is reached in many orders, and we throw all but one away at the end.
We should never build the same combination in two orders. How can we make sure each combination is built in only one order?
Add a rule: numbers are added in the order they appear in the list. Once we have chosen a number, we may choose the same number again, or one that comes later in the list, but never one that comes earlier. We keep an index start that remembers "the earliest number we are still allowed to pick". With that rule, [2, 2, 3] can only be built as 2, then 2, then 3. The other orders would need a step backwards, and start forbids that.
One more trick: sort the list first. Then, once a number is too big for what we still need, every number after it is too big as well, so we can stop the loop with break, instead of checking each one.
This is backtracking: choose a number, explore what comes after, then undo the choice (remove the number) and try the next one.
path and call dfs(0, target).dfs(start, remaining): remaining is 0⟶save a copy of the path and returni from start: candidates[i] is more than remaining⟶stop the loop (all later ones are bigger too)dfs(i, remaining − number). Use i, not i + 1, because the same number may be used again.By hand, with [2, 3, 5] and target 7:
The answer is [[2, 2, 3], [2, 5]]. Press Play to watch the path grow and shrink.
start rule cut away most of them.These are the questions that usually come next. Open one to see an answer you could say out loud.
What they are checkingWhether you can change a search that allows repeats into one that does not, and whether you know the duplicate trap. This is the well-known "Combination Sum II".
A strong answerTwo changes. First, I call dfs(i + 1, …) instead of dfs(i, …), so a number is never used twice. Second, because the list may hold equal numbers, I sort it and skip a number if it equals the one before it at the same level of choice. That stops me from building the same combination twice just because there are two equal numbers.
What they are checkingWhether you notice that listing everything is much more work than counting, and that the problem becomes a different kind of problem.
A strong answerNow I do not need the combinations themselves, so I can use a table instead of a search, like the coin-change counting problem: ways[a] is the number of combinations that make a, with ways[0] = 1. The loop over the numbers goes on the outside and the loop over amounts on the inside, so that [2, 3] and [3, 2] are not counted as two ways. See Coin Change. That takes O(n × T) time instead of exponential time.
path instead of a copy, and forgetting the undo. What happens?"What they are checkingWhether you understand that the path is one shared list that keeps changing.
A strong answerIf I append path itself, I save a reference to the same list, not its current contents. When the search carries on and the path changes, every saved answer changes with it, and at the end they are all empty. That is why I save path[:], a copy. If I forget the undo (path.pop()), numbers pile up in the path, so later branches start with leftover numbers and give wrong sums.