Problem overview
The function subsetsWithDup receives a slice 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, no matter which copy of a repeated number they use. In [1, 2, 2] the first 2 and the second 2 each give the subset [2], and it must appear once.
The subsets are written in ascending order and returned in lexicographic order: compare two subsets number by number, the first difference 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 [0] it is [[] [0]].
A subset of repeated numbers is fixed by how many copies of each value it takes. For [4, 4, 4, 1, 4], sorted to [1, 4, 4, 4, 4], it is fixed by whether it has the 1 and how many 4s it has, from 0 to 4. That gives 2 × 5 = 10 subsets.
Approach: choose the next number in a loop
The lesson uses backtracking in a loop form. First sort nums, so equal numbers sit next to each other and each subset comes out in ascending order. A call to backtrack gets 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. - It loops over the indices
ifromstarton. Each turn chooses the next number of the subset: addnums[i], recurse withi + 1, then remove the number again.
Duplicates are handled by one line. Inside one call, each turn of the loop picks a different next number, and picking a second copy of the same value as the next number would rebuild exactly the same subsets. So the loop skips nums[i] when i > start and nums[i] == nums[i-1]. At i == start the number is the first one this call tries, so it is taken even if it equals the number before it. That is how [2, 2] gets its second 2: the recursive call after the first 2 starts at the index of the second 2.
A different form of backtracking decides, for each number, to take it or skip it. With distinct numbers both forms find the same subsets in a different order. The loop form is easier with duplicates, because all the choices for one position of the subset happen in one loop, where equal values are side by side and the repeats can be skipped.
Step by step
- Sort
numswithsort.Ints. Equal numbers become neighbours, each subset is in ascending order, and the subsets come out in the required order. - Create
resultas an empty[][]int. Each subset is itself a slice. - Start the search with
backtrack(nums, 0, []int{}, &result): any index may be used, the subset is empty, and the helper gets the address ofresult. - Return
resultonce the call is done. - Write the helper
backtrackwith the parametersnums,start,currentandresult *[][]int. It returns nothing. - Save a copy of
currentat the top of the helper, with noifaround it.append([]int{}, current...)makes the copy. - Loop with
i := start; i < len(nums); i++. Starting atstartkeeps the numbers of a 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. - Take the number: append
nums[i]tocurrent. - Recurse with
i+1as the newstart, so each number is used at most once. - Undo the choice:
current = current[:len(current)-1], so the loop can try the next number in its place.
The solution in Go
func subsetsWithDup(nums []int) [][]int {
sort.Ints(nums)
result := [][]int{}
backtrack(nums, 0, []int{}, &result)
return result
}
func backtrack(nums []int, start int, current []int, result *[][]int) {
*result = append(*result, append([]int{}, current...))
for i := start; i < len(nums); i++ {
if i > start && nums[i] == nums[i-1] {
continue
}
current = append(current, nums[i])
backtrack(nums, i+1, current, result)
current = current[:len(current)-1]
}
}
The Go details worth a closer look:
- Why copy the subset.
currentshares its backing array with the slices of other calls, and it keeps changing as the search goes on. Appendingcurrentitself would store a slice that later calls overwrite. On[1, 2, 2], a version that savescurrentwithout copying returns[[] [2] [1 2] [1 2 2] [2] [2 2]], where the second subset should be[1].append([]int{}, current...)builds a new slice with its own memory. - Why a pointer to the result.
appendcan return a slice with a new backing array, and the helper must put it wheresubsetsWithDupsees it. Withresult *[][]int, the assignment*result = append(*result, ...)does that. - The undo.
current[:len(current)-1]is the slice without its last element. It doesn't clear memory, but the nextappendoverwrites that slot, and the copy saved earlier is unaffected. sort.Intssorts in place. It reorders the slice the caller passed: calling the function with[3, 1, 3]leaves the caller's slice as[1 3 3].- The three-dot spread. In
append([]int{}, current...), the...passes the elements ofcurrentas separate arguments.
Complexity
Time: O(n · 2^n) for n numbers, sorting included. There are at most 2^n subsets, and copying one takes up to n steps. Sorting costs O(n log n), which is smaller.
Space: O(n) beyond the answer. The subset being built holds at most n numbers, and the recursion is at most n calls deep.
Edge cases and common mistakes
- Dropping the skip line. Without it,
[1, 2, 2]returns eight subsets:[[] [1] [1 2] [1 2 2] [1 2] [2] [2 2] [2]], with[1 2]and[2]twice. - Skipping with
i > 0. Comparing againsti > 0instead ofi > startalso skips the second 2 inside[2, 2], so[1, 2, 2]returns only[[] [1] [1 2] [2]]and loses[1 2 2]and[2 2]. - Forgetting to sort. The skip line compares neighbours, so equal values must be adjacent. The required order also depends on sorting.
- Negative numbers.
[-1, 1, -1]sorts to[-1, -1, 1]and gives[[] [-1] [-1 -1] [-1 -1 1] [-1 1] [1]]. - All numbers equal. Ten 7s give 11 subsets, one for each count from 0 to 10, while ten different numbers give 1024.
- The empty subset. It is always the first element of the answer, as the empty copy saved by the first call.
Testing
[1, 2, 2]gives[[] [1] [1 2] [1 2 2] [2] [2 2]].[0]gives[[] [0]].[4, 4, 4, 1, 4]gives[[] [1] [1 4] [1 4 4] [1 4 4 4] [1 4 4 4 4] [4] [4 4] [4 4 4] [4 4 4 4]], ten subsets.[-1, 1, -1]gives[[] [-1] [-1 -1] [-1 -1 1] [-1 1] [1]].
