Start learning free

LESSON 360 OF 568 · BACKTRACKING

Subsets II in C++: Backtracking with Duplicate Skipping

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

Given a vector 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 12
    Ctrl + Enter to check

    C++ · BACKTRACKING

    Subsets Ii in C++, explained

    Problem overview

    The method subsetsWithDup receives a vector<int> named nums, which may contain repeated numbers, and returns every distinct subset as a vector<vector<int>>. A subset is any selection of the numbers, from none (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], taking the first 2 or the second 2 gives the same subset [2], which must appear once.

    The answer has a fixed order. Each subset is written in ascending order, and the subsets are in lexicographic order: the first position where two subsets differ decides, and when one is the beginning of the other, the shorter comes first. For [1,2,2] the result is [[],[1],[1,2],[1,2,2],[2],[2,2]]. For [4,4,4,1,4], which sorts to [1,4,4,4,4], a subset is fixed by whether it has the 1 and how many 4s it has (0 to 4), so there are 2 × 5 = 10 subsets.

    Approach: build subsets by choosing the next number

    Sort first. Equal numbers then sit next to each other, which makes a repeat easy to spot, and the sorted order gives each subset its ascending order.

    The search works like this. A call receives start, the first index it may still use, and current, the subset built so far. Every subset the search builds is an answer, so the call saves a copy of current right away. 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 to grow the subset further, and removes the number again so the loop can try the following one. Starting at start and never going back keeps every subset in index order, and so in ascending order, and uses each position at most once.

    Without more care, repeated numbers produce repeated subsets. For [1,2,2] the loop at the top level would pick the first 2, then the second 2, and both would grow into [2]. The fix is one condition: inside a call, skip nums[i] when i > start and it equals nums[i - 1]. Every turn of the loop picks a different next number, and taking another copy of a value the loop already tried would rebuild the same subsets. A copy is still taken when it is the first one this call tries, at i == start, and that is how [2,2] gets its second 2.

    The order comes out right without extra work. The search saves a subset before extending it, so a subset appears before every longer subset that starts with it. The loop tries smaller numbers first, so the subsets that start with a smaller number come first.

    Step by step

    1. Sort nums with sort(nums.begin(), nums.end()).
    2. Create result, a vector<vector<int>> for the subsets found.
    3. Create current, an empty vector<int>: the subset being built.
    4. Start the search at index 0 with the empty subset.
    5. Return result after the call. The helper added to that same vector.
    6. Declare the helper. backtrack returns void and takes nums, start, current and result. The vectors are passed by reference because the helper changes current and result, and the caller must see it.
    7. Save the subset. The first line pushes current into result, with no condition: [], [1] and [1,2] are all answers.
    8. Loop with i from start to the end of nums. When start is past the last index, the loop doesn't run and the search stops there.
    9. Skip repeats. If i > start and nums[i] == nums[i - 1], continue.
    10. Take the number with current.push_back(nums[i]).
    11. Recurse with i + 1 as the new start.
    12. Undo with current.pop_back(), so current is what it was before this turn.

    The solution in C++

    class Solution {
    public:
        vector<vector<int>> subsetsWithDup(vector<int>& nums) {
            sort(nums.begin(), nums.end());
            vector<vector<int>> result;
            vector<int> current;
            backtrack(nums, 0, current, result);
            return result;
        }
    
        void backtrack(vector<int>& nums, int start, vector<int>& current, vector<vector<int>>& result) {
            result.push_back(current);
            for (int i = start; i < nums.size(); i++) {
                if (i > start && nums[i] == nums[i - 1]) {
                    continue;
                }
                current.push_back(nums[i]);
                backtrack(nums, i + 1, current, result);
                current.pop_back();
            }
        }
    };
    

    The C++ details worth a closer look:

    • push_back copies. result.push_back(current) stores a copy of the vector as it is at that moment. That is why current can keep changing afterwards without touching the saved subsets.
    • The same current is shared by every call. It is passed by reference, so a single vector is built up and torn down during the whole search. This is why each push_back needs its matching pop_back.
    • nums is sorted in place. The parameter is a non-const reference, so after the call the caller's vector is sorted too: calling the method on {2,1,2} leaves it as {1,2,2}.
    • Signed and unsigned. nums.size() is unsigned and i is an int. The loop is fine for lengths up to 10, and compilers may warn about the mixed comparison.

    Complexity

    Time: O(n · 2^n) for n numbers. There are at most 2^n subsets, and copying one into the result takes up to n steps. Sorting costs O(n log n), which is smaller.

    Space: O(n) besides the answer. The search is at most n calls deep, and current holds at most n numbers.

    Ten different numbers give the maximum of 1024 subsets. Ten equal numbers give only 11, one for each count from 0 to 10, and the duplicate rule is what keeps the search from building more.

    Edge cases and common mistakes

    • Comparing with i > 0 instead of i > start. That skips a repeat even when it is the first number a call tries, and subsets like [1,2,2] and [2,2] are lost. For [1,2,2] the result is only [[],[1],[1,2],[2]].
    • No skip at all. For [1,2,2] the result has 8 entries and repeats [1,2] and [2]: [[],[1],[1,2],[1,2,2],[1,2],[2],[2,2],[2]].
    • Forgetting to sort. The skip rule only works when equal numbers are adjacent. For {2,1,2} without sorting the search returns 8 subsets, with [2] twice, and the order isn't the required one.
    • Forgetting pop_back(). current would keep growing across turns of the loop, and the subsets would contain numbers that were never chosen together.
    • Passing i instead of i + 1. A number could then be reused within a subset.
    • Saving only at the end. Every partial subset is an answer, so the push into result goes at the top of the function, not inside a condition.
    • All numbers equal. [5,5,5] gives [[],[5],[5,5],[5,5,5]].

    Testing

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