Problem overview
The method canPartition receives nums, an IntArray of positive integers, with up to 200 values from 1 to 100. It returns true when the numbers can be split into two groups with equal sums, and false otherwise. Every number goes into exactly one group, and repeated values are separate numbers, each copy going into one group.
For [1, 5, 11, 5] the answer is true: [1, 5, 5] and [11] both add up to 11. For [1, 2, 3, 5] the total is 11, which is odd, so two equal halves are impossible. For [1, 2, 5] the total is 8, so each group would need to add up to 4, and no choice of those three numbers makes 4, so the answer is false.
The statement asks for O(n · sum) time and O(sum) extra space, where sum is the total of the numbers.
Approach: can some of the numbers make half?
If the total is odd, the answer is false at once. If it is even, call half of it half. When some of the numbers add up to exactly half, the numbers left over add up to the other half. So the question becomes: can some subset of nums sum to half?
Trying every subset is hopeless: 200 numbers can be grouped in 2^200 ways. Dynamic programming avoids that by remembering which sums are reachable. Keep a table can where can[s] says whether some of the numbers seen so far add up to s, for every s from 0 to half. Before any number is seen, only can[0] is true, because no numbers at all make 0.
Now add the numbers one at a time. For a number num, every sum that was reachable before gives a new reachable sum with num added. So can[s] becomes true when can[s - num] was true. Taking the sums from half down to num is the detail that matters: going down, can[s - num] still has its value from before this number, so the number is used at most once. Going up, can[s - num] could already include the same num, which would use it again.
The same table with the loop going up is the pattern for problems where an item can be used any number of times. Here each number is used once, so the loop runs down.
Step by step
- Add the numbers up with
nums.sum()and store the result intotal. - Reject an odd total. If
total % 2 == 1, returnfalse: two equal halves always add up to an even number. - Store
halfastotal / 2. The goal is now to make exactlyhalffrom some of the numbers. - Create
can, aBooleanArrayofhalf + 1entries. A new boolean array starts allfalse. - Set
can[0] = true. The empty selection makes 0, and every other sum grows from it. - Loop over
numswith the variablenum. After each pass,candescribes all sums that the numbers handled so far can make. - Loop
soverhalf downTo num. A sum smaller thannumcan't contain it, so the loop stops atnum. - Update the table:
can[s] = can[s] || can[s - num]. Eitherswas reachable already, ors - numwas and addingnumreaches it. - Return
can[half]after the outer loop.
The solution in Kotlin
class Solution {
fun canPartition(nums: IntArray): Boolean {
val total = nums.sum()
if (total % 2 == 1) {
return false
}
val half = total / 2
val can = BooleanArray(half + 1)
can[0] = true
for (num in nums) {
for (s in half downTo num) {
can[s] = can[s] || can[s - num]
}
}
return can[half]
}
}
Kotlin details worth knowing:
nums.sum()on anIntArrayreturns anInt. With at most 200 values of at most 100 each, the total is at most 20,000, far from the 32-bit limit, so there is no overflow to worry about.downTois a range that counts down.half downTo numincludes both ends. Whennumis bigger thanhalf, the range is empty and the loop body never runs.val canand mutable entries.valmeans the name always refers to the same array, but its elements can still change.||short-circuits: whencan[s]is already true, the right side is not evaluated. It makes no difference to the result here.- The remainder operator.
total % 2 == 1works for the positive totals of this lesson. The check is safe because every number is positive.
Tracing the example
For [1, 5, 11, 5], the total is 22 and half is 11. Printing the sums marked true in can after each number gives:
start: [0]
after 1: [0, 1]
after 5: [0, 1, 5, 6]
after 11: [0, 1, 5, 6, 11]
after 5: [0, 1, 5, 6, 10, 11]
After the 11, the sum 11 is reachable (11 alone), so can[11] is true and the method returns true. The second 5 adds 10 as well, from 5 + 5.
Complexity
Time: O(n · sum). Each of the n numbers goes over at most half + 1 sums with constant work for each. The sum is at most 20,000 here, so the largest input needs about two million steps.
Space: O(sum). The table holds half + 1 booleans.
Edge cases and common mistakes
- Odd total.
[1, 2, 3, 5]and[1, 1, 3]returnfalsewithout touching the table. - A single number.
[1]returnsfalse: its total is odd. Even a single number can never be split, since the other group would be empty with sum 0, and every number is at least 1. - A number bigger than half. In
[100, 1, 1], the total is 102 andhalfis 51. The 100 doesn't fit in the table, its loop does nothing, and the answer isfalse. - Equal numbers.
[2, 2]and[100, 100]returntrue: the copies are separate numbers, and each goes into one group. Twelve 1s also returntrue. - Looping upward. If the inner loop runs
num..half, a number can be counted several times. With that change,[1, 2, 5]returnstrue, because 4 can be made as 1 + 1 + 1 + 1 or 2 + 2, and[100, 1, 1]returnstruetoo, with fifty-one 1s. The downward loop returnsfalsefor both. - Largest input. Two hundred 100s give
true. Changing one of them to 99 makes the total 19,999, which is odd, so the answer isfalse.
Testing
[1, 5, 11, 5]givestrue.[1, 2, 3, 5]givesfalse.[1, 2, 5]givesfalse.[2, 2]givestrue.[3, 3, 3, 4, 5]givestrue.[100, 1, 1]givesfalse.
