Problem overview
The function combination_sum2 receives candidates, a vector of positive integers that may contain repeats, and a target. It returns every distinct combination of candidates that adds up to target, as a Vec<Vec<i32>>. Each candidate can be used at most once, so a value that appears twice in candidates can appear up to twice in one combination.
Two combinations are the same when they hold the same numbers the same number of times, whichever copies they use. With [1, 1, 7] and target 8, the first 1 with the 7 and the second 1 with the 7 are one combination, [1, 7], listed once. The numbers in each combination are in ascending order, and the combinations are in lexicographic order: the first position where two of them differ decides which comes first. When nothing adds up to the target, the result is empty.
For [10, 1, 2, 7, 6, 1, 5] and target 8, the answer is [[1, 1, 6], [1, 2, 5], [1, 7], [2, 6]]. For [2, 5, 2, 1, 2] and target 5 it is [[1, 2, 2], [5]], and for [2, 3] and target 4 it is [], because the single 2 cannot be used twice.
Approach: sort, then choose the next number
The search tries combinations in a way that never builds the same one twice. Sort candidates first. Equal numbers then sit next to each other, the numbers of every combination come out in ascending order, and the output has the order the task asks for.
A recursive function gets three pieces of state: start, the first index it may still use, remaining, how much the combination still needs, and current, the combination built 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 on as the next number: it adds candidates[i], calls itself with i + 1 and with remaining lowered by that number, and then removes the number again.
Two details do the real work:
- Skipping repeats. The loop skips
candidates[i]wheni > startand it equalscandidates[i - 1]. Within one call, every turn of the loop should pick a different next value, because a second copy of the same value would lead to exactly the same combinations. Ati == startthe value is the first one this call tries, so it is taken even when it equals the one before. That is how[1, 1, 6]gets its second 1. - Stopping early. The candidates are sorted, so when
candidates[i]is bigger thanremaining, every later candidate is bigger too. The loop canbreakinstead of checking the rest.
Recursing with i + 1 is what limits each candidate to one use. A search that recursed with i would allow a number to be picked again and again. With that change, [2, 3] and target 4 would return [[2, 2]], and [10, 1, 2, 7, 6, 1, 5] would give combinations such as [1, 1, 1, 1, 1, 1, 1, 1].
Step by step
- Sort
candidatesin place withsort(). The parameter is declaredmutso the function can change its own vector. - Create
resultas an empty vector. Rust infersVec<Vec<i32>>from how it is used. - Start the search with
Self::backtrack: a borrow ofcandidates, start index 0, the wholetargetstill needed, a fresh empty vector for the combination and a mutable borrow ofresult. - Return
resultas the last expression. - Declare the helper
backtrackwithcandidates: &Vec<i32>,start: usize,remaining: i32,current: &mut Vec<i32>andresult: &mut Vec<Vec<i32>>. - Check for a finished combination. When
remaining == 0, the numbers incurrentadd up to exactly the target. - Save a copy and return. Push
current.clone()ontoresult, sincecurrentkeeps changing. Every candidate is at least 1, so adding anything more would pass the target, and the call can return at once. - Loop
ioverstart..candidates.len(). Starting atstartinstead of 0 means a combination takes its numbers in index order, so no choice of indexes is built twice. - Skip repeats with
continuewheni > startandcandidates[i] == candidates[i - 1]. - Stop when a number is too big. If
candidates[i] > remaining,break: the sort guarantees nothing after it fits either. - Choose the number: push
candidates[i]ontocurrent. - Recurse with
i + 1as the start andremaining - candidates[i]as the amount still needed. - Undo the choice with
current.pop(), so the loop can try the next candidate in its place.
The solution in Rust
impl Solution {
fn combination_sum2(mut candidates: Vec<i32>, target: i32) -> Vec<Vec<i32>> {
candidates.sort();
let mut result = Vec::new();
Self::backtrack(&candidates, 0, target, &mut Vec::new(), &mut result);
result
}
fn backtrack(candidates: &Vec<i32>, start: usize, remaining: i32, current: &mut Vec<i32>, result: &mut Vec<Vec<i32>>) {
if remaining == 0 {
result.push(current.clone());
return;
}
for i in start..candidates.len() {
if i > start && candidates[i] == candidates[i - 1] {
continue;
}
if candidates[i] > remaining {
break;
}
current.push(candidates[i]);
Self::backtrack(candidates, i + 1, remaining - candidates[i], current, result);
current.pop();
}
}
}
The Rust details worth a closer look:
- Borrowing across the recursion.
currentandresultare&mutreferences, and the recursive call passes them on as they are. Rust reborrows them for the call, so they are usable again as soon as it returns. &mut Vec::new(). It creates a temporary empty vector and lends it mutably for the first call. Nothing else needs it afterwards.current.clone(). It builds a new vector with the same numbers. The result must hold snapshots, becausecurrentis changed by the pushes and pops that follow.i - 1on ausize. It is evaluated only wheni > start, which meansiis at least 1. The&&operator short-circuits, socandidates[i - 1]is never evaluated ati == 0.- Plain
i32arithmetic.remainingnever goes below zero, because of thebreakbefore the subtraction. The bounds in the task keep the sums small.
Complexity
Time: O(n * 2^n) in the worst case, for n candidates. There are at most 2^n ways to pick some of them, and copying a combination takes up to n steps. Sorting adds O(n log n). In practice the search is much smaller, because the sort and the break keep it to distinct combinations whose sum stays within the target.
Space: O(n) beyond the answer. The combination being built and the chain of recursive calls each hold at most one entry per candidate used.
Edge cases and common mistakes
- Forgetting to skip repeats. Without the
continueline,[1, 1, 7]and target 8 returns[[1, 7], [1, 7]]. On the first example it returns[[1, 1, 6], [1, 2, 5], [1, 7], [1, 2, 5], [1, 7], [2, 6]], and on[2, 5, 2, 1, 2]with target 5 it returns[1, 2, 2]three times. - Using
iinstead ofi + 1. That is the rule of the version of the problem where a number can be reused. Here it lets a single 2 stand for[2, 2]. - Skipping with
i > 0. The skip must compare againststart. A skip based oni > 0also blocks the second 1 in[1, 1, 6], even though it is the first number that call tries: the first example then returns[[1, 2, 5], [1, 7], [2, 6]], and[2, 5, 2, 1, 2]with target 5 returns only[[5]]. - Not sorting. The checks against
candidates[i - 1]and the earlybreakboth depend on the order, and so does the lexicographic output. - A
continuewhere thebreakbelongs. The result would still be right, but the loop would examine candidates that cannot fit. - No answer.
[4]with target 5 returns[], and[5]with target 5 returns[[5]]. - Many equal values.
[1, 1, 1, 1]with target 2 returns[[1, 1]], exactly one combination despite the many ways to choose two of the four 1s.
Testing
[10, 1, 2, 7, 6, 1, 5], target 8 gives[[1, 1, 6], [1, 2, 5], [1, 7], [2, 6]].[2, 5, 2, 1, 2], target 5 gives[[1, 2, 2], [5]].[2, 3], target 4 gives[].[1, 1, 7], target 8 gives[[1, 7]].[1, 1, 1, 1], target 2 gives[[1, 1]].[5], target 5 gives[[5]].
