Problem overview
The method subsetsWithDup receives nums, an array of integers that may repeat, and returns every distinct subset: every way to pick some 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. In [1, 2, 2], taking the first 2 or the second 2 gives the same subset [2], and it must be listed once.
Each subset is written in ascending order, and the subsets are returned 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 answer is [[], [1], [1, 2], [1, 2, 2], [2], [2, 2]]. For [4, 4, 4, 1, 4], which is [1, 4, 4, 4, 4] once sorted, a subset is fixed by whether it has the 1 and by how many 4s it has (0 to 4), so there are 2 · 5 = 10 subsets.
Approach: choose the next number in a loop
The search grows a subset one number at a time. A call receives start, the first index it may still use, and current, the subset built so far. It does two things:
- It saves a copy of
current. Every subset the search builds is an answer, not only the full ones, so there is no condition around this. - It loops over the indexes
ifromstarton. Each turn addsnums[i]tocurrent, calls itself withi + 1, and removes the number again.
Starting the loop at start and passing i + 1 means a subset takes its numbers in index order, so each index is used at most once and each subset is built only once from a given choice of indexes.
The duplicates need one more rule. Sort the array first, so equal numbers sit side by side. Inside one call, each turn of the loop picks a different next number for the subset. If nums[i] equals nums[i - 1] and i > start, the previous turn already tried that value as the next number, and trying it again would build exactly the same subsets. So the loop skips it. At i == start nothing was tried in this call yet, so the number is taken even when it equals the one before it. That is how [2, 2] gets its second 2: the first call takes the first 2, and the call below it, with start at the second 2, takes that one.
Sorting has a second benefit: it writes each subset in ascending order and produces the subsets in the lexicographic order the answer requires.
Step by step
- Sort a copy. Parameters are constants in Swift, so declare a new constant also named
nums, set tonums.sorted(). It hides the parameter for the rest of the method. - Create
result, an empty[[Int]]. - Create
current, an empty[Int]: the subset being built. - Start the search with
backtrack(nums, 0, ¤t, &result). Nothing is chosen yet, and every index from 0 on may be used. - Return
result. - Declare the helper
backtrackwithnums,start, and twoinoutparameters,currentandresult, so the calls change the same two arrays. - Save the subset at the top: append
currenttoresult. - Loop with
ioverstart..<nums.count. - Skip repeats: if
i > start && nums[i] == nums[i - 1], docontinue. - Choose
nums[i]by appending it tocurrent. - Recurse with
nums,i + 1,¤tand&result. - Undo the choice with
current.removeLast(), so the next turn of the loop starts from the same subset.
The solution in Swift
class Solution {
func subsetsWithDup(_ nums: [Int]) -> [[Int]] {
let nums = nums.sorted()
var result = [[Int]]()
var current = [Int]()
backtrack(nums, 0, ¤t, &result)
return result
}
func backtrack(_ nums: [Int], _ start: Int, _ current: inout [Int], _ result: inout [[Int]]) {
result.append(current)
for i in start..<nums.count {
if i > start && nums[i] == nums[i - 1] {
continue
}
current.append(nums[i])
backtrack(nums, i + 1, ¤t, &result)
current.removeLast()
}
}
}
The Swift details worth a closer look:
sorted()versussort().sorted()returns a new array and leaves the original alone, which suits a constant parameter.sort()sorts in place and needs avar.- Arrays are values.
result.append(current)stores a copy ofcurrent. LaterappendandremoveLastcalls oncurrentdon't change the subsets already saved. In a language where arrays are references, this line needs an explicit copy. inoutparameters. The helper changes the caller'scurrentandresult, and each call site marks them with&. The calls therefore work on one shared pair of arrays and no big copies are made.start..<nums.countat the end. Whenstartequalsnums.countthe range is empty and the loop does nothing, which is where the search stops. Sincestartis at mostnums.count, the range is never reversed. A reversed range such as5..<3would be a run-time error in Swift.
Complexity
Time: O(n · 2^n). There are at most 2^n subsets, and copying one into result takes up to n steps. Sorting costs O(n log n), which is smaller. Skipping repeats only makes the search smaller: for ten equal numbers the method builds 11 subsets, not 1,024. For ten different numbers there are 1,024.
Space: O(n) besides the answer: the subset being built and one call per number in it.
Edge cases and common mistakes
- No skip at all. On
[1, 2, 2]the result becomes[[], [1], [1, 2], [1, 2, 2], [1, 2], [2], [2, 2], [2]]:[1, 2]and[2]appear twice. - Skipping with
i > 0instead ofi > start. The second 2 can then never be taken after the first, so[1, 2, 2]gives[[], [1], [1, 2], [2]]and[1, 2, 2]and[2, 2]are lost. The test must compare withstart, because a repeat is only redundant when the same call already tried that value. - Forgetting to sort. The skip compares neighbours, so equal numbers must be side by side. With the skip but without the sort, the input
[2, 1, 2]returns[[], [2], [2, 1], [2, 1, 2], [2, 2], [1], [1, 2], [2]]:[2]appears twice, the subsets are not in ascending order, and the order of the answer is wrong. - Forgetting the undo. Without
removeLast()the numbers pile up incurrentand every later subset is wrong. - Saving only at the end. A subset is an answer at every call, including the very first one with the empty
current. The empty subset is always the first element of the result.
Testing
[1, 2, 2]gives[[], [1], [1, 2], [1, 2, 2], [2], [2, 2]].[0]gives[[], [0]].[2, 1, 2]gives[[], [1], [1, 2], [1, 2, 2], [2], [2, 2]], the same as[1, 2, 2].[3, 1]gives[[], [1], [1, 3], [3]].[4, 4, 4, 1, 4]gives 10 subsets: the empty one first and[4, 4, 4, 4]last.
