Start learning free

LESSON 364 OF 572 · BACKTRACKING

Subsets II in C#: Backtracking and Skipping Duplicates, O(n * 2^n)

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

Given an array nums of integers that may contain duplicates, return every distinct subset of nums: every way to pick some of its numbers, from none of them (the empty subset) to all of them.

Two subsets are the same when they hold the same numbers the same number of times, whichever copies of a repeated number they use: in [1,2,2], picking the first 2 or the second 2 gives the same subset [2], and it must appear only once.

Sort nums first, and write the numbers of each subset in ascending order. Return the subsets in lexicographic order: compare two subsets number by number, and the first position where they differ decides which one comes first; when one subset is the beginning of the other, the shorter one comes first. So the empty subset always comes first. This is the order in which the search described in the note below finds them.

 

Example 1:

Input: nums = [1,2,2]
Output: [[],[1],[1,2],[1,2,2],[2],[2,2]]
Explanation: The two 2s are alike, so [1,2] and [2] each appear once, whichever 2 they use.
[1] is the beginning of [1,2], so it comes first; every subset that starts with 1 comes before the ones that start with 2.

Example 2:

Input: nums = [0]
Output: [[],[0]]
Explanation: With one number there are two subsets: the empty one, then the number itself.

Example 3:

Input: nums = [4,4,4,1,4]
Output: [[],[1],[1,4],[1,4,4],[1,4,4,4],[1,4,4,4,4],[4],[4,4],[4,4,4],[4,4,4,4]]
Explanation: Sorted, nums is [1,4,4,4,4]. A subset is fixed by whether it has the 1 and by how many 4s it has, from 0 to 4: 2 · 5 = 10 subsets.

 

Constraints:

  • 1 <= nums.length <= 10
  • -10 <= nums[i] <= 10
  • The same number may appear several times in nums.

Note:

The tutorial uses backtracking in the loop form of Combination Sum. Sort nums, so that equal numbers sit next to each other. A call gets start, the first index it may still use, and current, the subset built so far. It first saves a copy of current: every subset the search builds is an answer, not only the full ones. Then a loop chooses the next number of the subset: for each index i from start on, it adds nums[i], calls itself with i + 1, and removes the number again.

The duplicates are handled by one line: the loop skips nums[i] when i > start and nums[i] equals nums[i - 1]. Inside one call, every turn of the loop picks a different next number, and taking a second copy of the same value as the next number would build exactly the same subsets again. A copy is still taken when it is the first one this call tries, at i = start: that is how [2,2] gets its second 2.

Subsets used the other form: each call decided about one number, skip or take, and only the calls that had decided about every number saved a subset. With distinct numbers, both forms find the same subsets, only in a different order. The loop form makes duplicates easy: all the choices for one position of the subset are made in one loop, where equal numbers are side by side and the repeats can be skipped.

There are at most 2n subsets and copying one takes up to n steps, so the time is O(n · 2n), sorting included. Besides the answer, the extra space is O(n): the subset being built and one call per number in it.

