Problem overview
The method maxSumOfThreeSubarrays receives an array nums with at least three integers and returns the largest total of three non-empty, non-overlapping subarrays, taken in order from left to right.
For nums = [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 + 9.
Approach: precompute the best first and third subarray
Fix the second (middle) subarray at nums[i..j]. The best first subarray is the best one anywhere in nums[0..i - 1], and the best third one is the best anywhere in nums[j + 1..n - 1]. The slower version of this lesson recomputes those two values with a fresh pass over the array for every pair (i, j), which costs O(n³) in total.
The values depend only on where the prefix ends or where the suffix starts, so they can be computed once and stored:
leftMaxSum(i)is the maximum subarray sum insidenums[0..i];rightMaxSum(i)is the maximum subarray sum insidenums[i..n - 1].
Each takes one pass to fill, using the idea behind Kadane's algorithm. After that, a lookup replaces each repeated pass, and only the two loops over (i, j) remain: O(n²). In the example, leftMaxSum(2) is 5 (the subarray [2, 3]) and rightMaxSum(6) is 9 (the subarray [7, -2, 4]). With the middle subarray [7, -2, 9], which sums to 14, the total is 5 + 14 + 9 = 28.
Step by step
- Store the length as
val n. - Create
leftMaxSumwithArray.fill(n)(0). It can be aval: the elements change, but the variable keeps pointing to the same array. - Set
leftMaxSum(0)tonums(0). The first prefix has only one subarray. - Create
maxSumas avarstarting atnums(0). It holds the best sum of a subarray that ends at the current index. - Loop
ifrom 1 ton - 1. - Update
maxSumtonums(i) + math.max(maxSum, 0). The best subarray ending atieither extends the previous one, when its sum is positive, or starts ati. - Fill
leftMaxSum(i)as the larger ofleftMaxSum(i - 1)andmaxSum. The best subarray innums[0..i]either ends atior lies completely before it. - Create
rightMaxSumthe same way, as an array ofnzeros. - 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
idownwards fromn - 2to 0, with the rangen - 2 to 0 by -1. Both ends are included. - Update
maxSumtonums(i) + math.max(maxSum, 0), the same rule, now looking right. - Fill
rightMaxSum(i)as the larger ofrightMaxSum(i + 1)andmaxSum. - Reset
maxSumtoInt.MinValueto hold the best answer. The answer can be negative, so 0 would be wrong. - Loop
ifrom 1 ton - 1, the possible starts of the middle subarray. Index 1 is the smallest start that leaves the first subarray non-empty. - Take
sum1 = leftMaxSum(i - 1): the best first subarray that ends beforei. - Start
sum2at 0 as avar, since it grows as the middle subarray is extended. - Loop
jfromiton - 2withi until n - 1, so a non-empty third subarray remains. Fori = n - 1the loop is empty. - Add
nums(j)tosum2. - Take
sum3 = rightMaxSum(j + 1): the best third subarray that starts afterj. - Update
maxSumwithmath.max(maxSum, sum1 + sum2 + sum3). - End the method with
maxSum. In Scala the last expression is the result, so noreturnis needed.
The solution in Scala
class Solution {
def maxSumOfThreeSubarrays(nums: Array[Int]): Int = {
val n = nums.length
val leftMaxSum = Array.fill(n)(0)
leftMaxSum(0) = nums(0)
var maxSum = nums(0)
for (i <- 1 until n) {
maxSum = nums(i) + math.max(maxSum, 0)
leftMaxSum(i) = math.max(leftMaxSum(i - 1), maxSum)
}
val rightMaxSum = Array.fill(n)(0)
rightMaxSum(n - 1) = nums(n - 1)
maxSum = nums(n - 1)
for (i <- n - 2 to 0 by -1) {
maxSum = nums(i) + math.max(maxSum, 0)
rightMaxSum(i) = math.max(rightMaxSum(i + 1), maxSum)
}
maxSum = Int.MinValue
for (i <- 1 until n) {
val sum1 = leftMaxSum(i - 1)
var sum2 = 0
for (j <- i until n - 1) {
sum2 += nums(j)
val sum3 = rightMaxSum(j + 1)
maxSum = math.max(maxSum, sum1 + sum2 + sum3)
}
}
maxSum
}
}
The Scala details worth a closer look:
Array.fill(n)(0)builds an array ofnzeros. The first parameter list is the size and the second is the value used to fill it.valarrays are still mutable.valfixes which array the name refers to. It doesn't freeze the contents, soleftMaxSum(i) = ...is allowed.n - 2 to 0 by -1counts down, andtoincludes the final 0. Writingn - 2 until 0 by -1would stop before 0 and leaverightMaxSum(0)unset.- Reusing a
var.maxSumis assigned three different meanings in turn. That works because each phase sets it to a fresh starting value before using it. - The block's value. The final
maxSumhas noreturnbefore it. Scala evaluates it as the last expression of the method body and returns its value.
Complexity
Time: O(n²). Filling leftMaxSum and rightMaxSum takes one pass each. The last double loop tries each pair (i, j) with a constant amount of work per pair, which is quadratic. That beats the cubic brute force, but the next lesson reaches O(n).
Space: O(n). The two helper arrays have n elements each.
Edge cases and common mistakes
- All negative numbers.
[-1, -2, -3]returns-6. This is whymaxSumstarts atInt.MinValuefor the answer and atnums(0)for the run sums. - Exactly three numbers. The only choice is one element each. The loops try
i = 1andj = 1:leftMaxSum(0) + nums(1) + rightMaxSum(2)is the total of all three. - Mixing up "best ending at
i" and "best within the prefix".maxSumis the first;leftMaxSum(i)is the second. A prefix maximum must never decrease asigrows, and taking the larger of the previous value andmaxSumguarantees that. - Using
leftMaxSum(i)instead ofleftMaxSum(i - 1)for the first subarray. The first subarray would then be allowed to includenums(i), which belongs to the middle one. - Letting
jreachn - 1.rightMaxSum(j + 1)would read past the end of the array and throw an exception. - A downward loop that misses index 0. This happens with
untilinstead oftoin the range.
Testing
[2, 3, -8, 7, -2, 9, -9, 7, -2, 4]gives28.[-1, -2, -3]gives-6.[-5, -1, -4, -2, -8]gives-7, from the single elements-1,-4and-2.
