Start learning free

LESSON 354 OF 539 · BACKTRACKING

Subsets in C#: Backtracking in O(n · 2^n)

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

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

Write the numbers of each subset in the order they have in nums. Return the subsets in the order you get by deciding about the numbers one at a time, skipping before taking: every subset without nums[0] comes before every subset with it; inside each of those two groups, the subsets without nums[1] come before the ones with it, then the same for nums[2], and so on. So the empty subset always comes first, and all of nums comes last.

 

Example 1:

Input: nums = [1,2,3]
Output: [[],[3],[2],[2,3],[1],[1,3],[1,2],[1,2,3]]
Explanation: The first four subsets skip 1, the last four take it.
Among the first four, [] and [3] skip 2, while [2] and [2,3] take it; in each pair, the one without 3 comes first.
The last four are the same four with 1 in front.

Example 2:

Input: nums = [0]
Output: [[],[0]]
Explanation: With one number there are two subsets: without it, then with it.

Example 3:

Input: nums = [3,-1]
Output: [[],[-1],[3],[3,-1]]
Explanation: The subsets without 3 come first. The last subset is written [3,-1], in the order of nums, not sorted.

 

Constraints:

  • 1 <= nums.length <= 10
  • -10 <= nums[i] <= 10
  • All the numbers in nums are distinct.

Note:

The tutorial uses backtracking. Walk through nums one index at a time, keeping the numbers taken so far in a list current. At index i, first skip nums[i]: go on to index i + 1 with current as it is. Then take it: add it to current, go on to index i + 1, and remove it again. When the index reaches the end of nums, every number has been skipped or taken: save a copy of current. Trying skip before take gives exactly the order above. It is the same choice as when you generate binary strings: a subset is a string of n bits, 0 for skip and 1 for take, and the subsets come out in the order of those strings.

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

STEP 1 OF 10
    Ctrl + Enter to check

    C# · BACKTRACKING

    Subsets in C#, explained

    Problem overview

    The method Subsets receives an int[] called nums of distinct numbers and returns every subset: every way to pick some of the numbers, from none (the empty subset) to all of them. The result is a List<List<int>>, one inner list per subset.

    Two rules fix the output exactly. Each subset lists its numbers in the order they have in nums, and the subsets come in the order you get by deciding about the numbers one at a time, skipping before taking. For {1, 2, 3} the result is [[],[3],[2],[2,3],[1],[1,3],[1,2],[1,2,3]]: the first four subsets skip 1, the last four take it. For {3, -1} it is [[],[-1],[3],[3,-1]], so the last subset is written in the order of nums, not sorted.

    Approach: skip or take, one number at a time

    A subset is the answer to one yes/no question per number: is it in or out? With n numbers that is n decisions, so there are 2^n subsets. Backtracking walks through all of those decision sequences.

    The search keeps a list current of the numbers taken so far and an index i of the number to decide about. At index i it tries two branches, in this order:

    1. Skip nums[i]: go on to i + 1 with current unchanged.
    2. Take it: add it to current, go on to i + 1, then remove it again.

    When i reaches the end of nums, every number has been decided and current is one finished subset. Trying skip before take puts every subset without nums[0] before every subset with it, then the same inside each group for nums[1], and so on, which is the order the lesson asks for. It is the same as counting in binary with 0 for skip and 1 for take.

    The removal after the take branch is the "back" in backtracking. The caller continues with current exactly as it was before the call, so one list can be shared by the whole search.

    Step by step

    1. Create result, an empty List<List<int>>. Each subset is itself a list of numbers, so the outer list holds lists.
    2. Start the search with Backtrack(nums, 0, new List<int>(), result): no number is decided yet, the subset is empty, and every number from index 0 on can be skipped or taken.
    3. Return result. Backtrack added to this same list, so it now holds every subset.
    4. Write Backtrack as a public void method with the parameters nums, i, current and result. Each call decides about nums[i] and calls itself for the next index.
    5. Stop at the end. If i == nums.Length, add a copy of current to result and return. The copy is new List<int>(current).
    6. Skip: call Backtrack(nums, i + 1, current, result) without touching current.
    7. Take: current.Add(nums[i]), then call Backtrack with i + 1 again. This time current holds nums[i].
    8. Undo: current.RemoveAt(current.Count - 1) removes the number just added, so current is again what this call received.

    The solution in C#

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

    The C# details worth knowing:

    • Why the copy matters. A List<int> is a reference type, so adding current itself to result would store a reference to the same object each time. The search keeps changing that object, and once everything is undone it is empty, so every stored entry would show an empty list. Adding current to a list and then changing it changes what the outer list shows: after al.Add(cur) and cur.Add(2) on a list that held only 1, the outer list prints [[1,2]]. new List<int>(current) copies the numbers into a separate list.
    • Lists and arrays. nums.Length is the length of an array, while a list has Count. Mixing them up is a compile error.
    • RemoveAt takes an index. current.RemoveAt(current.Count - 1) removes the last element. Remove(value) would search for a value instead, and an index of -1 would throw ArgumentOutOfRangeException.
    • Methods can call each other in any order. Subsets uses Backtrack before its definition appears, which is fine inside a class.
    • The helper's types. Because List<T> is a class, passing current and result to Backtrack passes references, not copies. That is why the helper can fill the caller's result without returning anything.

    Complexity

    Time: O(n · 2^n). The recursion has 2^n leaves, one per subset, and the number of calls in total is about twice that. Copying a finished subset takes up to n steps, which gives the factor n.

    Space: O(n) beyond the answer. current holds at most n numbers and the recursion is at most n + 1 calls deep. The answer itself holds 2^n lists and takes far more space than that, which is not counted as extra space.

    Edge cases and common mistakes

    • One number. {0} gives [[],[0]]: without it, then with it.
    • The empty subset is always first and the full list always last, because skipping comes first at every index.
    • Negative numbers. {3, -1} gives [[],[-1],[3],[3,-1]]. Nothing is sorted: each subset keeps the order of nums.
    • Forgetting the copy. Every entry of result becomes the same list, which ends up empty.
    • Forgetting the undo. current keeps growing, so later subsets contain numbers that were never taken in their branch.
    • Taking before skipping. The subsets are all there, but in the wrong order for the lesson's tests.

    Testing

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