LeetCode #39 — Medium

Combination Sum

A complete visual walkthrough of backtracking with element reuse — from brute force to optimal pruning.

Solve on LeetCode
The Problem

Problem Statement

Given an array of distinct integers candidates and a target integer target, return a list of all unique combinations of candidates where the chosen numbers sum to target. You may return the combinations in any order.

The same number may be chosen from candidates an unlimited number of times. Two combinations are unique if the frequency of at least one of the chosen numbers is different.

The test cases are generated such that the number of unique combinations that sum up to target is less than 150 combinations for the given input.

Example 1:

Input: candidates = [2,3,6,7], target = 7
Output: [[2,2,3],[7]]
Explanation:
2 and 3 are candidates, and 2 + 2 + 3 = 7. Note that 2 can be used multiple times.
7 is a candidate, and 7 = 7.
These are the only two combinations.

Example 2:

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

Example 3:

Input: candidates = [2], target = 1
Output: []

Constraints:

  • 1 <= candidates.length <= 30
  • 2 <= candidates[i] <= 40
  • All elements of candidates are distinct.
  • 1 <= target <= 40
Patterns Used

Roadmap

  1. Brute Force — Why It Doesn't Scale
  2. The Core Insight — Backtracking with Reuse
  3. Decision Tree Walkthrough
  4. Edge Cases
  5. Full Annotated Code
  6. Interactive Demo
  7. Complexity Analysis
Step 01

Brute Force — Why It Doesn't Scale

The naive approach would generate all possible subsets of every length, with repetition, then check which ones sum to the target. For candidates = [2, 3, 6, 7] and target = 7, we would enumerate an enormous number of combinations — most of them useless.

The Problem

candidates
2
3
6
7
target = 7

With unlimited reuse, the search space is infinite unless we prune intelligently. For example, we could keep picking 2 forever: [2, 2, 2, 2, ...] and never stop.

We need a systematic way to explore combinations without duplicates and with early termination when the running sum exceeds the target.

Step 02

The Core Insight — Backtracking with Reuse

The trick is a classic backtracking pattern: at each step, we choose a candidate, add it to our current path, then recurse with a reduced target. After the recursive call returns, we undo the choice (backtrack) and try the next candidate.

The Key Difference: i, Not i + 1

In standard combination problems (no reuse), the recursive call starts from i + 1 to avoid picking the same element again. Here, we pass i (the same index) because the same number can be reused unlimited times.

WITH REUSE (#39)
backtrack(..., i, ...)
Can pick candidates[i] again in the next recursive call
NO REUSE (typical)
backtrack(..., i + 1, ...)
Moves past candidates[i], never picks it again
💡

Sorting first lets us prune early: once candidates[i] exceeds the remaining target, all subsequent candidates (which are larger) will too, so we can break immediately instead of continuing the loop.

Three rules govern the backtracking:

remain == 0
Found a valid combination! Add a copy of the current path to results.
candidates[i] > remain
This candidate (and all after it, since sorted) overshoot the target. Break.
otherwise
Pick candidates[i], recurse with remain - candidates[i], then undo.
Step 03

Decision Tree Walkthrough

Let's trace the backtracking for candidates = [2, 3, 6, 7], target = 7. The sorted order is already [2, 3, 6, 7]. Each node shows the current path and remaining target.

Backtracking Tree (Abridged)

[] remain=7
pick 2
[2] remain=5
pick 3
[3] remain=4
pick 6
[6] remain=1
pick 7
[7] remain=0
pick 2
[2,2] remain=3
pick 3
[2,3] remain=2
pick 6
[2,6] 6>5
[3] branch
[3,3] remain=1
[6] branch
[6,2] 2>1
pick 2
[2,2,2] remain=1
pick 3
[2,2,3] remain=0
from [2,3]
[2,3,2] 2=2 ok but only 2 left, pick 2
pick 2
[2,2,2,2] 2>1
found Valid combination
pruned Exceeds target, prune
explore Still exploring
🎯

Two solutions found: [2, 2, 3] and [7]. Notice how sorting + the break on overshoot aggressively prunes the tree. Without sorting, we would waste time exploring many more dead-end branches.

The start index parameter prevents generating duplicate combinations like [2, 3, 2] and [3, 2, 2] — once we move past candidate 2, we never go back to it. We only consider candidates at index start or later.

Step 04

Edge Cases

Single candidate equals target
candidates = [7], target = 7 → [[7]]
Target unreachable
candidates = [3], target = 2 → [] (3 > 2, immediate pruning)
All-ones reuse
candidates = [1], target = 3 → [[1, 1, 1]] (max reuse depth = target)
Large candidate set
Multiple candidates where some are multiples of others. Sorting ensures we explore smallest first and prune efficiently.
Step 05

Full Annotated Code

class Solution {
    public List<List<Integer>> combinationSum(int[] candidates, int target) {
        List<List<Integer>> result = new ArrayList<>();
        Arrays.sort(candidates);           // Sort for early termination
        backtrack(candidates, target, 0, new ArrayList<>(), result);
        return result;
    }

    private void backtrack(int[] candidates, int remain,
                           int start, List<Integer> path,
                           List<List<Integer>> result) {

        if (remain == 0) {               // Target reached — record solution
            result.add(new ArrayList<>(path));
            return;
        }

        for (int i = start; i < candidates.length; i++) {

            if (candidates[i] > remain)    // Prune: too large (and all after)
                break;

            path.add(candidates[i]);       // Choose

            backtrack(candidates,
                      remain - candidates[i],
                      i,                         // ← NOT i+1: allow reuse
                      path, result);

            path.remove(path.size() - 1);  // Un-choose (backtrack)
        }
    }
}
Step 06

Interactive Demo

Enter candidates and a target, then step through the backtracking tree to see how combinations are built and pruned.

⚙ Backtracking Visualizer



Press "Step →" or "Run All" to begin.
Step 07

Complexity Analysis

Time
O(nT/min)
Space
O(n)

Where n is the number of candidates, T is the target value, and min is the smallest candidate. The maximum recursion depth is T/min (picking the smallest element repeatedly). At each level, we branch up to n ways. Space is O(T/min) for the recursion stack and current path.

Approach Breakdown

BRUTE FORCE
O(nT) time
O(T) space

Without sorting or pruning, the recursion tree branches n ways at each level and can reach depth T (picking 1 repeatedly). Dead-end branches that overshoot the target are only detected after the recursive call, wasting O(nT) work.

OPTIMAL
O(nT/min) time
O(T / min) space

Sorting candidates enables a break when candidates[i] > remain, cutting off all larger candidates at once. Max depth is T/min (smallest candidate repeated). The start-index parameter prevents duplicate orderings like [2,3] and [3,2].

🎯

Sorting candidates and breaking early when the sum exceeds the target dramatically prunes the search tree. Without sorting, we would explore dead-end branches that overshoot the target. With sorting, once candidates[i] > remain, all subsequent candidates are larger and can be skipped entirely.

Coach Notes

Common Mistakes

Review these before coding to avoid predictable interview regressions.

Missing undo step on backtrack

Wrong move: Mutable state leaks between branches.

Usually fails on: Later branches inherit selections from earlier branches.

Fix: Always revert state changes immediately after recursive call.