Problem overview
The function maxSumOfThreeSubarrays receives an IntArray called nums with at least three numbers. It must choose three non-overlapping, non-empty, contiguous subarrays so that their combined sum is as large as possible, and return that sum. The subarrays can be any length, and they can sit right next to each other.
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] add up to 5 + 14 + 9 = 28, and no choice of three does better. The two negative numbers, -8 and -9, that separate the groups are left out.
Approach: try every triple of subarrays
A subarray is fixed by its first and last index. Three non-overlapping subarrays, in left-to-right order, are fixed by six indices:
i1 <= j1 < i2 <= j2 < i3 <= j3
The brute force walks through every such sextuple, adds up the three sums and keeps the largest. It is the most direct reading of the problem, and it is a good first solution because each line follows from the statement. It is also very slow, and the later lessons on this problem replace it with faster ideas.
Two ideas keep the code short:
- Build each sum while extending the subarray. When
j1moves one step right, the first subarray gains one element, sosum1grows bynums[j1]. There is no need to add the whole subarray again. - Nest the loops in left-to-right order. The start of each subarray begins after the end of the previous one, which guarantees there is no overlap and avoids counting the same triple in a different order.
Step by step
- Store
nums.sizeinnwithval, because it is used in every loop bound and never changes. - Declare
maxSumwithvar, starting atInt.MIN_VALUE. Every subarray must be non-empty, so the answer can be negative, and a starting value of 0 could beat it. The smallest possibleIntloses to any real sum. - Loop
i1over every index for the start of the first subarray. - Declare
sum1inside that loop, starting at 0 for an empty subarray. Each new start gets a fresh sum. - Loop
j1fromi1to the end. It is the last index of the first subarray, and it can equali1for a one-element subarray. - Add
nums[j1]tosum1at the top of that loop, sosum1covers[i1, j1]. - Loop
i2fromj1 + 1to the end. The second subarray starts after the first one ends. - Declare
sum2inside thei2loop, starting at 0. - Loop
j2fromi2to the end, the last index of the second subarray. - Add
nums[j2]tosum2. - Loop
i3fromj2 + 1to the end, the start of the third subarray. - Declare
sum3inside thei3loop, starting at 0. - Loop
j3fromi3to the end, the last index of the third subarray. - Add
nums[j3]tosum3. - Update
maxSumwithmaxOf(maxSum, sum1 + sum2 + sum3). At this point all three subarrays are fully chosen. - Return
maxSumafter the outermost loop finishes.
The solution in Kotlin
class Solution {
fun maxSumOfThreeSubarrays(nums: IntArray): Int {
val n = nums.size
var maxSum = Int.MIN_VALUE
for (i1 in 0 until n) {
var sum1 = 0
for (j1 in i1 until n) {
sum1 += nums[j1]
for (i2 in (j1 + 1) until n) {
var sum2 = 0
for (j2 in i2 until n) {
sum2 += nums[j2]
for (i3 in (j2 + 1) until n) {
var sum3 = 0
for (j3 in i3 until n) {
sum3 += nums[j3]
maxSum = maxOf(maxSum, sum1 + sum2 + sum3)
}
}
}
}
}
}
return maxSum
}
}
The Kotlin details worth a closer look:
untilexcludes its upper bound.i1 until nvisits0ton - 1, which are exactly the valid indices. The inclusive form would be0..n - 1.- Empty ranges are fine. When
j1 + 1is alreadyn,(j1 + 1) until nis empty and the loop body simply doesn't run. This is why no explicit bounds checks appear in the code. - Loop variables are
vals. The indices are read-only inside the body. The running sums, which change, are declared withvar. - Where each sum is declared decides when it resets.
sum2lives inside thei2loop, so every new start for the second subarray begins from 0, while it keeps growing asj2moves. The same holds forsum1andsum3. - Int.MIN_VALUE as a sentinel. It is -2147483648. If no triple exists, which happens when
numshas fewer than three elements, the function returns this value. With three or more numbers, at least one triple is always found.
Complexity
Time: O(n⁶). Each of the six indices ranges over up to n positions, and the constraint i1 <= j1 < i2 <= j2 < i3 <= j3 only cuts the count by a constant factor. The work per innermost step is constant. Doubling the length of the array multiplies the running time by roughly 64, so this version only suits small inputs.
Space: O(1). The function keeps a handful of integers.
Edge cases and common mistakes
- All negative numbers.
[-1, -2, -3]returns-6: three non-empty subarrays are required, so all three elements are used. InitializingmaxSumwith 0 would wrongly return 0. - Exactly three numbers. The only option is one element per subarray, so the answer is the sum of the whole array.
- Adjacent subarrays.
[5, 5, 5]returns15. Startingi2atj1 + 1allows the second subarray to begin right after the first, with no gap. - Skipping elements.
[1, -5, 2, -5, 3]returns6, from[1],[2]and[3]. The numbers between the chosen subarrays don't have to be used. - Allowing overlap. Starting
i2atj1instead ofj1 + 1would let two subarrays share an element and count it twice. - Declaring a sum in the wrong place. If
sum2were declared outside thei2loop, it would carry over from the previous start and produce sums of subarrays that don't exist. - Too few elements. With fewer than three numbers the function returns
Int.MIN_VALUE, but the statement guarantees at least three.
Testing
[2, 3, -8, 7, -2, 9, -9, 7, -2, 4]gives28.[-1, -2, -3]gives-6.[1, -5, 2, -5, 3]gives6.[5, 5, 5]gives15.[1, 2, 3, 4]gives10.