Skip to content
Codeloom
DSA

Combination Sum: Backtracking With Reuse

Solve Combination Sum with a clean backtracking template. Pruning, duplicate avoidance, complexity discussion, and interview script.

·5 min read · By Codeloom
Intermediate 9 min read

What you'll learn

  • The general backtracking template for combinatorial search
  • How a start index prevents duplicate combinations
  • Why reusable candidates change the recursion choice
  • How to prune by sorting and early exit
  • How to discuss complexity for backtracking problems

Prerequisites

  • Familiarity with [recursion](/blog/recursion-fundamentals)
  • Comfort with [arrays](/blog/arrays-introduction)

Combination Sum is the backtracking gateway problem. Master the start-index template here and a dozen other combinatorial problems become routine.

The Problem

Given a list of distinct positive integers candidates and a target integer target, return all unique combinations of candidates that sum to target. Each candidate may be reused unlimited times.

Example:

  • Input: candidates = [2,3,6,7], target = 7
  • Output: [[2,2,3],[7]]

Intuition

We want every multiset that sums to the target. To avoid generating the same multiset in different orders ([2,2,3] vs [3,2,2]), we enforce non-decreasing index order through a start parameter: at each step we may reuse the current index or move forward, but never go back. Because candidates can repeat, the recursion stays at i rather than advancing to i + 1. Sorting first lets us break out of the loop as soon as a candidate exceeds the remaining target.

Explanation with Example

candidates = [2, 3, 6, 7], target = 7.

  • Start with path [], remaining 7.
  • Pick 2, path [2], remaining 5.
    • Pick 2, path [2,2], remaining 3.
      • Pick 2, path [2,2,2], remaining 1. Loop sees 2 > 1, break.
      • Pick 3, path [2,2,3], remaining 0. Record [2,2,3].
    • Pick 3, path [2,3], remaining 2. Loop sees 3 > 2, break.
  • Pick 3, path [3], remaining 4.
    • 3 > ? Continue. Eventually no hit.
  • Pick 7, path [7], remaining 0. Record [7].

Final: [[2,2,3],[7]].

                  target=7, path=[]
       +----------+----------+----------+
     pick 2     pick 3     pick 6     pick 7
     rem=5      rem=4      rem=1      rem=0
      |          |          X         FOUND [7]
    pick 2     pick 3
    rem=3      rem=1
      |          X (no candidate <= rem with i)
    pick 2
    rem=1
      X
    pick 3
    rem=0
    FOUND [2,2,3]

start index forbids going back, preventing duplicate combos.
Recursion tree for combinationSum([2,3,6,7], target=7)

Code

Note the recursive call passes i, not i + 1, because the same candidate can be reused. Sorting and the c > remaining break together give a useful constant-factor prune.

def combination_sum(candidates, target):
    candidates.sort()
    result = []

    def backtrack(start, path, remaining):
        if remaining == 0:
            result.append(path[:])
            return
        for i in range(start, len(candidates)):
            c = candidates[i]
            if c > remaining:
                break
            path.append(c)
            backtrack(i, path, remaining - c)
            path.pop()

    backtrack(0, [], target)
    return result
import java.util.*;

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

    private void backtrack(int[] candidates, int start, List<Integer> path, int remaining, List<List<Integer>> result) {
        if (remaining == 0) {
            result.add(new ArrayList<>(path));
            return;
        }
        for (int i = start; i < candidates.length; i++) {
            if (candidates[i] > remaining) break;
            path.add(candidates[i]);
            backtrack(candidates, i, path, remaining - candidates[i], result);
            path.remove(path.size() - 1);
        }
    }
}
#include <vector>
#include <algorithm>
using namespace std;

class Solution {
public:
    vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
        sort(candidates.begin(), candidates.end());
        vector<vector<int>> result;
        vector<int> path;
        backtrack(candidates, 0, path, target, result);
        return result;
    }

private:
    void backtrack(vector<int>& candidates, int start, vector<int>& path, int remaining, vector<vector<int>>& result) {
        if (remaining == 0) {
            result.push_back(path);
            return;
        }
        for (int i = start; i < (int)candidates.size(); i++) {
            if (candidates[i] > remaining) break;
            path.push_back(candidates[i]);
            backtrack(candidates, i, path, remaining - candidates[i], result);
            path.pop_back();
        }
    }
};

Edge Cases

  • Target less than smallest candidate: return empty list.
  • Single candidate equal to target: returns one combination.
  • Target zero: return [[]] if the problem allows it; typical constraints exclude this.
  • Large targets with small candidates: solution count can explode; the start-index trick prevents duplicate exploration but not exponential growth in valid combinations.

Complexity Analysis

  • Time: O(N^(T/M)) in the worst case, where N is the candidate count, T is target, M is the smallest candidate. The branching factor is N, depth bounded by T/M.
  • Space: O(T/M) for the recursion stack and current path; plus output size.

Backtracking complexity is hard to make tight; explain the bound and the pruning instead. See Big-O for context.

How to Explain It in an Interview

Open with the template: “I’ll use backtracking with a start index to avoid generating the same combination in different orders.” Explain why we pass i instead of i + 1: the problem allows reuse. Mention the sort + early break as a constant-factor improvement. Walk through one branch end-to-end; interviewers care about clean state management (push and pop in symmetry).

  • Combination Sum II (each candidate used once, duplicates allowed in input)
  • Combination Sum III
  • Subsets
  • Permutations

Wrap up

The start-index backtracking template is one of the highest-ROI patterns to internalize. Combination Sum is the cleanest demo: distinct candidates, reusable, fixed target. Once you can write this in under three minutes, the rest of the combinatorial set is just variations on the loop condition.