Problem overview
The method maxSumOfThreeSubarrays receives an Array[Int] called nums with at least three numbers. It must choose three non-overlapping, non-empty, continuous subarrays so that the total of their sums is as large as possible, and return that total. The three subarrays are taken from left to right, and they can't share an element.
In the example, nums = [2, 3, -8, 7, -2, 9, -9, 7, -2, 4]. The subarrays [2, 3], [7, -2, 9] and [7, -2, 4] have sums 5, 14 and 9, so the answer is 28. The -8 and the -9 are left out because they sit between the chosen subarrays.
Approach: try every triple of subarrays
This first version of the lesson is the brute force. A subarray is fixed by its first index and its last index, so three subarrays are fixed by six indices:
i1 <= j1 < i2 <= j2 < i3 <= j3
The code tries every such combination with six nested loops, adds up the three sums and keeps the best. Each later subarray must start after the previous one ends, which is why i2 begins at j1 + 1 and i3 at j2 + 1. That rule is what guarantees the subarrays don't overlap.
The sums aren't recomputed from scratch each time. The loop over j1 adds nums(j1) to sum1 as the first subarray grows by one element, and the same is done for sum2 and sum3. Every time the innermost loop has extended the third subarray, the total sum1 + sum2 + sum3 is a complete candidate.
This approach is correct but slow: the number of index combinations grows with the sixth power of n. It works for the small inputs of this lesson, and it is the starting point for the faster lessons on the same problem, which avoid trying so many combinations.
Step by step
- Store the length in
nwithval, since it never changes and the loops use it many times. - Create
maxSumas avarstarting atInt.MinValue. The answer can be negative, so 0 would be a wrong starting value.Int.MinValueis below every possible sum, so the first candidate replaces it. - Loop
i1over all start positions of the first subarray, from 0 untiln. - Declare
sum1as avarequal to 0 inside that loop. Each new start gets a fresh, empty sum. - Loop
j1fromi1untiln, the end of the first subarray. It starts ati1because the subarray needs at least one element. - Add
nums(j1)tosum1. Nowsum1is the sum ofnums(i1)throughnums(j1). - Loop
i2fromj1 + 1untiln, the start of the second subarray. It must come after the first one ends. - Declare
sum2as avarequal to 0 inside that loop. - Loop
j2fromi2untiln, the end of the second subarray. - Add
nums(j2)tosum2. - Loop
i3fromj2 + 1untiln, the start of the third subarray. - Declare
sum3as avarequal to 0 inside that loop. - Loop
j3fromi3untiln, the end of the third subarray. - Add
nums(j3)tosum3. - Update
maxSumwithmath.max(maxSum, sum1 + sum2 + sum3). - Write
maxSumas the last line of the method, after the outermost loop. Scala returns the last expression, so noreturnis needed.
The solution in Scala
class Solution {
def maxSumOfThreeSubarrays(nums: Array[Int]): Int = {
val n = nums.length
var maxSum = Int.MinValue
for (i1 <- 0 until n) {
var sum1 = 0
for (j1 <- i1 until n) {
sum1 += nums(j1)
for (i2 <- (j1 + 1) until n) {
var sum2 = 0
for (j2 <- i2 until n) {
sum2 += nums(j2)
for (i3 <- (j2 + 1) until n) {
var sum3 = 0
for (j3 <- i3 until n) {
sum3 += nums(j3)
maxSum = math.max(maxSum, sum1 + sum2 + sum3)
}
}
}
}
}
}
maxSum
}
}
The Scala details worth a closer look:
untilversusto.0 until nis a range that stops beforen, while0 to nincludesn. For array indices,untilis the one you want, since valid indices end atn - 1.- Empty ranges are fine. When
j1 + 1equalsn, the rangen until nis empty and the loop body simply doesn't run. That is how a subarray at the very end of the array leaves no room for the others, with no special check. - Where each
varlives. Avardeclared inside a loop body is created anew on every pass. That is whysum2andsum3start from 0 for each new start index, whilesum1keeps growing across thej1loop. Int.MinValueis -2147483648. Adding to it would wrap around, but this code only compares it withmath.max, so that never happens.- The parentheses in
(j1 + 1) until nare optional but make the range easy to read.
Complexity
Time: O(n^6). Each of the six loops runs up to n times, and the loops are nested. The actual number of combinations is smaller by a constant factor, since the indices must be in increasing order, but it still grows with the sixth power. Doubling the length of the array makes the work about 64 times larger.
Space: O(1). Only a few integers are stored.
Edge cases and common mistakes
- Exactly three numbers.
[1, 2, 3]has only one way to split, one element per subarray, so the answer is 6. - All numbers negative. For
[-1, -2, -3]the answer is -6, and for[-5, -1, -2, -3, -4]it is -6 too, from the single elements -1, -2 and -3. StartingmaxSumat 0 would wrongly return 0. - Skipping elements is allowed. In
[1, -5, 1, -5, 1]the answer is 3: three single-element subarrays with the two -5s left between them. - Starting
i2atj1. That would let the second subarray reuse the last element of the first, so the subarrays would overlap. - Putting
var sum1 = 0outside thei1loop. The sum would carry over between different start positions and count elements that don't belong to the subarray. - Using
j1 <- i1 + 1 until n. A subarray would then need at least two elements, and the single-element subarrays would be missed.
Testing
[2, 3, -8, 7, -2, 9, -9, 7, -2, 4]gives28.[1, 2, 3]gives6.[-1, -2, -3]gives-6.[-5, -1, -2, -3, -4]gives-6.[1, -5, 1, -5, 1]gives3.
