Start learning free

LESSON 365 OF 572 · BACKTRACKING

Combination Sum II in C#: Backtracking Without Repeats, O(n * 2^n)

Practice O(n · 2n) Read the full explanation

Given an array of integers candidates, which may contain duplicates, and an integer target, return every distinct combination of candidates that adds up to target.

Each candidate may be used at most once in a combination, so a number that appears twice in candidates can appear up to twice in one combination. Two combinations are the same when they hold the same numbers the same number of times, whichever copies of a repeated number they use: in [1,1,7] with target = 8, the first 1 with the 7 and the second 1 with the 7 are one combination, [1,7], 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. This is the order in which the search described in the note below finds them. If no combination adds up to target, return an empty array.

 

Example 1:

Input: candidates = [10,1,2,7,6,1,5], target = 8
Output: [[1,1,6],[1,2,5],[1,7],[2,6]]
Explanation: Sorted, the candidates are [1,1,2,5,6,7,10]. [1,1,6] uses both 1s.
[1,7] can be made with either 1, but it is listed once. [1,2,5] comes before [1,7] because at the second number, 2 is smaller than 7.

Example 2:

Input: candidates = [2,5,2,1,2], target = 5
Output: [[1,2,2],[5]]
Explanation: There are three 2s; [1,2,2] uses two of them and appears once. 5 alone is 5.

Example 3:

Input: candidates = [2,3], target = 4
Output: []
Explanation: 2 + 3 = 5 is too much, and the 2 can't be used twice, so no combination adds up to 4.

 

Constraints:

  • 1 <= candidates.length <= 100
  • 1 <= candidates[i] <= 50
  • 1 <= target <= 30

Note:

The tutorial uses backtracking, as in Combination Sum. Sort candidates. A call gets start, the first index it may still use, remaining, what the combination still needs, and current, the combination built so far. When remaining is 0, it saves a copy of current. Otherwise a loop tries each index i from start on as the next number: it adds candidates[i], calls itself with i + 1 and remaining - candidates[i], and removes the number again. Because the candidates are sorted, the loop can stop at the first one that is bigger than remaining.

Two things change from Combination Sum. First, the deeper call starts at i + 1, not at i: there, a number could be chosen again and again; here, each candidate is used at most once. Second, as in Subsets II, the loop skips candidates[i] when i > start and it equals candidates[i - 1]. Inside one call, every turn of the loop picks a different next number, and a second copy of the same value would build exactly the same combinations again. A copy is still taken when it is the first one this call tries, at i = start: that is how [1,1,6] gets its second 1.

The search is exponential: with n candidates, there are at most 2n ways to pick some of them, and copying a combination takes up to n steps, so the time is O(n · 2n) in the worst case. The sorted order keeps it far smaller in practice: the calls only build distinct combinations whose sum is at most target. Besides the answer, the extra space is O(n): the combination being built and one call per number in it.

