Skip to main content

Combination Sum

How to use controlled backtracking to build unique combinations in canonical order.

Given an array of distinct integers candidates and an integer target, return all unique combinations where the chosen numbers sum to target.

You may use the same number from candidates as many times as you want.

In the SeniorPath playground:

  • each combination must be in non-decreasing order
  • the final list must follow the natural DFS order after sorting candidates in ascending order

Example:

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

What to notice before coding

Common mistakes

Step 1 - Understand what "unique combination" means

The problem does not want every possible ordering.

If [2, 2, 3] already exists, then [2, 3, 2] and [3, 2, 2] are not new answers.

That changes the DFS shape a lot.

Step 2 - Write the most direct baseline

The natural baseline is:

  • try each candidate
  • keep the current path
  • stop when the sum matches or passes the target

It works.

But if you can always choose any candidate again, you end up building the same combination in many orders.

Step 3 - Ask how to block those duplicate orders

The strong idea is imposing a canonical order.

Instead of letting the path jump back and forth between candidates, you decide:

  • either use the current number and stay on the same index
  • or advance to larger candidates

That means the path never goes backward.

Step 4 - Turn that into backtracking

The needed state is:

  • startIndex: where the loop is allowed to restart
  • remaining: how much is left to reach the target
  • path: the current combination

If remaining === 0, you found an answer.

If remaining < 0, that path is dead.

The interactive editor needs JavaScript. You can still read the prompt and copy the starter code below.

Starter code

export function combinationSum(candidates: number[], target: number): number[][] {
  // your solution here
  return []
}
solution.ts
Stuck? Hints available.

Haven't solved it yet?

Viewing the solution now may reduce learning. It's worth trying a bit more.

Open the reference solution

Without JavaScript, the reference solution is shown inline instead of in a dialog.

Solution

Final complexity

Time: O(number of explored paths)

Space: O(target / min(candidates))

Solution 1 - Generate too many orders and deduplicate later

Good baseline because it makes the waste visible.

export function combinationSum(candidates: number[], target: number): number[][] {
  const unique = new Set<string>()

  function dfs(remaining: number, path: number[]) {
    if (remaining === 0) {
      unique.add([...path].sort((a, b) => a - b).join(","))
      return
    }

    if (remaining < 0) {
      return
    }

    for (const candidate of candidates) {
      path.push(candidate)
      dfs(remaining - candidate, path)
      path.pop()
    }
  }

  dfs(target, [])

  return [...unique].map((entry) => entry.split(",").map(Number))
}

It can work.

But it has an ugly flaw: it builds the same set of numbers in many orders and only cleans the mess at the end.

Solution 2 - Backtracking + DFS with an index to keep canonical order

Now the idea gets disciplined:

  1. sort candidates
  2. do DFS from a startIndex
  3. in the loop, only move forward
  4. when you choose candidates[i], recurse with the same i so reuse stays allowed
export function combinationSum(candidates: number[], target: number): number[][] {
  const sorted = [...candidates].sort((a, b) => a - b)
  const result: number[][] = []
  const path: number[] = []

  function dfs(startIndex: number, remaining: number) {
    if (remaining === 0) {
      result.push([...path])
      return
    }

    for (let index = startIndex; index < sorted.length; index += 1) {
      const candidate = sorted[index]

      if (candidate > remaining) {
        break
      }

      path.push(candidate)
      dfs(index, remaining - candidate)
      path.pop()
    }
  }

  dfs(0, target)
  return result
}

The most important detail is here:

dfs(index, remaining - candidate)

It is index, not index + 1.

That is what allows the same number to be used again.

If the problem banned reuse, then it would become index + 1.

What to say in the interview

A good short explanation would be:

The baseline tries candidates in any order and then has to deduplicate equal answers. The stronger version sorts the input and uses backtracking with a startIndex. That keeps each combination in canonical order, avoids repeated permutations, and still allows reuse because the recursive call stays on the same index.

That shows four good signals:

  • you understood why duplication appears
  • you controlled order instead of cleaning it later
  • you explained reuse correctly
  • you tied the recursion parameter back to the rule of the problem

When this pattern shows up again

The same idea comes back when the problem asks you to:

  • enumerate combinations
  • choose items with reuse
  • avoid duplicate answers caused by different orderings
  • prune early when the remaining value already makes the path impossible

The real gain here is not just “using recursion”.

It is noticing that canonical order simplifies the whole problem.

Next challenge Design Add and Search Words Data Structure Previous challenge Alien Dictionary

Related challenges

Related articles