Problem overview
The method subsetsWithDup receives nums, an IntArray of up to 10 integers that may repeat. It returns every distinct subset: every way to pick some of the numbers, from none (the empty subset) to all. Two subsets are the same when they hold the same numbers the same number of times, so in [1,2,2] picking the first 2 or the second 2 gives one subset, [2], which must appear once.
The answer has a fixed order too. Each subset is written in ascending order, and the subsets are in lexicographic order: the first position where two subsets differ decides which comes first, and a subset that is the beginning of another comes before it. The empty subset is therefore first. For [1,2,2] the answer is [[], [1], [1, 2], [1, 2, 2], [2], [2, 2]].
Approach: grow subsets by choosing the next number
Every subset is a sequence of numbers taken in index order. Backtracking builds them one number at a time. A call receives current, the subset built so far, and start, the first index it may still use. It saves a copy of current right away, because every subset along the way is an answer, not only the full ones. Then a loop tries every index i from start on as the next number: add nums[i], recurse with i + 1, remove it again.
Starting the loop at start instead of 0 keeps each subset in index order, so no subset is built twice by picking the same indexes in a different order.
Duplicates need one more idea. Sort nums so that equal numbers sit side by side. Inside one call, each turn of the loop picks a different candidate for the next number. If nums[i] equals nums[i - 1] and i > start, this call has already tried that value as the next number, and trying it again would rebuild the same subsets. So the loop skips it. At i == start the number is the first this call tries, and it is taken even when it equals the number before it. That is how [2, 2] gets its second 2: the first 2 is chosen by one call, and the next call starts right after it, where the second 2 is its first candidate.
Sorting also puts the numbers of each subset in ascending order and makes the subsets come out in the required order.
Step by step
- Sort
numsin place withsort(). Duplicates become neighbours, which makes them easy to detect. - Create
resultwithmutableListOf<List<Int>>(): a list that holds lists. - Start the search with
backtrack(nums, 0, mutableListOf(), result): index 0 is the first usable index, and the subset is empty. - Return
resultafter the call. - Declare
backtrackwithnums,start,currentandresult. - Save a snapshot of
currentwithcurrent.toList(), at the top and without any condition. - Loop
ioverstart until nums.size. Whenstartis past the last index, the loop does not run, and that is where the recursion ends. - Skip repeats: if
i > startandnums[i] == nums[i - 1], usecontinue. - Choose
nums[i]by adding it to the end ofcurrent. - Explore by calling
backtrackwithi + 1, so each number is used at most once. - Undo the choice with
removeAt(current.size - 1), so the loop can try the next number on the samecurrent.
The solution in Kotlin
class Solution {
fun subsetsWithDup(nums: IntArray): List<List<Int>> {
nums.sort()
val result = mutableListOf<List<Int>>()
backtrack(nums, 0, mutableListOf(), result)
return result
}
fun backtrack(nums: IntArray, start: Int, current: MutableList<Int>, result: MutableList<List<Int>>) {
result.add(current.toList())
for (i in start until nums.size) {
if (i > start && nums[i] == nums[i - 1]) {
continue
}
current.add(nums[i])
backtrack(nums, i + 1, current, result)
current.removeAt(current.size - 1)
}
}
}
Kotlin details worth knowing:
IntArray.sort()sorts in place and returns nothing, so the caller's array ends up sorted. That is fine here. The functionsorted()would return a newListinstead and leave the array as it was.- Why
toList().currentis one list that keeps changing during the whole search. Adding it directly would store the same list many times, and by the end it would be empty again. Withresult.add(current)in place of the copy, the call on[1,2,2]returns[[], [], [], [], [], []]: six references to one empty list.toList()stores a snapshot. mutableListOf()without a type is accepted in the call because Kotlin infersMutableList<Int>from the parameter type.removeAtversusremove. On aMutableList<Int>,removeAt(index)removes by position. Use it here, since the last element is what must go.continueskips the rest of the current turn of theforloop and moves to the nexti.
Tracing the example
Printing each call and each skip while the search runs on [1,2,2] gives:
backtrack(start=0, current=[])
backtrack(start=1, current=[1])
backtrack(start=2, current=[1, 2])
backtrack(start=3, current=[1, 2, 2])
i=2: skip (2 again)
backtrack(start=2, current=[2])
backtrack(start=3, current=[2, 2])
i=2: skip (2 again)
The first skip is in the call with current=[1]: at i=1 it takes the first 2, and at i=2 the second 2 would only repeat what the first one built. The second skip is the same situation in the first call. Each call saves its current when it starts, which produces the six subsets in order.
Complexity
Time: O(n · 2^n). A set of n numbers has 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) besides the answer: the subset under construction and one call for each number in it.
Edge cases and common mistakes
- One number.
[0]gives[[], [0]]. - Unsorted input.
[4,4,4,1,4]becomes[1,4,4,4,4]after sorting, and the answer has 10 subsets: with or without the 1, times 0 to 4 fours.[2,1,2]gives the same result as[1,2,2]. - All numbers equal. Ten 1s give 11 subsets, one for each count from 0 to 10.
- All numbers different.
[3,1,2]gives[[], [1], [1, 2], [1, 2, 3], [1, 3], [2], [2, 3], [3]], and the numbers 1 to 10 give 1024 subsets, which is2^10. - Negative numbers.
[-1,-1]gives[[], [-1], [-1, -1]]. - Forgetting the skip. Without the
continuetest,[1,2,2]returns[[], [1], [1, 2], [1, 2, 2], [1, 2], [2], [2, 2], [2]], with[1, 2]and[2]twice. - Comparing with
i > 0instead ofi > start. The skip then fires for the second 2 even when it is the first candidate of its call, and the result for[1,2,2]is[[], [1], [1, 2], [2]]: both[1, 2, 2]and[2, 2]are missing. - Forgetting to sort. With
[2,1,2]left unsorted, the equal 2s are not neighbours, the check fails to see them, and the result is[[], [2], [2, 1], [2, 1, 2], [2, 2], [1], [1, 2], [2]]: not in ascending order and with[2]twice.
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]].[2,1,2]gives[[], [1], [1, 2], [1, 2, 2], [2], [2, 2]].[-1,-1]gives[[], [-1], [-1, -1]].
