Problem overview
The function maxSumOfThreeSubarrays receives a slice of integers, nums, with at least three elements. It must choose three non-empty, non-overlapping subarrays, placed one after another, and return the largest possible total of their sums. Values can 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, so the answer is 28.
Approach: fix the middle, look up the sides
Choosing all three subarrays at once is too many combinations. The key idea is to fix the middle subarray and let the two outer ones take care of themselves. For a given middle range, the best first subarray is the best one that lies entirely to its left, and the best third is the best one entirely to its right. Both can be precomputed:
leftMaxSum[i]is the best subarray sum insidenums[0..i];rightMaxSum[i]is the best subarray sum insidenums[i..n-1].
Then each middle range [i, j] is scored as leftMaxSum[i-1] + sum(nums[i..j]) + rightMaxSum[j+1]. Trying all middle ranges costs O(n²), because there are about n²/2 of them and the running sum makes each one constant work. Another lesson on this problem removes the inner loop.
Step by step
- Store
n, the length ofnums. - Create
leftMaxSumwithmake([]int, n). - Seed it:
leftMaxSum[0] = nums[0], the only subarray of a one-element prefix. - Track the best sum ending at the current index in
maxSum, starting atnums[0]. - Loop from index 1 to
n - 1. - Update
maxSumtonums[i] + max(maxSum, 0): extend the previous run if it is positive, otherwise start fresh atnums[i]. - Update
leftMaxSum[i]tomax(leftMaxSum[i-1], maxSum): the best in the prefix either ends atior lies before it. - Create
rightMaxSumthe same way and seedrightMaxSum[n-1]withnums[n-1]. - Reuse
maxSumfor the best sum starting at the current index, beginning atnums[n-1]. - Loop downward from
n - 2to 0. SetmaxSumtonums[i] + max(maxSum, 0), thenrightMaxSum[i]tomax(rightMaxSum[i+1], maxSum). - Reset
maxSumto-(1 << 31)to hold the best answer. It must be lower than any possible total. - Choose where the middle starts:
ifrom 1 ton - 1, so the first subarray is not empty. Readsum1asleftMaxSum[i-1]. - Choose where the middle ends:
jfromiup to but not includingn - 1, so the third subarray is not empty. Keepsum2as a running sum ofnums[i..j], and readsum3asrightMaxSum[j+1]. - Keep the larger of
maxSumandsum1 + sum2 + sum3, then returnmaxSum.
The solution in Go
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)
for i := 1; i < n; i++ {
sum1 := leftMaxSum[i-1]
sum2 := 0
for j := i; j < n-1; j++ {
sum2 += nums[j]
sum3 := rightMaxSum[j+1]
maxSum = max(maxSum, sum1+sum2+sum3)
}
}
return maxSum
}
Go details:
:=declares once,=reassigns.maxSumis declared with:=the first time. Later lines use plain=to reuse it for a different purpose. Writing:=again in the same scope would be a compile error because there would be no new variable.- Built-in
max. Go 1.21 and later includemaxandminfor ordered types, so the code needs no helper. - A descending loop is written with the post statement
i--and the conditioni >= 0. Sinceintis signed, the loop ends cleanly at -1. - The sentinel.
-(1 << 31)is -2147483648. It is far below any total for ordinary inputs, but sums beyond that range would not be handled correctly.
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]. Take the middle range at indices 3 to 5 ([7, -2, 9]). Then sum1 = leftMaxSum[2] = 5, sum2 = 14 and sum3 = rightMaxSum[6] = 9, for a total of 28. No other middle range gives more.
Why the recurrence works
The line maxSum = nums[i] + max(maxSum, 0) is the heart of the precomputation. A subarray that ends at i either is just nums[i] or continues a subarray that ended at i-1. Continuing helps only when that earlier sum is positive, so the code adds max(maxSum, 0). The prefix table then takes the larger of the previous prefix answer and this run, because the best subarray of nums[0..i] either ends at i or lies completely before it. The suffix table is the mirror image, scanning from the right.
Complexity
Time: O(n²). The two precomputing passes are O(n). The final nested loops visit each pair (i, j) with i <= j once, about n²/2 pairs, with constant work each.
Space: O(n). The two arrays hold n values each.
Edge cases and common mistakes
- Exactly three numbers. Each subarray is one element, and the answer is their sum. The loops allow only
i = 1andj = 1. - All negative numbers. For
[-1, -2, -3]the answer is -6, since all three must be used. Starting the finalmaxSumat 0 would give a wrong answer. - All positive numbers. For
[1, 2, 3, 4, 5]the answer is 15: the three subarrays together can cover everything. - Overlapping subarrays.
sum1must come fromleftMaxSum[i-1]andsum3fromrightMaxSum[j+1]. Using indexiorjwould let the outer subarrays share elements with the middle. - Empty outer subarrays. The bounds
i >= 1andj <= n-2keep both outer subarrays non-empty. - Resetting the running sum.
sum2must be created peri, inside the outer loop.
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.