Problem overview
The function subsetsWithDup receives a list nums 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, whichever copy of a repeated number they use. In [1, 2, 2], taking the first 2 or the second 2 gives the same subset [2], and it must appear once.
The answer is also ordered. Each subset is written in ascending order, and the subsets are sorted lexicographically: compare number by number, and when one subset is the beginning of another, the shorter one 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 by how many 4s it has, from 0 to 4, so there are 2 · 5 = 10 subsets.
Approach: grow subsets with a loop, skip repeated values
First, sort nums. Equal numbers then sit side by side, so a repeat can be spotted by comparing a number with the one before it. Sorting also makes every subset come out in ascending order.
The search is a function that receives start, the first index it may still use, and current, the subset built so far. Every subset the search builds is an answer, not only the full ones, so each call first saves a copy of current. Then a loop picks 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. Passing i + 1 means each position of nums is used at most once and the numbers of a subset stay in index order.
Duplicates are handled by one line: inside the loop, skip nums[i] when i > start and it equals nums[i - 1]. Within one call, each turn of the loop chooses a different next number. Choosing a second copy of a value that an earlier turn of the same loop already chose would rebuild exactly the same subsets. A copy is still allowed when it is the first one the call tries, at i == start, and that is how [2, 2] gets its second 2.
The search on [1, 2, 2] behaves like this. Each line shows the depth of the call:
depth 0: save []
depth 1: save [1]
depth 2: save [1, 2]
depth 3: save [1, 2, 2]
depth 1: skip i=2 (value 2)
depth 1: save [2]
depth 2: save [2, 2]
depth 0: skip i=2 (value 2)
After [1, 2] and [1, 2, 2] are saved, the call that holds [1] reaches the second 2 at i = 2. That 2 is not the first number this call tried, so it is skipped: [1, 2] already exists. The saves come out in exactly the order the statement asks for.
The idea is the loop form used for Combination Sum. A skip-or-take form, where each call decides about one number, also finds subsets, but with duplicates it is harder to keep them distinct. In the loop form all the choices for one position are made in one loop, where equal numbers are side by side.
Step by step
- Sort
numsin place withsort(), so repeats are adjacent. - Create
result, an empty list of subsets. - Start the search with
self.backtrack(nums, 0, [], result): from index 0, with an empty subset. - Return
result. The helper adds to this same list. - Declare the helper
backtrackwithnums,start,currentandresult. - Save a copy of
currentat the top withcurrent.copy(). There is noifaround it, since every call's subset is an answer. A copy is needed becausecurrentkeeps changing during the search. - Loop
ioverrange(start, len(nums)), the indexes whose numbers could come next. Starting atstartinstead of 0 keeps each subset in index order. Whenstartis past the last index the loop doesn't run, and that is where the search stops. - Skip repeats. If
i > startandnums[i] == nums[i - 1], usecontinue. - Choose
nums[i]by appending it tocurrent. - Recurse with
nums,i + 1,currentandresult. - Undo the choice with
current.pop(), so the loop can try the next number withcurrentrestored.
The solution in Python
class Solution:
def subsetsWithDup(self, nums: List[int]) -> List[List[int]]:
nums.sort()
result = []
self.backtrack(nums, 0, [], result)
return result
def backtrack(self, nums, start, current, result):
result.append(current.copy())
for i in range(start, len(nums)):
if i > start and nums[i] == nums[i - 1]:
continue
current.append(nums[i])
self.backtrack(nums, i + 1, current, result)
current.pop()
The Python details worth a closer look:
- Why
copy().currentis one list that the whole search shares and keeps changing. Appending it directly would store the same list object many times. Without.copy(), the finalresultfor[1, 2, 2]is six empty lists, because by the end everypop()has emptied that one list.current[:]orlist(current)also make a copy. sort()works in place. It sorts the caller's list and returnsNone. AftersubsetsWithDup([2, 1, 2]), the list that was passed in reads[1, 2, 2].sorted(nums)would return a new list instead, if the input must stay untouched.continueinside aforloop. It skips the rest of this turn and moves to the nexti, so nothing is appended or undone for a skipped number.- Negative numbers. Sorting handles them like any others:
[-1, -1, 3]gives[[], [-1], [-1, -1], [-1, -1, 3], [-1, 3], [3]].
Complexity
Time: O(n · 2^n). A list of n numbers has at most 2^n subsets, and copying one takes up to n steps. Sorting adds O(n log n), which is smaller. With repeated values the number of distinct subsets, and so the work, is lower than that bound.
Space: besides the answer, O(n): the subset being built and one active call per number in it.
Edge cases and common mistakes
- The empty subset. It is always part of the answer and always first.
[0]gives[[], [0]]. - Appending without a copy. For
[1, 2, 2]this returns[[], [], [], [], [], []]. - Forgetting the skip. Without the duplicate test,
[1, 2, 2]returns[[], [1], [1, 2], [1, 2, 2], [1, 2], [2], [2, 2], [2]]:[1, 2]and[2]appear twice. - Comparing with 0 instead of
start. A testi > 0skips too much. For[1, 2, 2]it returns[[], [1], [1, 2], [2]]and loses[1, 2, 2]and[2, 2], because the second 2 must be allowed when it is the first number a call tries. - Not sorting. Without sorting, equal numbers need not be adjacent, so the comparison with
nums[i - 1]misses repeats, and the output order is not the required one. - Passing
iinstead ofi + 1. That would let one position be used again and again.
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].[1, 1, 2, 2]gives 9 subsets:[[], [1], [1, 1], [1, 1, 2], [1, 1, 2, 2], [1, 2], [1, 2, 2], [2], [2, 2]].[1, 2, 3]gives all 8 subsets, since nothing repeats.
