Start learning free

LESSON 356 OF 540 · BACKTRACKING

Combination Sum in Go: Backtracking on Sorted Candidates

Practice O(nt/m) Read the full explanation

Given an array of distinct integers candidates and an integer target, return every combination of numbers from candidates that adds up to target.

The same number may be chosen any number of times. Two combinations are the same when they use the same numbers the same number of times, whatever the order: [2, 2, 3] and [3, 2, 2] are one combination, and it must appear only once.

Write the numbers of each combination in ascending order, and return the combinations in lexicographic order: compare two combinations number by number, and the first position where they differ decides which one comes first. If no combination adds up to target, return an empty array.

 

Example 1:

Input: candidates = [2,3,6,7], target = 7
Output: [[2,2,3],[7]]
Explanation: 2 + 2 + 3 = 7, using the 2 twice, and 7 alone is 7.
No other combination adds up to 7. [2,2,3] comes first because its first number, 2, is smaller than 7.

Example 2:

Input: candidates = [5,2,3], target = 8
Output: [[2,2,2,2],[2,3,3],[3,5]]
Explanation: The candidates are not sorted, but every combination is written in ascending order.
[2,2,2,2] and [2,3,3] both start with 2; at their second number, 2 is smaller than 3. [3,5] starts with 3, so it comes last.

Example 3:

Input: candidates = [2], target = 1
Output: []
Explanation: The only candidate, 2, is already bigger than 1, so no combination adds up to 1.

 

Constraints:

  • 1 <= candidates.length <= 30
  • 2 <= candidates[i] <= 40
  • All the numbers in candidates are distinct.
  • 1 <= target <= 40

Note:

The tutorial uses backtracking. Sort candidates, then build a combination one number at a time: try every candidate from the position of the last number chosen onward (that same number included, since numbers can repeat), go deeper with what is left of target, then remove the number again and try the next one. Starting from the last chosen position builds each combination exactly once, already in ascending order, and the combinations come out in lexicographic order. Because the candidates are sorted, the loop can stop at the first one that is bigger than what is left.

The search is exponential: with n candidates, a target t and a smallest candidate m, a combination has at most t / m numbers, and each of them is one of n candidates, so the time is O(nt/m) in the worst case. Besides the answer, the extra space is O(t / m): the combination being built and one call per number in it.

