Problem overview
The method canPartition receives nums, an array of positive integers, and returns true when the numbers can be split into two groups with equal sums, and false otherwise. Every number goes into exactly one group. Repeated values are separate numbers: each copy goes 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 it can't be split into two equal halves: false. For [1, 2, 5] the total is 8, so each group would need to add up to 4, but no group of these numbers adds up to 4: the sums up to 4 that the numbers can make are only 0, 1, 2 and 3. The answer is false.
The statement sets a budget of O(n · sum) time and O(sum) extra space, where n is the number of values and sum is their total.
Approach: can some of the numbers make exactly half?
If the total is odd, the answer is false at once, since two equal halves always add up to an even number. Otherwise let half be the total divided by 2. If some of the numbers add up to exactly half, the numbers that are left make the other half. So the question becomes: can some of the numbers add up to exactly half?
Trying every possible group is too slow: 200 numbers can be grouped in 2^200 ways. Dynamic programming avoids that with a table of booleans, 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: no numbers at all make 0.
Then the numbers are added one at a time. When a number num arrives, every sum that could be made before gives a new sum with num added: can[s] becomes true when can[s - num] was already true. Here is how the table changes for [1, 5, 11, 5] (half is 11), listing the sums that are true:
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]
The sum 11 is reachable, so the answer is true. For [1, 2, 5] (half is 4) the table ends as follows, and 4 is never reached:
start [0]
after 1 [0, 1]
after 2 [0, 1, 2, 3]
after 5 [0, 1, 2, 3]
Why the sums go down
The inner loop runs from half down to num. Going down, can[s - num] still holds the value from before this number was handled, because the smaller index hasn't been updated yet. So each number is used at most once. A loop that goes up would read an entry that this same number has just set, and would use the number again. On [1, 2, 5] an upward loop makes 4 as 1 + 1 + 1 + 1 or 2 + 2 and wrongly answers true. (Problems where a value may be used any number of times are the ones that go up on purpose.)
Step by step
- Add everything up.
let total = nums.reduce(0, +)starts from 0 and combines each number into the result with+. - Reject odd totals. If
total % 2 == 1, returnfalse. - Compute
halfastotal / 2. - Create
can, avararray ofhalf + 1valuesfalse. The entrycan[s]will say whether the sumscan be made. - Set
can[0] = true: no numbers make the sum 0, and every group grows from this entry. - Loop over
numswith the variablenum. After the loop has handled a number,canknows every sum that the numbers so far can make, each one used at most once. - Loop over the sums downward with
stride(from: half, through: num, by: -1). A sum smaller thannumcan't contain it, so the loop stops atnum. - Update the table with
can[s] = can[s] || can[s - num]. This is the key line: the sumsis possible if it already was, or ifs - numwas possible andnumcompletes it. - Return
can[half]after the outer loop.
The solution in Swift
class Solution {
func canPartition(_ nums: [Int]) -> Bool {
let total = nums.reduce(0, +)
if total % 2 == 1 {
return false
}
let half = total / 2
var can = [Bool](repeating: false, count: half + 1)
can[0] = true
for num in nums {
for s in stride(from: half, through: num, by: -1) {
can[s] = can[s] || can[s - num]
}
}
return can[half]
}
}
The Swift details worth a closer look:
reduce(0, +). The operator+is itself a function, so it can be passed as the combining step. For an empty array,reducereturns the starting value 0.- Counting down. A closed range such as
num...halfcan only count up.stride(from:through:by:)withby: -1counts down, includesnum, and is simply empty whennumis more thanhalf. For examplestride(from: 5, through: 7, by: -1)yields nothing, whilestride(from: 5, through: 2, by: -1)yields 5, 4, 3, 2. - A range with the bounds the wrong way crashes. A loop
for s in num...halfwithnum = 100andhalf = 50stops the program at run time, because the lower bound of a range can't be above its upper bound. This is why a big number, such as the 100 in[100], needsstride. ||short-circuits.can[s] || can[s - num]only looks atcan[s - num]whencan[s]isfalse. Either way the result is the same, ands - numis never negative becausesstarts at least atnum.- Integer division.
total / 2truncates. That is safe here, because an oddtotalhas already returned.
Complexity
Time: O(n · sum). Each of the n numbers goes over at most half + 1 sums, with a constant amount of work each. Since half is half the total, this is O(n · sum).
Space: O(sum). The table holds half + 1 booleans, and nothing else grows with the input.
Edge cases and common mistakes
- Odd total.
[1, 2, 3, 5]returnsfalsebefore the table is even built. - A number bigger than half. For
[100]the total is 100,halfis 50, and the number 100 is more than 50. Thestrideis empty,can[50]staysfalse, and the answer isfalse. - Two equal numbers.
[1, 1]returnstrue: each copy is its own number, and goes to its own group. - An even total that can't be split.
[2, 2, 2, 2, 2]has a total of 10 and a half of 5, but sums of 2s are always even, so it returnsfalse. Checking the parity of the total is necessary but not enough. - Going up instead of down. Written with the sums counted upward and a guard for numbers above
half, the loop answerstruefor[1, 2, 5], which has no valid split. - Initial value. Without
can[0] = truenothing is ever reachable, and every answer isfalse.
Testing
[1, 5, 11, 5]givestrue.[1, 2, 3, 5]givesfalse.[1, 2, 5]givesfalse.[100]givesfalse.[1, 1]givestrue.[2, 2, 2, 2, 2]givesfalse.[3, 3, 3, 4, 5]givestrue: 4 + 5 and 3 + 3 + 3 both make 9.
