Problem overview
The function subsets receives a vector<int> named nums of distinct integers and returns every subset of it, from the empty subset to nums itself, as a vector<vector<int>>. Inside each subset the numbers keep the order they have in nums.
The order of the subsets is fixed too. The numbers are decided one at a time, skipping before taking: every subset without nums[0] comes before every subset with it, and inside each of those groups the subsets without nums[1] come before the ones with it, and so on.
For nums = {1, 2, 3} the answer is:
[[],[3],[2],[2,3],[1],[1,3],[1,2],[1,2,3]]
The first four subsets skip 1 and the last four take it. Among the first four, [] and [3] skip 2, while [2] and [2,3] take it. For {0} the answer is [[],[0]], and for {3, -1} it is [[],[-1],[3],[3,-1]]: the last subset is written in the order of nums, not sorted.
Approach: skip or take, one number at a time
A subset is a series of yes-or-no decisions, one per number: is this number in or out? With n numbers there are 2^n series, so the answer has exactly that many subsets, and any solution has to build all of them. The question is how to produce each decision series once, in the required order.
Backtracking does it with recursion. Keep a vector current with the numbers taken so far, and a call that decides about index i:
- If
ihas reached the end ofnums, every number has been decided, andcurrentis a finished subset. Save a copy. - Otherwise, first skip
nums[i]: go on toi + 1withcurrentunchanged. - Then take it: add it to
current, go on toi + 1, and remove it again.
The removal is the "back" in backtracking. When the call that took nums[i] returns, current has to look as it did when the call started, because the caller carries on with it.
Trying skip before take gives the order the statement wants: the whole skip branch, with all its subsets, is finished before the take branch starts. The order is the same as counting in binary with 0 for skip and 1 for take, with the first number as the leftmost bit.
Step by step
- Create
result, an emptyvector<vector<int>>. Each subset is a list of numbers, so the result holds lists. - Create
current, an emptyvector<int>: the subset being built. Nothing is taken at the start. - Start the search with
backtrack(nums, 0, current, result). Index 0 is the first number to decide about, and the helper adds every subset that completes what it gets. - Return
result. The helper added to this same vector, so it now holds everything. - Declare the helper
backtrackinside the class, returningvoid, withnums, the indexi,currentandresult. The vectors are passed by reference so that every call sees and changes the same ones. - Check for the end: if
i == nums.size(), no number is left to decide about. - Save and return.
result.push_back(current)stores a copy ofcurrent, so the subset stays inresultwhilecurrentgoes on changing. - Skip first: call
backtrackwithi + 1, leavingcurrentalone. This finds every subset that doesn't containnums[i]. - Take:
push_backthe number ontocurrent. Numbers are added in index order, so each subset keeps the order ofnums. - Recurse again with
i + 1. This call finds every subset that containsnums[i]. - Undo:
current.pop_back()removes the number, socurrentis what this call received.
The solution in C++
class Solution {
public:
vector<vector<int>> subsets(vector<int>& nums) {
vector<vector<int>> result;
vector<int> current;
backtrack(nums, 0, current, result);
return result;
}
void backtrack(vector<int>& nums, int i, vector<int>& current, vector<vector<int>>& result) {
if (i == nums.size()) {
result.push_back(current);
return;
}
backtrack(nums, i + 1, current, result);
current.push_back(nums[i]);
backtrack(nums, i + 1, current, result);
current.pop_back();
}
};
The C++ details worth a closer look:
- References for the shared state.
currentandresultare passed asvector<int>&andvector<vector<int>>&. Without the&each call would work on its own copy:resultwould stay empty in the caller, and copying would cost extra time at every call. push_backcopies.result.push_back(current)makes a new vector insideresultwith the current contents. Laterpush_backandpop_backcalls oncurrentdon't touch the saved subsets.i == nums.size()compares anintwith an unsigned value. Hereiis never negative, so the comparison is correct, though some compilers warn about the mix.- The empty subset. The first subset saved is the one where every number was skipped, and
currentis empty at that moment, so[]comes first without any special case. - Members calling members.
subsetscallsbacktrack, which is declared below it. Inside a class definition the order of member functions doesn't matter.
Complexity
Time: O(n · 2^n). There are 2^n subsets, and saving each one copies up to n numbers. The recursion itself makes fewer than 2^(n + 1) calls, each doing constant work apart from the copy at the end.
Space: O(n) extra. Besides the answer, the program holds current, at most n numbers, and one call per index on the stack, so the recursion is at most n + 1 calls deep.
Edge cases and common mistakes
- Forgetting
pop_back(). The taken number stays incurrentfor the rest of the search. For{1, 2, 3}the first calls then produce[[],[3],[3,2],[3,2,3],[3,2,3,1],[3,2,3,1,3],[3,2,3,1,3,2],[3,2,3,1,3,2,3]], which has repeated numbers and the wrong subsets. - Taking before skipping. The subsets are all there, but in a different order, so the lesson's tests fail. The skip call has to come first.
- Passing
currentby value. If the helper takesvector<int> currentwithout the&, the undo isn't needed, because every call changes its own copy. The answer is correct, but every call copies the vector, which is extra work. - Saving at the wrong moment. Saving a subset at every call, instead of only when
ireaches the end, gives duplicates. - Sorting the subsets' numbers. Every subset keeps the order of
nums:{3, -1}gives[3,-1], not[-1,3]. - One number.
{0}gives[[],[0]]: two subsets, the empty one first. - The biggest input. With 10 numbers the answer has 1,024 subsets. The first one is
[], and the last one is all ofnums.
Testing
nums = {1, 2, 3}gives[[],[3],[2],[2,3],[1],[1,3],[1,2],[1,2,3]].nums = {0}gives[[],[0]].nums = {3, -1}gives[[],[-1],[3],[3,-1]].nums = {5, 4, 3, 2, 1, 0, -1, -2, -3, -4}gives 1,024 subsets, the first[]and the last[5,4,3,2,1,0,-1,-2,-3,-4].