STEP 1 OF 12
    Ctrl + Enter to check

    GO · BACKTRACKING

    Combination Sum in Go, explained

    Problem overview

    The function combinationSum receives a slice of distinct integers, candidates, and an integer target. It returns every combination of candidates that adds up to target. A candidate can be used any number of times, and two combinations are the same when they use the same numbers the same number of times, in any order: [2, 2, 3] and [3, 2, 2] count once. Each combination is written in ascending order, and the combinations are returned in lexicographic order, compared number by number. When nothing adds up to target, the result is an empty slice.

    For candidates = [2, 3, 6, 7] and target = 7, the answer is [[2 2 3] [7]]: 2 + 2 + 3 and 7 alone. For candidates = [5, 2, 3] and target = 8, the answer is [[2 2 2 2] [2 3 3] [3 5]], even though the input isn't sorted. For candidates = [2] and target = 1, the answer is [].

    Approach: build a combination one number at a time

    This is a search problem, and backtracking fits it. Start with an empty combination and a remaining amount equal to target. At each step, try adding one candidate: subtract it from the remaining amount, build on the result, and then remove the candidate again to try the next one. When the remaining amount reaches exactly 0, the combination so far is a valid answer.

    Three details make the search produce each combination once, in the required order:

    • Sort the candidates first. Combinations are then built in ascending order, and the results come out in lexicographic order without a separate sort.
    • Only move forward. After choosing the candidate at index j, the next number is chosen from index j onward, never from an earlier index. Index j itself is allowed, because a number can repeat. This builds [2, 2, 3] and never [3, 2, 2], so no combination appears twice.
    • Stop early. Because the candidates are sorted, once one is bigger than the remaining amount, all later ones are too. The loop can break instead of trying them.

    The search is exponential. With n candidates, a smallest candidate m and a target t, a combination has at most t / m numbers, so the time is O(n^(t/m)) in the worst case. The constraints (at most 30 candidates, all at least 2, and a target of at most 40) keep real inputs small.

    Step by step

    The outer function

    1. Sort the candidates with sort.Ints, which sorts in place in ascending order.
    2. Create result as [][]int{}, a slice of combinations, each of which is a slice of ints.
    3. Start the search with backtrack: the candidates, target as the amount still needed, 0 as the first usable index, an empty combination, and &result. The helper needs the address because append can return a new slice, and the new one has to be stored where combinationSum can see it.
    4. Return result.

    The helper backtrack

    1. Write the signature. remaining is what the combination still needs, i is the first index it may use, current is the combination built so far, and result is a *[][]int, a pointer to the slice of combinations. It returns nothing.
    2. Test for a full combination: if remaining == 0.
    3. Save a copy. Inside the test, append a copy of current to *result and store the new slice back in *result. Then return. Every candidate is at least 2, so adding more numbers would only go past the target.
    4. Loop over the usable candidates: for j := i; j < len(candidates); j++.
    5. Stop at the first candidate that is too big: if candidates[j] > remaining, break.
    6. Choose it: append candidates[j] to current and keep the longer slice.
    7. Recurse with remaining-candidates[j] and j as the first index, so the same number can be chosen again. result is already a pointer, so it is passed on as it is.
    8. Undo the choice: current = current[:len(current)-1] drops the last number, so the loop can try the next candidate in its place.

    The solution in Go

    func combinationSum(candidates []int, target int) [][]int {
    	sort.Ints(candidates)
    	result := [][]int{}
    	backtrack(candidates, target, 0, []int{}, &result)
    	return result
    }
    
    func backtrack(candidates []int, remaining int, i int, current []int, result *[][]int) {
    	if remaining == 0 {
    		*result = append(*result, append([]int{}, current...))
    		return
    	}
    	for j := i; j < len(candidates); j++ {
    		if candidates[j] > remaining {
    			break
    		}
    		current = append(current, candidates[j])
    		backtrack(candidates, remaining-candidates[j], j, current, result)
    		current = current[:len(current)-1]
    	}
    }
    

    Go details worth knowing:

    • Why the copy matters. A slice is a small header that points at a backing array. current keeps being shortened and extended over the same array, so a slice saved without copying changes later. This program saves cur and then reuses its array:
      var res [][]int
      cur := make([]int, 0, 4)
      cur = append(cur, 1, 2)
      res = append(res, cur)
      cur = cur[:1]
      cur = append(cur, 9)
      fmt.Println(res)
      
      It prints [[1 9]], not [[1 2]]. append([]int{}, current...) builds a new slice with its own array, so the saved combination stays as it was.
    • Why a pointer to the result. Go passes slices by value, so if backtrack appended to its own copy of result, the caller wouldn't see the new length. Passing *[][]int and appending through *result updates the caller's variable.
    • sort.Ints sorts the caller's slice. After the call, the original candidates slice is in ascending order: [5, 2, 3] becomes [2 3 5].
    • Empty result. result starts as [][]int{}, so with no combinations the function returns an empty, non-nil slice, printed as [].

    Complexity

    Time: O(n^(t/m)) in the worst case, for n candidates, target t and smallest candidate m. The recursion is at most t / m levels deep, and each level can choose among at most n candidates. Sorting costs O(n log n) and is small next to the search. The break and the forward-only index prune the tree a lot in practice.

    Space: beyond the answer itself, O(t / m): the combination being built and one call per number in it.

    Edge cases and common mistakes

    • No combination. [3, 5] with target 7 returns an empty slice, with length 0. [2] with target 1 does too, because the only candidate is already bigger than the target and the loop breaks at once.
    • One number repeated. [2] with target 4 returns [[2 2]]. Passing j to the recursive call allows the repeat. Passing j + 1 would use each candidate once and miss this answer.
    • Starting the loop at 0 in every call. It finds [2, 2, 3], [2, 3, 2] and [3, 2, 2] as separate answers.
    • Saving current without copying. The saved combinations are overwritten by later work, as the example above shows, and the final answer holds wrong or repeated rows.
    • Forgetting the sort. The break relies on the candidates being in ascending order: on an unsorted slice like [5, 2, 3] it would stop at the first big number and skip smaller ones after it.
    • Forgetting the undo line. The combination keeps growing, and later candidates are added after numbers that should have been removed.

    Testing

    • candidates = [2, 3, 6, 7], target = 7 gives [[2 2 3] [7]].
    • candidates = [5, 2, 3], target = 8 gives [[2 2 2 2] [2 3 3] [3 5]].
    • candidates = [2, 3, 5], target = 8 gives [[2 2 2 2] [2 3 3] [3 5]].
    • candidates = [2], target = 1 gives [].
    • candidates = [2], target = 4 gives [[2 2]].
    • candidates = [3, 5], target = 7 gives [].
    Keep going with Go500+ step-by-step lessons, from your first line of code to coding interviews. Free to start.
    Start learning free