Backtracking · 12 of 19
Combinational Sum
Find every bag of coins, where each coin type can be used again and again, that adds up to exactly the price.
- Simple way
- time O(n^(t/m) * t/m)
room O(t/m) - Clever way
- time O(n^(t/m))
room O(t/m)
The problem, simply
You get a list of different numbers, called candidates, and a target number. Find every group of candidates that adds up to the target. You may use the same number as many times as you like.
A group is about which numbers are in it, not their order. So 2 + 2 + 3 and 3 + 2 + 2 are the same group and must appear only once.
A story to remember it
Picture it
Each circle is how much is still left to pay. Each branch is the coin we just put down. The first choice can be 2, 3, 6 or 7. A circle with 0 means we paid exactly.
- looking now
- answer
Examples
A normal case
- In
candidates = [2, 3, 6, 7], target = 7- Out
[[2, 2, 3], [7]]
2 + 2 + 3 is 7, and 7 alone is 7. No other group works.
Many answers
- In
candidates = [2, 3, 5], target = 8- Out
[[2, 2, 2, 2], [2, 3, 3], [3, 5]]
The same coin can be used four times. Order does not matter, so 3 + 5 and 5 + 3 count once.
No way to pay
- In
candidates = [2], target = 1- Out
[]
Even one 2 is too much. Return an empty list.
Unsorted input
- In
candidates = [7, 3, 2], target = 7- Out
[[2, 2, 3], [7]]
The input is not sorted. Sort it first so the early stop works.
Way 1: the simple way
At every step, try every coin, in any order. When you hit exactly 0, sort the coins you used and drop the group in a set, so repeats vanish.
- If nothing is left to pay, sort a copy of the picked coins and add it to a set.
- If you paid too much, give up on this branch.
- Otherwise, for each coin: put it down, try again with less to pay, then pick it back up.
Trying coins in any order finds the same answer three times: 2, 2, 3 and 2, 3, 2 and 3, 2, 2. The set throws away two of them, but we still did the work.
- answer
- rejected
import java.util.*;
class Solution {
public List<List<Integer>> combinationSum(int[] candidates, int target) {
Set<List<Integer>> seen = new HashSet<>();
tryAll(candidates, target, new ArrayList<>(), seen);
return new ArrayList<>(seen);
}
private void tryAll(int[] cand, int left, List<Integer> picked, Set<List<Integer>> seen) {
if (left == 0) {
List<Integer> copy = new ArrayList<>(picked);
Collections.sort(copy); // 3,2,2 and 2,2,3 become the same list
seen.add(copy);
return;
}
if (left < 0) return; // paid too much
for (int c : cand) { // any coin, in any order
picked.add(c);
tryAll(cand, left - c, picked, seen);
picked.remove(picked.size() - 1); // pick the coin back up
}
}
}How long it takes: let t be the target and m the smallest coin. A path can be t/m coins deep, and each step tries all n coins, so there are up to n^(t/m) paths, and each answer is sorted. That is O(n^(t/m) * t/m). Extra room: the path and the call stack are at most t/m deep, O(t/m), not counting the answers.
Way 2: the clever way
Two small rules make the tree much smaller.
- Sort the coins.
- Remember a starting spot
start. From a circle, only try coins atstartor later. Pass the sameidown (noti + 1), because a coin may be used again. This alone kills all repeats: groups are always built small to big. - If coin
iis bigger than what is left,break. Every coin after it is even bigger. - When nothing is left, copy the picked list into the answer.
The choice tree for target 7
01 / 18
Start with 7 left to make. The candidates are sorted: 2, 3, 6, 7.
- picked
- none
- left
- 7
- found
- 0
- looking now
- seen
- done
- answer
- rejected
- path
import java.util.*;
class Solution {
public List<List<Integer>> combinationSum(int[] candidates, int target) {
List<List<Integer>> result = new ArrayList<>();
Arrays.sort(candidates); // small coins first
backtrack(candidates, target, 0, new ArrayList<>(), result);
return result;
}
private void backtrack(int[] cand, int left, int start,
List<Integer> picked, List<List<Integer>> result) {
if (left == 0) {
result.add(new ArrayList<>(picked)); // paid exactly
return;
}
for (int i = start; i < cand.length; i++) {
if (cand[i] > left) break; // this coin and all after it are too big
picked.add(cand[i]);
backtrack(cand, left - cand[i], i, picked, result); // i, not i + 1: reuse allowed
picked.remove(picked.size() - 1); // undo the choice
}
}
}How long it takes: in the worst case it is still about O(n^(t/m)) circles, because the number of answers itself can grow that fast. But no answer is built twice, no sorting or set is needed, and the break cuts whole fans of branches. Extra room: O(t/m) for the path and the call stack.
Watch out for
- Forgetting to sort. Without sorting,
breakis wrong because a later coin could still fit. Usecontinueif you cannot sort. - Adding
pickeditself to the answer. It keeps changing. Addnew ArrayList<>(picked). - Starting the loop at 0 every time. That brings back the repeated groups from Way 1.
- A zero or negative coin makes the tree endless. The problem says all coins are positive; say so if asked.
What interviewers ask next
- Each number may be used once, and the input has repeats (Combination Sum II). Pass
i + 1, and skipcand[i] == cand[i - 1]wheni > start, so the same value is not tried twice at one level. - Only count the ways, do not list them. That is the coin change counting problem: a 1D table
ways[s] += ways[s - coin], looping coins on the outside, inO(n * t). - What if order matters, so
2 + 3and3 + 2are different? Loop the target on the outside and coins on the inside. That is Combination Sum IV. - Pick exactly
knumbers from 1 to 9 that add ton(Combination Sum III). Same tree, plus a depth limit ofk.
Remember this
Grown-up words: the coins are candidates, the price is the target, the drawing is a recursion tree or state space tree, putting a coin down and picking it back up is backtracking, and the early break is pruning.