Problem overview
The method combinationSum2 receives an int[] named candidates, which may contain duplicates, and an int named target. It returns a List<List<Integer>> with every distinct combination of candidates that adds up to target. Each candidate can be used at most once, so a value that appears twice in the array can appear up to twice in a combination. Two combinations are the same when they hold the same numbers the same number of times: with [1,1,7] and a target of 8, using the first 1 or the second 1 gives the same combination [1, 7], which must be listed once.
Each combination is written in ascending order and the list is in lexicographic order. For [10,1,2,7,6,1,5] and target 8, the sorted candidates are [1,1,2,5,6,7,10] and the answer is [[1, 1, 6], [1, 2, 5], [1, 7], [2, 6]]. For [2,3] and target 4 the answer is []: the 2 can't be used twice, and 2 + 3 is 5.
Approach: sorted backtracking with a duplicate skip
This is the Subsets II search with a sum attached. Sort the candidates. A call receives start, the first index it may use, remaining, how much the combination still needs, and current, the combination so far. When remaining is 0, the numbers in current add up to the target, so a copy is saved. Otherwise a loop tries each index i from start as the next number: add candidates[i], recurse with i + 1 and remaining - candidates[i], then remove it.
Two details separate this from the version where numbers can be reused:
- The deeper call starts at
i + 1, not ati. Passingiwould let the same candidate be chosen again and again. With that mistake,[2,3]and target 4 returns[[2, 2]]instead of[]. - Duplicate values need the skip. Inside one call, every turn of the loop picks a different next number, and a second copy of a value already tried at this position would rebuild the same combinations. So the loop skips
candidates[i]wheni > startand it equalscandidates[i - 1]. Ati == startthe value is the first one this call tries, so it is taken even if it equals its left neighbour. That is how[1, 1, 6]gets its second 1.
Sorting also allows a cut-off. Once candidates[i] is bigger than remaining, every later candidate is at least as big, so none fits, and the loop can break instead of continuing.
The runs show why the skip matters. For [1,1,7] and target 8, the solution returns [[1, 7]]. A version with the same search but no skip returns [[1, 7], [1, 7]]. A version that skips with i > 0 instead of i > start loses [1, 1, 6] on the first example and returns [[1, 2, 5], [1, 7], [2, 6]].
Step by step
- Sort
candidateswithArrays.sort. Equal values become neighbours, the output gets its order, and the loop can stop at the first number that is too big. - Create
resultas anArrayListof lists. - Start the search at index 0 with the whole
targetstill needed and an empty combination.backtrackis assumed to exist until it is written. - Return
result. - Declare
backtrack, avoidmethod with the array,start,remaining,currentandresult. - Check
remaining == 0. Nothing is left to make up, socurrentis a full combination. - Save a copy and return.
new ArrayList<>(current)is a snapshot, becausecurrentkeeps changing. Every candidate is at least 1, so adding more would overshoot the target and returning is right. - Loop over the indexes from
start. - Skip repeats with
continuewheni > startand the value equals the previous one. - Break when
candidates[i] > remaining. - Choose
candidates[i]by adding it tocurrent. - Recurse with
i + 1and the reducedremaining. - Undo by removing the last element, at index
current.size() - 1.
The solution in Java
class Solution {
public List<List<Integer>> combinationSum2(int[] candidates, int target) {
Arrays.sort(candidates);
List<List<Integer>> result = new ArrayList<>();
backtrack(candidates, 0, target, new ArrayList<>(), result);
return result;
}
void backtrack(int[] candidates, int start, int remaining, List<Integer> current, List<List<Integer>> result) {
if (remaining == 0) {
result.add(new ArrayList<>(current));
return;
}
for (int i = start; i < candidates.length; i++) {
if (i > start && candidates[i] == candidates[i - 1]) {
continue;
}
if (candidates[i] > remaining) {
break;
}
current.add(candidates[i]);
backtrack(candidates, i + 1, remaining - candidates[i], current, result);
current.remove(current.size() - 1);
}
}
}
Java details worth noting: current.remove(current.size() - 1) passes an int, so it removes the element at that index. Passing an Integer would remove the first element equal to that value, which is a different operation. Also, new ArrayList<>(current) copies the elements; storing current itself would make every saved combination change as the search continues, and they would all end up empty.
The order of the two checks matters. The skip comes first and the cut-off second; either order gives the same result here, but the skip must compare i with start, not with 0.
Complexity
With n candidates there are at most 2^n ways to pick some of them, and copying a combination takes up to n steps, so the worst case is O(n · 2^n) time. In practice it is far smaller: the sort lets the loop break early, and the duplicate skip means that only distinct combinations whose sum stays within the target are ever built. For example, 100 candidates equal to 1 with target 30 return a single combination of thirty 1s. Besides the answer, the extra space is O(n): the combination being built plus one call per number in it.
Edge cases and common mistakes
- No combination. Return an empty list, as in
[2,3]with target 4. - Several copies of a value.
[1,1,1,1]with target 3 returns[[1, 1, 1]], one combination, not four. - A single candidate equal to the target.
[2,5,2,1,2]with target 5 contains[5]alone, next to[1, 2, 2]. - Skipping with
i > 0. It drops combinations that use two equal numbers. - Recursing with
i. It reuses a candidate, which this problem forbids. - Forgetting to sort. The skip and the cut-off both rely on sorted order.
Testing
These inputs were run against the solution above:
| candidates, target | Result |
|---|---|
[10,1,2,7,6,1,5], 8 | [[1, 1, 6], [1, 2, 5], [1, 7], [2, 6]] |
[2,5,2,1,2], 5 | [[1, 2, 2], [5]] |
[2,3], 4 | [] |
[1,1,7], 8 | [[1, 7]] |
[1,1,1,1], 3 | [[1, 1, 1]] |
