1065-combinations-sum — Combination Sum
Tutorial
The Problem
Find all combinations of numbers from a given set that sum to a target, where each number may be used multiple times. This is a backtracking enumeration problem with applications in change-making (all ways to make an amount), spell correction (all word sequences summing to a score), and exam scheduling (all subsets hitting exactly N hours).
The key pruning: sort candidates and break early when the current candidate exceeds the remaining target — this transforms exponential worst case into practical tractability.
🎯 Learning Outcomes
Code Example
#![allow(clippy::all)]
// 1065: Combination Sum — Find Combos Summing to Target
// Approach 1: Backtracking with reuse allowed
fn combination_sum(candidates: &mut Vec<i32>, target: i32) -> Vec<Vec<i32>> {
candidates.sort();
let mut results = Vec::new();
let mut current = Vec::new();
fn backtrack(
start: usize,
remaining: i32,
candidates: &[i32],
current: &mut Vec<i32>,
results: &mut Vec<Vec<i32>>,
) {
if remaining == 0 {
results.push(current.clone());
return;
}
for i in start..candidates.len() {
if candidates[i] > remaining {
break;
} // sorted, so prune
current.push(candidates[i]);
backtrack(i, remaining - candidates[i], candidates, current, results);
current.pop();
}
}
backtrack(0, target, candidates, &mut current, &mut results);
results
}
// Approach 2: Functional with iterators
fn combination_sum_func(candidates: &[i32], target: i32) -> Vec<Vec<i32>> {
let mut sorted = candidates.to_vec();
sorted.sort();
fn solve(start: usize, remaining: i32, sorted: &[i32]) -> Vec<Vec<i32>> {
if remaining == 0 {
return vec![vec![]];
}
if remaining < 0 {
return vec![];
}
let mut results = Vec::new();
for i in start..sorted.len() {
if sorted[i] > remaining {
break;
}
for mut combo in solve(i, remaining - sorted[i], sorted) {
combo.insert(0, sorted[i]);
results.push(combo);
}
}
results
}
solve(0, target, &sorted)
}
// Approach 3: Combination sum II (each number used once)
fn combination_sum_unique(candidates: &mut Vec<i32>, target: i32) -> Vec<Vec<i32>> {
candidates.sort();
let mut results = Vec::new();
let mut current = Vec::new();
fn backtrack(
start: usize,
remaining: i32,
candidates: &[i32],
current: &mut Vec<i32>,
results: &mut Vec<Vec<i32>>,
) {
if remaining == 0 {
results.push(current.clone());
return;
}
for i in start..candidates.len() {
if candidates[i] > remaining {
break;
}
if i > start && candidates[i] == candidates[i - 1] {
continue;
} // skip dupes
current.push(candidates[i]);
backtrack(
i + 1,
remaining - candidates[i],
candidates,
current,
results,
);
current.pop();
}
}
backtrack(0, target, candidates, &mut current, &mut results);
results
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_combination_sum() {
let mut cands = vec![2, 3, 6, 7];
let r = combination_sum(&mut cands, 7);
assert_eq!(r.len(), 2);
assert!(r.contains(&vec![2, 2, 3]));
assert!(r.contains(&vec![7]));
}
#[test]
fn test_combination_sum_func() {
let r = combination_sum_func(&[2, 3, 5], 8);
assert_eq!(r.len(), 3);
}
#[test]
fn test_combination_sum_unique() {
let mut cands = vec![10, 1, 2, 7, 6, 1, 5];
let r = combination_sum_unique(&mut cands, 8);
assert!(r.contains(&vec![1, 1, 6]));
assert!(r.contains(&vec![1, 2, 5]));
assert!(r.contains(&vec![1, 7]));
assert!(r.contains(&vec![2, 6]));
}
}Key Differences
current.push(c) / current.pop() in-place; OCaml prepends c :: current creating new lists at each step.candidates.sort(), OCaml uses List.sort compare.start: usize parameter; OCaml uses List.iteri with an index comparison — less clean.itertools**: The itertools crate provides (0..n).combinations(k) for non-reuse combinations; Rust has no built-in for the reuse variant.OCaml Approach
let combination_sum candidates target =
let candidates = List.sort compare candidates in
let results = ref [] in
let rec backtrack start remaining current =
if remaining = 0 then results := List.rev current :: !results
else
List.iteri (fun i c ->
if i >= start && c <= remaining then
backtrack i (remaining - c) (c :: current)
) candidates
in
backtrack 0 target [];
!results
The structure is identical. OCaml's list prepend and List.rev at collection mirrors Rust's Vec::push and Vec::clone.
Full Source
#![allow(clippy::all)]
// 1065: Combination Sum — Find Combos Summing to Target
// Approach 1: Backtracking with reuse allowed
fn combination_sum(candidates: &mut Vec<i32>, target: i32) -> Vec<Vec<i32>> {
candidates.sort();
let mut results = Vec::new();
let mut current = Vec::new();
fn backtrack(
start: usize,
remaining: i32,
candidates: &[i32],
current: &mut Vec<i32>,
results: &mut Vec<Vec<i32>>,
) {
if remaining == 0 {
results.push(current.clone());
return;
}
for i in start..candidates.len() {
if candidates[i] > remaining {
break;
} // sorted, so prune
current.push(candidates[i]);
backtrack(i, remaining - candidates[i], candidates, current, results);
current.pop();
}
}
backtrack(0, target, candidates, &mut current, &mut results);
results
}
// Approach 2: Functional with iterators
fn combination_sum_func(candidates: &[i32], target: i32) -> Vec<Vec<i32>> {
let mut sorted = candidates.to_vec();
sorted.sort();
fn solve(start: usize, remaining: i32, sorted: &[i32]) -> Vec<Vec<i32>> {
if remaining == 0 {
return vec![vec![]];
}
if remaining < 0 {
return vec![];
}
let mut results = Vec::new();
for i in start..sorted.len() {
if sorted[i] > remaining {
break;
}
for mut combo in solve(i, remaining - sorted[i], sorted) {
combo.insert(0, sorted[i]);
results.push(combo);
}
}
results
}
solve(0, target, &sorted)
}
// Approach 3: Combination sum II (each number used once)
fn combination_sum_unique(candidates: &mut Vec<i32>, target: i32) -> Vec<Vec<i32>> {
candidates.sort();
let mut results = Vec::new();
let mut current = Vec::new();
fn backtrack(
start: usize,
remaining: i32,
candidates: &[i32],
current: &mut Vec<i32>,
results: &mut Vec<Vec<i32>>,
) {
if remaining == 0 {
results.push(current.clone());
return;
}
for i in start..candidates.len() {
if candidates[i] > remaining {
break;
}
if i > start && candidates[i] == candidates[i - 1] {
continue;
} // skip dupes
current.push(candidates[i]);
backtrack(
i + 1,
remaining - candidates[i],
candidates,
current,
results,
);
current.pop();
}
}
backtrack(0, target, candidates, &mut current, &mut results);
results
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_combination_sum() {
let mut cands = vec![2, 3, 6, 7];
let r = combination_sum(&mut cands, 7);
assert_eq!(r.len(), 2);
assert!(r.contains(&vec![2, 2, 3]));
assert!(r.contains(&vec![7]));
}
#[test]
fn test_combination_sum_func() {
let r = combination_sum_func(&[2, 3, 5], 8);
assert_eq!(r.len(), 3);
}
#[test]
fn test_combination_sum_unique() {
let mut cands = vec![10, 1, 2, 7, 6, 1, 5];
let r = combination_sum_unique(&mut cands, 8);
assert!(r.contains(&vec![1, 1, 6]));
assert!(r.contains(&vec![1, 2, 5]));
assert!(r.contains(&vec![1, 7]));
assert!(r.contains(&vec![2, 6]));
}
}#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_combination_sum() {
let mut cands = vec![2, 3, 6, 7];
let r = combination_sum(&mut cands, 7);
assert_eq!(r.len(), 2);
assert!(r.contains(&vec![2, 2, 3]));
assert!(r.contains(&vec![7]));
}
#[test]
fn test_combination_sum_func() {
let r = combination_sum_func(&[2, 3, 5], 8);
assert_eq!(r.len(), 3);
}
#[test]
fn test_combination_sum_unique() {
let mut cands = vec![10, 1, 2, 7, 6, 1, 5];
let r = combination_sum_unique(&mut cands, 8);
assert!(r.contains(&vec![1, 1, 6]));
assert!(r.contains(&vec![1, 2, 5]));
assert!(r.contains(&vec![1, 7]));
assert!(r.contains(&vec![2, 6]));
}
}
Deep Comparison
Combination Sum — Comparison
Core Insight
Combination sum is backtracking with an index parameter controlling reuse. Starting at i (not i+1) allows the same element to be picked again. Sorting enables pruning: if candidates[i] > remaining, all later candidates are too large.
OCaml Approach
List.sort compare for sorting candidatesList.iteri with index filtering for start position:: + List.rev for result buildingref list for accumulating resultsRust Approach
sort() in-place on Vecbreak in sorted loop for pruning — cleaner than filteringpush/pop on current for backtrackingcontinue for duplicate skipping in Combination Sum IIComparison Table
| Aspect | OCaml | Rust |
|---|---|---|
| Sorting | List.sort compare (returns new list) | .sort() (in-place) |
| Pruning | Index filtering with List.iteri | break in sorted loop |
| Result building | Prepend :: + List.rev | push + pop |
| Reuse control | Recurse with same index | Recurse with same i |
| Dedup (variant) | Would need explicit skip | if i > start && arr[i] == arr[i-1] |
Exercises
combination_sum_no_reuse(candidates: &mut [i32], target: i32) -> Vec<Vec<i32>> where each number can be used at most once (sort + skip duplicates).combination_sum_count(candidates: &[i32], target: i32) -> usize that counts the number of combinations without collecting them.max_depth: usize parameter that limits the maximum number of elements in any single combination.