STEP 1 OF 11
    Ctrl + Enter to check

    C# · BACKTRACKING

    Subsets Ii in C#, explained

    Problem overview

    The method SubsetsWithDup receives an int[] named nums that may repeat values, and returns every distinct subset as a List<List<int>>: every way of keeping some of the numbers, from none (the empty subset) to all. Two subsets count as the same when they hold the same numbers the same number of times. In [1,2,2], keeping the first 2 or the second 2 gives the same subset [2], and it must appear once.

    The numbers of each subset are in ascending order, and the subsets come in lexicographic order: the first position where two subsets differ decides, and a subset that is the beginning of another comes first. For [1,2,2] the result is [[],[1],[1,2],[1,2,2],[2],[2,2]]. For [4,4,4,1,4] there are 10 subsets. Sorted, the array is [1,4,4,4,4], and a subset is fixed by whether it holds the 1 (2 choices) and how many 4s it holds (0 to 4, so 5 choices).

    Approach: grow a subset by choosing its next number

    Every subset the search builds is an answer, so each call of the recursive helper first saves a copy of the subset it was given. After that, a loop picks the next number of the subset: for each index i from start on, add nums[i], call the helper with i + 1 so that later numbers come only from after this one, and then remove the number again. Starting at start and never at 0 keeps every subset in index order, so each choice of indexes is built once.

    Duplicates need one extra rule, and sorting makes it simple. After Array.Sort, equal numbers sit side by side. Inside one call, each turn of the loop picks a different candidate for the next position, and trying a value that the previous turn already tried would rebuild exactly the same subsets. So the loop skips nums[i] when i > start and it equals nums[i - 1]. The first copy this call tries, at i == start, is always taken. That is how [2,2] gets its second 2: the deeper call starts after the first 2, and there the second 2 is at start.

    Sorting also gives the required order. The search finds the empty subset first, and then the subsets starting with the smallest number, each followed by its extensions, so the results are already lexicographic.

    Step by step

    1. Sort nums with Array.Sort. It sorts in place in ascending order.
    2. Create result, a list of lists.
    3. Start the search with Backtrack(nums, 0, new List<int>(), result): nothing chosen yet, and every index from 0 is available.
    4. Return result.
    5. Write Backtrack. It takes the array, start, the current subset and the result.
    6. Save a copy of current at the top with new List<int>(current). There is no condition around it, because every call's subset is an answer.
    7. Loop i from start to the end of the array.
    8. Skip repeats: if i > start and nums[i] == nums[i - 1], continue.
    9. Choose nums[i] with Add, recurse with i + 1, and undo the choice with RemoveAt(current.Count - 1).

    The solution in C#

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

    The C# details worth knowing:

    • Why the copy matters. List<int> is a reference type. result.Add(current) would store the same list object every time, and since the search keeps adding and removing numbers in it, every entry of result ends up showing whatever current holds at the end, an empty list. new List<int>(current) is a snapshot that stays as it is.
    • Array.Sort changes the caller's array. It sorts in place, so after the call the original nums is in ascending order.
    • RemoveAt(current.Count - 1) removes the last element by index. Calling Remove(nums[i]) would remove the first element equal to that value, which isn't always the last one.
    • Nested generics. List<List<int>> is a list whose elements are lists of int. The two closing angle brackets are written together without a space.

    Complexity

    Time: O(n * 2^n). There are at most 2^n subsets, and copying one takes up to n steps. Sorting is O(n log n) and doesn't change the bound. Duplicates only reduce the count: ten equal numbers give 11 subsets, not 1024.

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

    Edge cases and common mistakes

    • One element. [0] gives [[],[0]].
    • Unsorted input. [2,1,2] gives the same answer as [1,2,2], because the method sorts first. Without sorting, equal numbers are not always side by side, and the skip rule can't see all repeats.
    • Forgetting the skip. On [1,2,2] the result has 8 entries, with [1,2] and [2] twice.
    • Skipping with i > 0 instead of i > start. The second 2 is then never taken after the first one, and [1,2,2] loses [1,2,2] and [2,2], leaving [[],[1],[1,2],[2]].
    • Adding current without copying. Every entry of the result ends up the same list, the empty one.
    • Saving only at the end. Only full-length subsets would be saved, but the answer needs all of them.

    Testing

    • [1,2,2] gives [[],[1],[1,2],[1,2,2],[2],[2,2]].
    • [0] gives [[],[0]].
    • [4,4,4,1,4] gives [[],[1],[1,4],[1,4,4],[1,4,4,4],[1,4,4,4,4],[4],[4,4],[4,4,4],[4,4,4,4]].
    • [2,1,2] gives [[],[1],[1,2],[1,2,2],[2],[2,2]].
    • Ten different numbers give 1024 subsets, and ten equal numbers give 11.
    Keep going with C#500+ step-by-step lessons, from your first line of code to coding interviews. Free to start.
    Start learning free