Problem overview
The method maxSumOfThreeSubarrays receives an array nums of integers, with at least three elements. It must choose three non-empty, non-overlapping, continuous subarrays so that the total of their sums is as large as possible, and return that total.
In the example [2, 3, -8, 7, -2, 9, -9, 7, -2, 4], the best choice is [2, 3], [7, -2, 9] and [7, -2, 4]. Their sums are 5, 14 and 9, so the answer is 28. The -8 and the -9 sit between the chosen pieces and are left out.
Approach: try every triple of subarrays
This first lesson on the problem uses the most direct idea. A subarray is fixed by its first index and its last index. Three subarrays are fixed by six indices, and for them to be in order and not overlap they must satisfy:
i1 <= j1 < i2 <= j2 < i3 <= j3
So six nested loops enumerate every valid choice. For each one, the total is sum1 + sum2 + sum3, and the answer is the largest total seen.
The sums are not recomputed from scratch. When the end j1 moves one step right, the subarray gets one more element, so sum1 grows by nums[j1]. The same holds for sum2 and sum3. Each of the three sums is reset to 0 whenever its start index changes.
This is slow, about O(n⁶) time, and the lessons that follow it on this problem make it faster. It is still worth writing first, because it is easy to see that it is correct: it looks at all the possibilities.
Step by step
- Store the length in
n. It is used in every loop bound and it never changes, so it is alet. - Create
maxSumas avarstarting atInt.min. The best total can be negative, so 0 is not a safe start.Int.minis smaller than any possible total, so the first triple replaces it. - Choose the first start
i1with a loop over every index. - Create
sum1at 0 inside that loop. It is the sum of the first subarray, and each new start needs a fresh one. - Choose the first end
j1fromi1to the last index. The end can equal the start, which makes a one-element subarray. - Add
nums[j1]tosum1. Nowsum1is the sum ofnums[i1...j1]. - Choose the second start
i2fromj1 + 1. Starting right after the first subarray is what keeps the two from overlapping. - Create
sum2at 0 for eachi2. - Choose the second end
j2fromi2onward. - Add
nums[j2]tosum2. - Choose the third start
i3fromj2 + 1. - Create
sum3at 0 for eachi3. - Choose the third end
j3fromi3onward. - Add
nums[j3]tosum3. - Update
maxSumwith the larger of its value andsum1 + sum2 + sum3. - Return
maxSumafter the outermost loop ends.
The solution in Swift
class Solution {
func maxSumOfThreeSubarrays(_ nums: [Int]) -> Int {
let n = nums.count
var maxSum = Int.min
for i1 in 0..<n {
var sum1 = 0
for j1 in i1..<n {
sum1 += nums[j1]
for i2 in (j1 + 1)..<n {
var sum2 = 0
for j2 in i2..<n {
sum2 += nums[j2]
for i3 in (j2 + 1)..<n {
var sum3 = 0
for j3 in i3..<n {
sum3 += nums[j3]
maxSum = max(maxSum, sum1 + sum2 + sum3)
}
}
}
}
}
}
return maxSum
}
}
The Swift details that matter here:
- Half-open ranges.
0..<nruns from 0 up ton - 1. The closed range0...nwould includen, one past the last valid index. - Empty ranges are fine, backwards ranges are not. When
j1is the last index,(j1 + 1)..<nisn..<n, which is empty, so its loop body never runs. A range whose lower bound is larger than its upper bound crashes the program. The nested starts never go that far, because each one begins at most atn. - Parentheses around
j1 + 1. They are not required, but they make the range easy to read. varandlet. The three sums andmaxSumchange, so they arevar. The loop indices are constants that each iteration gets fresh.Int.min. It is the smallest value anIntcan hold. Do not add to it, because arithmetic overflow stops a Swift program with a run-time error. Here it is only compared withmax, which is safe.
Complexity
Time: O(n⁶). There are six nested loops, each running over up to n positions, and the work inside is constant. The ordering rules shrink the real count by a constant factor, but not the exponent. Doubling the length of the array makes the program about 64 times slower, so it is only practical for small arrays.
Space: O(1). The function uses a handful of integers.
Edge cases and common mistakes
- Exactly three elements.
[1, 2, 3]has only one valid choice, three single-element subarrays, so the answer is 6. - All negative numbers.
[-1, -2, -3, -4]returns-6. Each subarray must have at least one element, so the best you can do is take the three values that hurt the least. This is whymaxSumstarts atInt.minrather than 0. - Adjacent subarrays are allowed.
[1, 1, 1, 1, 1]returns 5, using, for example,[1],[1]and[1, 1, 1]. Startingi2atj1 + 2instead ofj1 + 1would wrongly force a gap. - Not resetting a sum. If
sum2were declared outside thei2loop, it would keep the total of the previous start and give wrong answers. - Overlap. Starting
i2atj1would let the second subarray share an element with the first.
Testing
[2, 3, -8, 7, -2, 9, -9, 7, -2, 4]gives28.[1, 2, 3]gives6.[-5, -1, -3]gives-9.[-1, -2, -3, -4]gives-6.[1, 1, 1, 1, 1]gives5.
