Problem overview
The function maxSumOfThreeSubarrays receives a slice of integers, nums, with at least three elements. It chooses three non-empty, non-overlapping subarrays, one after another, and returns the largest possible total of their sums. Values may be negative.
For nums = [2, 3, -8, 7, -2, 9, -9, 7, -2, 4] the best choice is [2, 3], [7, -2, 9] and [7, -2, 4], with sums 5, 14 and 9. The answer is 28.
Approach: remove the loop over the middle's start
The quadratic solution fixes the middle subarray and looks up the best first subarray on its left and the best third on its right. It still loops over both ends of the middle. This lesson keeps the same two tables, leftMaxSum and rightMaxSum, but loops only over where the middle ends.
The trick uses prefix sums. Let P(i) be the sum of nums[0..i]. If the middle runs from k to i, its sum is P(i) - P(k-1). The best first subarray before it is worth leftMaxSum[k-1]. So the first and middle parts together are worth P(i) + (leftMaxSum[k-1] - P(k-1)). For a fixed i only the bracket depends on k, so keep its best value so far, maxDiff, while i moves right. Adding rightMaxSum[i+1] for the third subarray finishes each candidate in constant time.
Step by step
- Store
n, and buildleftMaxSumwithmake([]int, n). SeedleftMaxSum[0]withnums[0]. - Track the best run ending at each index in
maxSum, starting atnums[0]. For eachifrom 1, setmaxSum = nums[i] + max(maxSum, 0)andleftMaxSum[i] = max(leftMaxSum[i-1], maxSum). - Build
rightMaxSumthe same way from the right: seed the last entry withnums[n-1], reusemaxSumfromnums[n-1], and loop fromn - 2down to 0. - Reset
maxSumto-(1 << 31)as the best answer so far, lower than any total. - Declare
partialSumasnums[0]. At the start of each iteration it holds the sum ofnums[0..i-1]. - Declare
maxDiffas-(1 << 31). It holds the largestleftMaxSum[k-1]minus the sum ofnums[0..k-1]over all middle startskseen so far. - Loop over the middle's last index
ifrom 1 ton - 2, leaving room for a first subarray before it and a third one after it. - Add the option of starting the middle at
i:maxDiff = max(leftMaxSum[i-1]-partialSum, maxDiff), whilepartialSumis still the sum up toi-1. - Add
nums[i]topartialSum, which now coversnums[0..i]. - Score the candidate as
partialSum + rightMaxSum[i+1] + maxDiffand keep the larger of it andmaxSum. - Return
maxSum.
The solution in Go
type Solution struct{}
func maxSumOfThreeSubarrays(nums []int) int {
n := len(nums)
leftMaxSum := make([]int, n)
leftMaxSum[0] = nums[0]
maxSum := nums[0]
for i := 1; i < n; i++ {
maxSum = nums[i] + max(maxSum, 0)
leftMaxSum[i] = max(leftMaxSum[i-1], maxSum)
}
rightMaxSum := make([]int, n)
rightMaxSum[n-1] = nums[n-1]
maxSum = nums[n-1]
for i := n - 2; i >= 0; i-- {
maxSum = nums[i] + max(maxSum, 0)
rightMaxSum[i] = max(rightMaxSum[i+1], maxSum)
}
maxSum = -(1 << 31)
partialSum := nums[0]
maxDiff := -(1 << 31)
for i := 1; i < n-1; i++ {
maxDiff = max(leftMaxSum[i-1]-partialSum, maxDiff)
partialSum += nums[i]
maxSum = max(partialSum+rightMaxSum[i+1]+maxDiff, maxSum)
}
return maxSum
}
func (this Solution) maxSumOfThreeSubarrays(nums []int) int {
return maxSumOfThreeSubarrays(nums)
}
Go details:
- A function and a method with the same name. The package-level function
maxSumOfThreeSubarraysand the method ofSolutionwith the same name live in different namespaces, so they don't clash. Inside the method, the bare namemaxSumOfThreeSubarrays(nums)refers to the package-level function, so the method simply forwards to it. It is not recursion. - Built-in
max. Go 1.21 and later havemaxandminbuilt in, so no helper is required. - Reassigning with
=.maxSumis reused for three jobs. The first declaration uses:=; later lines use=. - Unused names are errors. Go refuses to compile a program with an unused local variable, so every variable here, including
partialSumandmaxDiff, must be read somewhere. - The sentinel.
-(1 << 31)is -2147483648. Totals below that range would not be handled.
Worked example
For the example, leftMaxSum is [2 5 5 7 7 14 14 14 14 14] and rightMaxSum is [14 14 14 14 9 9 9 9 4 4]. At i = 3, maxDiff becomes leftMaxSum[2] - (2 + 3 - 8) = 5 - (-3) = 8, which corresponds to starting the middle at index 3. At i = 5 the partial sum is 11, maxDiff is still 8, and rightMaxSum[6] is 9. The candidate is 11 + 9 + 8 = 28, the answer. The first part, [2, 3], plus the middle [7, -2, 9] is worth 11 + 8 = 19, and the third part adds 9.
Reading the three parts
It helps to name what each term in the final score stands for. partialSum is the total of everything from the start of the slice up to the end of the middle subarray. maxDiff subtracts the prefix before the middle's start and adds the best first subarray there, so the two together equal the best first subarray plus the middle. This only works because the first subarray is always chosen from the elements strictly before the middle, which is exactly what leftMaxSum[k-1] guarantees. The last term, rightMaxSum[i+1], adds the best third subarray strictly after the middle. No pair of parts can overlap, and every valid triple is considered by some pair (k, i).
Complexity
Time: O(n). Each of the three passes does constant work per element.
Space: O(n). The two tables hold n values each. The rest is a few integers.
Edge cases and common mistakes
- Exactly three numbers. The loop runs once, at
i = 1, and the answer is the sum of all three. - All negative numbers. For
[-1, -2, -3]the answer is -6, and for[-5, -1, -7, -2]it is -8. Starting the best answer at 0 would hide this. - Updating
maxDiffafter addingnums[i]. The difference needs the prefix sum up toi-1, so the order of the two lines matters. - Overlap. The first subarray comes from
leftMaxSum[k-1]and the third fromrightMaxSum[i+1], so the middle's range separates them. - Loop bounds. Starting at 0 would read
leftMaxSum[-1], which panics; ending atn-1would readrightMaxSum[n], which also panics.
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, 5]gives15.