STEP 1 OF 13
    Ctrl + Enter to check

    C# · BACKTRACKING

    Combination Sum Ii in C#, explained

    Problem overview

    The method CombinationSum2 receives an int[] named candidates, which may contain repeated values, and an int named target. It returns every distinct combination of candidates that adds up to target as a List<List<int>>. Each candidate can be used at most once, so a value that appears twice in the array can appear up to twice in one combination. Two combinations are the same when they hold the same numbers the same number of times, whichever copies were used. Each combination is written in ascending order, and the combinations come in lexicographic order. If nothing adds up to the target, the result is an empty list.

    For [10,1,2,7,6,1,5] and target = 8 the result is [[1,1,6],[1,2,5],[1,7],[2,6]]. The combination [1,7] can be made with either 1 but appears once. For [2,5,2,1,2] and 5 it is [[1,2,2],[5]], and for [2,3] and 4 it is []: 2 + 3 is 5, and the 2 can't be used twice.

    Approach: sort, choose the next number, skip repeated values

    The search builds a combination one number at a time. A call receives start, the first index it may still use, remaining, the amount the combination still needs, and current, the numbers chosen so far. When remaining is 0, current adds up to the target and a copy of it is saved. Otherwise a loop tries each index i from start on as the next number: add candidates[i], call itself with i + 1 and remaining - candidates[i], and remove the number again.

    Three ideas keep the search correct and small:

    • Sorting. After Array.Sort, equal numbers are neighbours, combinations come out in ascending order, and larger numbers sit at the end, which allows an early exit.
    • Passing i + 1, not i. The next number must come from after the one just taken, so each candidate is used at most once. A version that allows reuse passes i.
    • Skipping repeated values. Inside one call, every turn of the loop picks a different number for the next position. A second copy of a value that the previous turn already tried would rebuild the same combinations, so the loop skips candidates[i] when i > start and it equals candidates[i - 1]. The first copy, at i == start, is taken: that is how [1,1,6] gets its second 1.

    Because the array is sorted, once candidates[i] is larger than remaining, every later number is larger too, and the loop stops with break.

    Step by step

    1. Sort candidates with Array.Sort.
    2. Create result, a list of lists.
    3. Start the search with Backtrack(candidates, 0, target, new List<int>(), result): nothing chosen, all of target still needed, every index available.
    4. Return result.
    5. Write Backtrack. If remaining == 0, add a copy of current and return. All candidates are at least 1, so adding another number could only overshoot.
    6. Loop i from start to the end of the array.
    7. Skip repeats: if i > start and candidates[i] == candidates[i - 1], continue.
    8. Stop early: if candidates[i] > remaining, break.
    9. Choose with Add, recurse with i + 1 and the reduced remainder, and undo with RemoveAt(current.Count - 1).

    The solution in C#

    class Solution {
        public List<List<int>> CombinationSum2(int[] candidates, int target) {
            Array.Sort(candidates);
            List<List<int>> result = new List<List<int>>();
            Backtrack(candidates, 0, target, new List<int>(), result);
            return result;
        }
    
        public void Backtrack(int[] candidates, int start, int remaining, List<int> current, List<List<int>> result) {
            if (remaining == 0) {
                result.Add(new List<int>(current));
                return;
            }
            for (int i = start; i < candidates.Length; i++) {
                if (i > start && candidates[i] == candidates[i - 1]) {
                    continue;
                }
                if (candidates[i] > remaining) {
                    break;
                }
                current.Add(candidates[i]);
                Backtrack(candidates, i + 1, remaining - candidates[i], current, result);
                current.RemoveAt(current.Count - 1);
            }
        }
    }
    

    The C# details worth knowing:

    • The snapshot. List<int> is a reference type, and the search keeps changing current. new List<int>(current) copies its numbers into a new list, so the saved combination doesn't change later.
    • break against continue. continue ends this turn of the loop and goes on with the next i, which suits the repeat check. break leaves the loop altogether, which is right for "too big", because the sorted array has nothing smaller further on.
    • Array.Sort sorts the caller's array in place.

    Complexity

    Time: O(n * 2^n) in the worst case. With n candidates there are at most 2^n ways to pick some of them, and copying a combination takes up to n steps. The sort adds O(n log n). In practice the search is much smaller: it only continues through distinct combinations whose sum doesn't exceed target, and the break cuts the rest.

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

    Edge cases and common mistakes

    • No combination. [2,3] with target 4 returns an empty list.
    • Single candidate equal to the target. [5] with target 5 returns [[5]].
    • Many copies of one value. [1,1,1,1] with target 2 returns [[1,1]]: one combination, however many pairs of 1s could make it.
    • Skipping too much. The check i > 0 instead of i > start forbids a second copy even when the first copy is in the combination. For [10,1,2,7,6,1,5] and 8, [1,1,6] is lost and the result is [[1,2,5],[1,7],[2,6]].
    • Not skipping. For [1,1,7] and 8, [1,7] is returned twice.
    • Passing i instead of i + 1. A candidate can then be chosen again and again: for [2,3] and 4 the result is [[2,2]], but there is only one 2.

    Testing

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