Problem overview
subsets receives a Vec<i32> of distinct numbers named nums and returns every subset: every way to pick some of the numbers, from none of them (the empty subset) to all of them. Each subset keeps the order the numbers have in nums, and the subsets must come in a fixed order. You decide about the numbers one at a time, and for each number you skip it before you take it. All subsets without nums[0] come before all subsets with it, and inside each group the same rule applies to nums[1], and so on.
For [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. The last four are the first four with 1 added in front. For [3, -1] the answer is [[], [-1], [3], [3, -1]]: the last subset is written [3, -1], in the order of nums, not sorted.
Approach: a yes or no for every number
A subset is a series of yes/no decisions, one for each number: take it or skip it. With n numbers there are 2^n series, so there are 2^n subsets, and any solution has to produce them all. For three numbers that is 8, and for ten it is 1,024.
Backtracking generates the series with a recursive function. It holds the index i of the number to decide about, and a vector current with the numbers taken so far. At each index it tries both choices:
- Skip: go on to index
i + 1withcurrentunchanged. - Take: add
nums[i]tocurrent, go on to indexi + 1, then remove the number again.
When i reaches the end of nums, every number was skipped or taken, and current is a finished subset. Trying skip before take gives exactly the order the statement asks for. It is the same order as counting in binary with 0 for skip and 1 for take.
Step by step
- Create
resultas a mutableVec::new(). Rust works out its type,Vec<Vec<i32>>, from how it is used below: each subset is a list of numbers, so the result holds lists. - Start the search with
Self::backtrack(&nums, 0, &mut Vec::new(), &mut result). Nothing is decided yet, so the index is 0 and the subset being built is a new empty vector.&lendsnumsfor reading, and&mutlends a vector that the call may change. - Return
resultwith no semicolon.backtrackadded to this same vector, so it now holds every subset. - Write the helper
backtrackwith the parametersnums,i(ausize, because it is used as an index),currentandresult. - Check for the end: if
i == nums.len(), no number is left to decide about. - Save a snapshot:
result.push(current.clone()), thenreturn.currentkeeps changing as the search goes on, so the result needs its own copy. - Skip
nums[i]by callingbacktrackwithi + 1and the samecurrent. Doing this first puts every subset withoutnums[i]before every subset with it. - Take
nums[i]withcurrent.push(nums[i]). Numbers are added in index order, so each subset keeps the order ofnums. - Recurse with
i + 1again. This call finds every subset that containsnums[i]. - Undo the choice with
current.pop(). This is the "back" in backtracking: the caller continues withcurrentexactly as it was before this call.
The calls for [1, 2, 3] reach the end of nums eight times, and the saved subsets come out in the order shown above.
The solution in Rust
impl Solution {
fn subsets(nums: Vec<i32>) -> Vec<Vec<i32>> {
let mut result = Vec::new();
Self::backtrack(&nums, 0, &mut Vec::new(), &mut result);
result
}
fn backtrack(nums: &Vec<i32>, i: usize, current: &mut Vec<i32>, result: &mut Vec<Vec<i32>>) {
if i == nums.len() {
result.push(current.clone());
return;
}
Self::backtrack(nums, i + 1, current, result);
current.push(nums[i]);
Self::backtrack(nums, i + 1, current, result);
current.pop();
}
}
The Rust details worth a closer look:
- Why
clone().currentis a&mut Vec<i32>, a borrowed vector, whileresultneeds ownedVec<i32>values. Writingresult.push(current)does not compile: the compiler reportsexpected Vec<i32>, found &mut Vec<i32>.current.clone()builds a new vector with the same numbers, which is the snapshot the result keeps. - Passing the borrows on. Inside
backtrack,nums,currentandresultare already references, so they are passed to the next call as they are, with no extra&or&mut. Rust reborrows them for the call, so you can use them again afterwards. - The starting vector.
&mut Vec::new()creates a temporary empty vector and lends it mutably for the call. Nothing else needs it afterwards. - Associated functions.
backtrackhas noself, so it is called asSelf::backtrack. - Index type.
iis ausize, the type Rust requires for indexing and forlen(), soi == nums.len()compares two values of the same type. - Return value.
resulthas no semicolon, so it is the valuesubsetsreturns.
Complexity
Time: O(n · 2^n). The recursion has a leaf for each of the 2^n subsets, and about twice as many calls in all. Copying a finished subset takes up to n steps, so the copies cost O(n · 2^n).
Space: O(n) besides the answer. The vector current holds at most n numbers, and the recursion is at most n + 1 calls deep. The answer itself holds 2^n subsets and is not counted.
Edge cases and common mistakes
- One number.
[0]gives[[], [0]]: the subset without it, then the subset with it. - The empty subset. It is part of the answer, and it comes first, because skipping every number is the first path the recursion finishes.
- Forgetting
pop(). Without the undo, the taken numbers pile up incurrent. For[1, 2, 3]the output becomes[[], [3], [3, 2], [3, 2, 3], [3, 2, 3, 1], ..., with repeated numbers. - Taking before skipping. The subsets are all there but in the wrong order, so the lesson's tests fail.
- Sorting the numbers. The subsets must follow the order of
nums. For[3, -1]the last subset is[3, -1], not[-1, 3]. - Stopping at
nums.len() - 1. The check must bei == nums.len(), so the last number also gets its skip and take. Stopping one step early loses half the subsets.
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]].- A vector of ten distinct numbers gives 1,024 subsets.
