Medium#397 min readBacktrackingCombinationsPruningLeetCode

Combination Sum

Backtracking with reuse and a start pointer

Unbounded choices. How do you avoid duplicates?

Published Apr 16, 2026

Problem

Given an array of distinct positive integers candidates and a target target, return all unique combinations (as lists) whose numbers sum to target. Each number may be used an unlimited number of times. Combinations are unordered — [2, 2, 3] and [3, 2, 2] are the same.

Input
candidates = [2, 3, 6, 7], target = 7
Output
[[2, 2, 3], [7]]
Input
candidates = [2, 3, 5], target = 8
Output
[[2, 2, 2, 2], [2, 3, 3], [3, 5]]

Intuition

Two small twists over subsets:

  1. Reuse. At each recursive call, you can pick the same index again. So the recurse-with call is dfs(i, ...) rather than dfs(i + 1, ...).
  2. Dedup by ordering. To avoid emitting [2, 2, 3] and [3, 2, 2] as distinct, force the path to be non-decreasing by only considering indices start.

The start pointer does both jobs. It's the same knob as in subsets (#78), just tuned differently — subsets use start + 1 (each element once, order fixed by array); combination sum uses start (reuse allowed, order fixed by start).

Pruning helps a lot: sort candidates ascending, break out of the loop as soon as candidates[i] > remaining. Without sort, the candidate space can explode; with sort, the tree is much shallower.

Approach

Sorted backtracking with pruning

TimeO(n^(T/min))
SpaceO(T/min)
Recommended

Sort once, then classic backtracking with two tweaks: dfs(i, ...) allows reuse, and break when the remaining target drops below the current candidate.

The depth of the recursion is bounded by target / min(candidate); the branching factor shrinks with pruning.

impl Solution {
    pub fn combination_sum(mut candidates: Vec<i32>, target: i32) -> Vec<Vec<i32>> {
        candidates.sort();
        let mut out = Vec::new();
        let mut path = Vec::new();
        fn dfs(
            start: usize,
            remaining: i32,
            cand: &[i32],
            path: &mut Vec<i32>,
            out: &mut Vec<Vec<i32>>,
        ) {
            if remaining == 0 {
                out.push(path.clone());
                return;
            }
            for i in start..cand.len() {
                if cand[i] > remaining {
                    break;
                }
                path.push(cand[i]);
                dfs(i, remaining - cand[i], cand, path, out);
                path.pop();
            }
        }
        dfs(0, target, &candidates, &mut path, &mut out);
        out
    }
}
Rust · runnable

Unsorted backtracking (no pruning)

TimeO(n^(T/min))
SpaceO(T/min)

Without sorting, you keep the start pointer for dedup but can't early-break on a too-large candidate. Slower in practice on most inputs, but useful as a baseline to see why the sort matters.

impl Solution {
    pub fn combination_sum(candidates: Vec<i32>, target: i32) -> Vec<Vec<i32>> {
        let mut out = Vec::new();
        let mut path = Vec::new();
        fn dfs(
            start: usize,
            remaining: i32,
            cand: &[i32],
            path: &mut Vec<i32>,
            out: &mut Vec<Vec<i32>>,
        ) {
            if remaining == 0 {
                out.push(path.clone());
                return;
            }
            if remaining < 0 {
                return;
            }
            for i in start..cand.len() {
                path.push(cand[i]);
                dfs(i, remaining - cand[i], cand, path, out);
                path.pop();
            }
        }
        dfs(0, target, &candidates, &mut path, &mut out);
        out
    }
}
Rust · runnable

Trace

Sorted candidates = [2, 3, 6, 7], target = 7:

stepstartpathremainingaction
10[]7try 2
20[2]5try 2 again
30[2,2]3try 2, then try 3
40[2,2,2]12 > 1 break
51[2,2,3]0emit
61[2,3]23 > 2 break
72[2,6]-1pruned (doesn't happen with pre-check)
83[7]0emit

Two combinations: [2, 2, 3] and [7].

Edge cases

  • No combination sums to target — return [].
  • Target of 0 — the only combination is []; the base case handles it.
  • Single candidate that divides target — one combination of target / candidate copies.
  • Very small minimum candidate with large target — depth can be target / min; for the problem's stated constraints this is small (target 40, candidates 2).

Takeaway

The start pointer is the dedup mechanism for unordered combinations. Subsets use start + 1; combination-sum uses start. When you need to emit multisets without worrying about order, impose an order on the path itself (non-decreasing indices). This is the same idea as "canonical form" in set enumeration.

More in Backtracking