Problem overview
The method maxSumOfThreeSubarrays receives an array nums of at least three integers and returns the largest total of three non-empty, non-overlapping subarrays. A subarray is a run of neighbouring elements.
For [2, 3, -8, 7, -2, 9, -9, 7, -2, 4] the answer is 28: the subarrays [2, 3], [7, -2, 9] and [7, -2, 4] sum to 5, 14 and 9.
Approach: remove the loop over the middle subarray's start
A first solution tries every middle subarray nums[k...i] with two nested loops. It combines the best subarray before the middle (the left part) with the best one after it (the right part), using Kadane's algorithm each time, and takes O(n³). A second version stores the best subarray on every prefix and every suffix, and drops to O(n²). This lesson goes one step further and needs only one loop over the middle.
The key is to write the middle subarray's sum as a difference of prefix sums. Let P[i] be the sum of nums[0...i]. The middle subarray from k to i has the sum P[i] - P[k - 1]. With leftMaxSum and rightMaxSum as the best subarray sums on every prefix and suffix, the total for one choice is:
leftMaxSum[k - 1] + (P[i] - P[k - 1]) + rightMaxSum[i + 1]
Group the terms that depend on k:
P[i] + rightMaxSum[i + 1] + (leftMaxSum[k - 1] - P[k - 1])
For a fixed i, only the bracket depends on the start k, and k can be any value from 1 to i. So the best start is the one that maximizes leftMaxSum[k - 1] - P[k - 1]. That maximum can be kept in a variable, maxDiff, and updated as i advances: moving from i - 1 to i adds one new candidate start, k = i. No inner loop is needed.
Step by step
- Store
nums.countinnwithlet. - Create
leftMaxSum, avararray ofnzeros, where entryiis the best subarray sum withinnums[0...i]. - Set
leftMaxSum[0]tonums[0]. - Start
maxSumatnums[0]. It is the best sum of a subarray ending at the current index. - Loop
ifrom 1 ton - 1. - Extend or restart.
maxSumbecomesnums[i] + max(maxSum, 0): the previous run is worth extending only when its sum is positive. - Fill
leftMaxSum[i]with the larger ofleftMaxSum[i - 1]andmaxSum. The best subarray in the prefix either ends atior lies inside the shorter prefix. - Create
rightMaxSumin the same way. - Set
rightMaxSum[n - 1]tonums[n - 1]. - Reuse
maxSum, now for the best sum of a subarray starting at the current index, beginning withnums[n - 1]. - Loop
idownward fromn - 2to 0 withstride(from: n - 2, through: 0, by: -1), which includes the end value. - Update
maxSumtonums[i] + max(maxSum, 0). - Fill
rightMaxSum[i]with the larger ofrightMaxSum[i + 1]andmaxSum. - Reset
maxSumtoInt.minfor the final answer, which can be negative. - Declare
partialSum, starting atnums[0]. At the start of the iteration for indexiit holds the sum ofnums[0...i - 1]. - Declare
maxDiff, starting atInt.min. It will hold the best value ofleftMaxSum[k - 1]minus the sum ofnums[0...k - 1]over the starts seen so far. - Loop
iover the middle subarray's last index, from 1 ton - 2. It needs a first subarray before it and a third after it. - Add the new start.
maxDiffbecomes the larger of itself andleftMaxSum[i - 1] - partialSum. At this momentpartialSumis the sum ofnums[0...i - 1], as the formula needs fork = i. - Extend the prefix. Add
nums[i]topartialSum, so that it holds the sum ofnums[0...i]. - Update the answer with the larger of itself and
partialSum + rightMaxSum[i + 1] + maxDiff. This is the best first subarray plus a middle one ending ati, plus the best third one. - Return
maxSumafter the loop.
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] + max(maxSum, 0)
leftMaxSum[i] = 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] + max(maxSum, 0)
rightMaxSum[i] = max(rightMaxSum[i + 1], maxSum)
}
maxSum = Int.min
var partialSum = nums[0]
var maxDiff = Int.min
for i in 1..<(n - 1) {
maxDiff = max(leftMaxSum[i - 1] - partialSum, maxDiff)
partialSum += nums[i]
maxSum = max(partialSum + rightMaxSum[i + 1] + maxDiff, maxSum)
}
return maxSum
}
}
Swift details worth knowing:
- The order of the two updates matters.
maxDiffis updated beforepartialSumgrows, because the candidate for startk = ineeds the sum up toi - 1. Swapping the lines gives a wrong answer without any error. - Looping backwards. A range such as
(n - 2)...0crashes at run time, since a range can't have its lower bound above its upper bound.stride(from:through:by:)with a step of-1counts down, andthroughincludes the last value. Int.minas a starting value. BothmaxSumandmaxDiffbegin atInt.minand are overwritten in the first iteration before anyone uses them. The loop runs at least once becausenis at least 3. Arithmetic onInt.minitself would overflow, and an overflow stops a Swift program.- The ranges
1..<(n - 1)and1..<n. Both are non-empty forn >= 3. For smaller inputs1..<(n - 1)would have its lower bound above its upper bound, which crashes.
Complexity
Time: O(n). There are three loops, each over at most n indices, and every iteration does a constant number of operations.
Space: O(n). The two arrays leftMaxSum and rightMaxSum hold n numbers each. Everything else is a handful of integers.
Edge cases and common mistakes
- All negative numbers. For
[-1, -2, -3]the answer is-6. Kadane's updates start fromnums[0]andnums[n - 1], not from 0, so a negative best sum survives. - Exactly three numbers. Only
i = 1runs. The first and third subarrays are single elements and the answer is the total of the array. - Using
leftMaxSum[i]instead ofleftMaxSum[i - 1]. The first subarray must end before the middle one starts. - Updating
partialSumtoo early. It then includesnums[i]when the start candidate is computed, and the middle subarray is counted one element short. - Starting
maxDiffat 0. The difference can be negative, and a wrong starting value would pretend a better start exists. - Letting
ireachn - 1. ThenrightMaxSum[i + 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.
