Problem overview
The method maxSumOfThreeSubarrays receives an array nums with at least three integers. It must pick three non-empty subarrays that don't overlap and return the largest total of their sums.
For [2, 3, -8, 7, -2, 9, -9, 7, -2, 4] the answer is 28, from [2, 3], [7, -2, 9] and [7, -2, 4], which sum to 5, 14 and 9. The two negative numbers between them are skipped.
Approach: precompute the best subarray on every side
The three subarrays appear in left-to-right order. If the middle one is nums[i...j], then the first is the best subarray inside nums[0..<i] and the third is the best subarray inside nums[(j + 1)..<n].
A simpler solution calls the maximum-subarray routine for those two parts inside the double loop over i and j, which costs O(n³). But the two parts are always a prefix and a suffix of the array, and there are only n of each. So this lesson computes the answers once:
leftMaxSum[i]is the best subarray sum within the prefixnums[0...i];rightMaxSum[i]is the best subarray sum within the suffixnums[i...n - 1].
After that, each lookup is a single array read, and the double loop over the middle subarray costs O(n²) in total.
The recurrence
Let maxSum be the best sum of a subarray that ends at index i. That subarray either extends the one ending at i - 1, if that sum is positive, or starts fresh at nums[i]:
maxSum = nums[i] + max(maxSum, 0)
The best subarray inside nums[0...i] either ends at i or lies inside nums[0...i - 1]:
leftMaxSum[i] = max(leftMaxSum[i - 1], maxSum)
The suffix array works the same way, scanning from the right and tracking the best subarray that starts at i.
Step by step
- Store
nums.countinnwithlet. - Create
leftMaxSumwith[Int](repeating: 0, count: n). It is avar, because an array declared withletcan't have its elements changed. - Set
leftMaxSum[0]tonums[0]. The first prefix contains one element, so that element is its only subarray. - Start
maxSumatnums[0], the only subarray that ends at index 0. - Loop
ifrom 1 ton - 1. - Update
maxSumtonums[i] + Swift.max(maxSum, 0). - Fill
leftMaxSum[i]with the larger ofleftMaxSum[i - 1]andmaxSum. - Create
rightMaxSumthe same way, as avararray of zeros. - Set
rightMaxSum[n - 1]tonums[n - 1]. - Reuse
maxSum, now as the best sum of a subarray that starts at the current index, beginning withnums[n - 1]. - Loop
ifromn - 2down to 0.stride(from: n - 2, through: 0, by: -1)counts down and includes the end value. - Update
maxSumtonums[i] + Swift.max(maxSum, 0). - Fill
rightMaxSum[i]with the larger ofrightMaxSum[i + 1]andmaxSum. - Reset
maxSumfor the answer. The answer can be negative, so it must start below any possible total. The lesson usesInt(Int32.min), the smallest 32-bit integer converted toInt. - Loop over the middle subarray's first index
ifrom 1 ton - 1, so the first subarray is not empty. - Read
sum1asleftMaxSum[i - 1]: the best first subarray, ending before indexi. - Start
sum2at 0. It is avar, since it grows. - Loop
jfromiton - 2, the middle subarray's last index. The limit leaves room for a non-empty third subarray. - Add
nums[j]tosum2. - Read
sum3asrightMaxSum[j + 1]. - Keep the larger of
maxSumandsum1 + sum2 + sum3. - Return
maxSumwhen the loops finish.
The solution in Swift
class Solution {
func maxSumOfThreeSubarrays(_ nums: [Int]) -> Int {
let n = nums.count
var leftMaxSum = [Int](repeating: 0, count: n)
leftMaxSum[0] = nums[0]
var maxSum = nums[0]
for i in 1..<n {
maxSum = nums[i] + Swift.max(maxSum, 0)
leftMaxSum[i] = Swift.max(leftMaxSum[i - 1], maxSum)
}
var rightMaxSum = [Int](repeating: 0, count: n)
rightMaxSum[n - 1] = nums[n - 1]
maxSum = nums[n - 1]
for i in stride(from: n - 2, through: 0, by: -1) {
maxSum = nums[i] + Swift.max(maxSum, 0)
rightMaxSum[i] = Swift.max(rightMaxSum[i + 1], maxSum)
}
maxSum = Int(Int32.min)
for i in 1..<n {
let sum1 = leftMaxSum[i - 1]
var sum2 = 0
for j in i..<(n - 1) {
sum2 += nums[j]
let sum3 = rightMaxSum[j + 1]
maxSum = Swift.max(maxSum, sum1 + sum2 + sum3)
}
}
return maxSum
}
}
Swift details worth knowing:
Swift.max. The module prefix selects the standard library's globalmax. It matters inside a class that declares its own method namedmax, because the method would hide the global. This class has no such method, so plainmaxwould work here too.[Int](repeating: 0, count: n)builds an array ofnzeros, and an array declared withvarcan then be changed element by element.stridefor counting down. A range like(n - 2)...0doesn't work, because a range can't go backwards (writing one crashes at run time).stride(from:through:by:)with a negative step does count down and includes thethroughvalue.- Reusing
maxSum. The same variable serves three jobs: the best subarray ending ati, the best one starting ati, and finally the answer. Every use starts with an assignment, so nothing leaks from one to the next.
Complexity
Time: O(n²). Building the two arrays takes O(n) each. The double loop over i and j visits about n²/2 pairs, and each does a constant amount of work.
Space: O(n). The two arrays hold n numbers each.
The double loop is the remaining cost. The next lesson removes it by rewriting the middle sum as a difference of two prefix sums.
Edge cases and common mistakes
- All negative numbers. For
[-1, -2, -3]the answer is-6. Zero is a wrong starting value for the best sums, since the result would be too high. - The sentinel range. Starting the answer at
Int(Int32.min)works only while the true total is above-2147483648. With three elements of-3000000000the true answer is-9000000000, but this code returns-2147483648, because the starting value is never replaced.Int.minis a safer starting value. - Exactly three numbers. Only one choice exists, one element each, so the answer is the total. The loops handle it with
iequal to 1 or 2 and a singlejvalue. - Off-by-one on the sides. The first part for a middle subarray starting at
iisleftMaxSum[i - 1], notleftMaxSum[i], which already includesnums[i]. The third part isrightMaxSum[j + 1], notrightMaxSum[j]. - Forgetting that a prefix result is a maximum.
leftMaxSum[i]is not "the best subarray ending ati". It is the best one anywhere in the prefix, which is why it takes the larger of the previous entry and the newmaxSum. - Letting
jrun ton - 1. ThenrightMaxSum[j + 1]reads past the end of the array and the program crashes.
Testing
[2, 3, -8, 7, -2, 9, -9, 7, -2, 4]gives28.[-1, -2, -3]gives-6.[-5, -1, -7, -2]gives-8.[1, 2, 3, 4]gives10.[5, -10, 5, -10, 5]gives15.
