Problem overview
The method subsetsWithDup receives an Array[Int] called nums that may contain repeated numbers. It returns every distinct subset as a List[List[Int]], from the empty subset to the one holding all the numbers. 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 the same subset [2], which must appear once.
The order is fixed too. Each subset is written in ascending order, and the subsets are in lexicographic order: the first position where two subsets differ decides, and a subset that is the beginning of another comes first. The empty subset is always first.
For [1,2,2] the answer is List(List(), List(1), List(1, 2), List(1, 2, 2), List(2), List(2, 2)): six subsets, where plain subsets of three numbers would give eight. For [4,4,4,1,4] there are only ten: a subset is fixed by whether it has the 1 and by how many 4s it has, from 0 to 4, which is 2 times 5.
Approach: sort, then pick the next number in a loop
Sorting nums puts equal numbers next to each other, which makes repeats easy to recognise, and it makes every subset come out in ascending order.
The search uses the loop form of backtracking. A call receives start, the first index it may still use, and current, the subset built so far. Every subset the search builds is an answer, so each call first saves a copy of current. Then a loop chooses the next number of the subset: for each index i from start, it adds nums(i), calls itself with i + 1, and removes the number again.
Duplicates are handled by one condition. Inside one call, each turn of the loop picks a different 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 that turn is skipped. A repeated value is still taken when it is the first one this call tries, at i == start: that is how [2,2] gets its second 2.
Step by step
- Sort
numsin place withscala.util.Sorting.quickSort(nums). - Create
result, an emptyArrayBuffer[List[Int]](). Each subset is itself a list. - Start the search with
backtrack(nums, 0, ArrayBuffer[Int](), result): nothing chosen, every index available. - Return
result.toListas the last expression. - Write the helper
backtrackwithnums,start,currentandresult, returningUnit. - Save a snapshot of the current subset at the top:
result += current.toList. There is no condition around it, because every subset along the way is an answer. - Loop with
ioverstart until nums.length. Starting atstartkeeps each subset in index order, so in ascending order. Whenstartis past the last index the loop is empty, and that is where the search ends. - Skip repeats with
if (i == start || nums(i) != nums(i - 1)). Scala has nocontinue, so the rest of the loop body goes inside thisif. - Choose
nums(i):current += nums(i). - Explore with
backtrack(nums, i + 1, current, result). Passingi + 1means each number is used at most once. - Undo the choice with
current.remove(current.length - 1), so the loop can try the next number.
The solution in Scala
class Solution {
def subsetsWithDup(nums: Array[Int]): List[List[Int]] = {
scala.util.Sorting.quickSort(nums)
val result = ArrayBuffer[List[Int]]()
backtrack(nums, 0, ArrayBuffer[Int](), result)
result.toList
}
def backtrack(nums: Array[Int], start: Int, current: ArrayBuffer[Int], result: ArrayBuffer[List[Int]]): Unit = {
result += current.toList
for (i <- start until nums.length) {
if (i == start || nums(i) != nums(i - 1)) {
current += nums(i)
backtrack(nums, i + 1, current, result)
current.remove(current.length - 1)
}
}
}
}
Scala details worth a look:
current.toListis a copy.currentis oneArrayBufferthat the search keeps changing. Storing the buffer itself would put the same object intoresultmany times. Withresult += currenton[1,2,2], the six saved items all end up as empty buffers when the search finishes:ArrayBuffer(ArrayBuffer(), ArrayBuffer(), ArrayBuffer(), ArrayBuffer(), ArrayBuffer(), ArrayBuffer()).toListbuilds an immutable list with the numbers at that moment.quickSortsorts the argument.scala.util.Sorting.quickSort(nums)reorders the caller's array: after the call withArray(3,1,2), that array holds1, 2, 3. The lesson does it this way. To keep the original order, sort a copy.remove(index)on anArrayBuffertakes an index, not a value. Here it always removes the last element, atcurrent.length - 1.0 until nandstart until nums.lengthexclude the end, soiis always a valid index.- Short-circuit
||. Wheni == startis true,nums(i - 1)is not evaluated, so the first turn of a call atstart == 0does not read index-1.
Complexity
Time: O(n * 2^n). A set of n numbers has at most 2^n subsets, and copying one with toList takes up to n steps. Sorting costs O(n log n), which is smaller.
Space: O(n) beyond the answer: the subset being built and one call for each number in it.
Edge cases and common mistakes
- No duplicate check. Without the
if,[1,2,2]produces eight subsets, withList(1, 2)andList(2)twice:List(List(), List(1), List(1, 2), List(1, 2, 2), List(1, 2), List(2), List(2, 2), List(2)). - Comparing with
i == 0instead ofi == start. Then the second 2 is skipped even when it is the first choice of its call, and[1,2,2]gives onlyList(List(), List(1), List(1, 2), List(2)): the subsets[1,2,2]and[2,2]are lost. - Not sorting. Equal numbers must be neighbours for the comparison with
nums(i - 1)to find them. Sorting also gives the order the answer needs. - Passing
iinstead ofi + 1. A number could then be chosen again right after itself, so the subsets would no longer use each number at most once. - Forgetting the undo. Numbers would pile up in
currentand later subsets would contain numbers from earlier ones. - A single number.
[0]givesList(List(), List(0)).
Testing
[1,2,2]givesList(List(), List(1), List(1, 2), List(1, 2, 2), List(2), List(2, 2)).[0]givesList(List(), List(0)).[4,4,4,1,4]gives ten subsets, fromList(),List(1),List(1, 4)up toList(4, 4, 4, 4).[2,1,2,1]givesList(List(), List(1), List(1, 1), List(1, 1, 2), List(1, 1, 2, 2), List(1, 2), List(1, 2, 2), List(2), List(2, 2)).
