LeetCode 689 - Maximum Sum of 3 Non-Overlapping Subarrays
The problem asks us to select exactly three non-overlapping subarrays from the input array nums, where each subarray has length k. Among all valid choices, we must maximize the total sum of all elements contained in those three subarrays.
Difficulty: 🔴 Hard
Topics: Array, Dynamic Programming, Sliding Window, Prefix Sum
Solution
Problem Understanding
The problem asks us to select exactly three non-overlapping subarrays from the input array nums, where each subarray has length k. Among all valid choices, we must maximize the total sum of all elements contained in those three subarrays.
The output is not the subarrays themselves, but the starting indices of those three subarrays. Since the intervals must not overlap, if one subarray starts at index i, then the next valid subarray must start at index at least i + k.
The lexicographical ordering requirement is extremely important. If multiple solutions produce the same maximum total sum, we must return the one with the smallest indices when compared from left to right. For example, [0,3,5] is lexicographically smaller than [1,3,5] because the first differing index is smaller.
The input constraints are large enough that brute force enumeration becomes impractical. The array length can be as large as 2 * 10^4, which means any cubic or even quadratic-heavy solution will struggle. This strongly suggests that we need a linear or near-linear algorithm.
Several details are important:
- Every subarray has exactly length
k - We must choose exactly three subarrays
- The chosen subarrays cannot overlap
- Multiple optimal answers may exist, and we must return the lexicographically smallest one
- The constraints guarantee that at least three subarrays can exist because
k <= floor(nums.length / 3)
A naive implementation can easily fail in tie-breaking scenarios. Another common bug is accidentally allowing overlapping intervals when computing the best combinations independently.
Approaches
Brute Force Approach
The most straightforward solution is to enumerate every possible combination of three subarrays.
First, we compute the sum of every window of size k. If there are m = n - k + 1 possible windows, we can try all triples (i, j, l) such that:
i + k <= jj + k <= l
For every valid triple, we compute:
windowSum[i] + windowSum[j] + windowSum[l]
We track the maximum total and update the answer whenever we find a better combination.
This approach is correct because it exhaustively checks every valid possibility. However, it is far too slow. There can be roughly O(n) possible starting positions for each subarray, leading to an O(n^3) search space.
With n = 2 * 10^4, cubic complexity is infeasible.
Key Insight for the Optimal Solution
The critical observation is that once we fix the middle subarray, the left and right choices become independent optimization problems.
Suppose the middle subarray starts at index mid.
Then:
- The left subarray must end before
mid - The right subarray must begin after
mid + k - 1
For every possible middle position, we want:
- The best left window before it
- The best right window after it
If we precompute these efficiently, then every middle position can be evaluated in constant time.
This transforms the problem from trying all triples into:
- Precompute all window sums
- Precompute the best left index for every position
- Precompute the best right index for every position
- Iterate through possible middle windows
This reduces the complexity to linear time.
Approach Comparison
| Approach | Time Complexity | Space Complexity | Notes |
|---|---|---|---|
| Brute Force | O(n³) | O(n) | Enumerates all valid triples of subarrays |
| Optimal | O(n) | O(n) | Uses sliding window and precomputed best left/right intervals |
Algorithm Walkthrough
Step 1: Compute All Window Sums
First, compute the sum of every subarray of length k.
We use a sliding window because recalculating each sum independently would be inefficient.
If:
nums = [1,2,1,2,6,7,5,1]
k = 2
Then the window sums become:
[3,3,3,8,13,12,6]
Each entry represents the sum of a size-k subarray starting at that index.
Step 2: Build the Best Left Array
We create an array called bestLeft.
For every position i, bestLeft[i] stores the index of the maximum-sum window in the range [0, i].
If multiple windows have the same sum, we keep the earlier index to preserve lexicographical ordering.
Example:
windowSums = [3,3,3,8,13,12,6]
bestLeft = [0,0,0,3,4,4,4]
At position 5, the best window seen so far starts at index 4 because sum 13 is largest.
Step 3: Build the Best Right Array
Now create bestRight.
For every position i, bestRight[i] stores the index of the maximum-sum window in the range [i, end].
This array is built from right to left.
Tie-breaking is subtle here. We use >= instead of > so that earlier indices are preferred during ties.
Example:
bestRight = [4,4,4,4,4,5,6]
Step 4: Enumerate the Middle Window
Now iterate through every valid middle window.
If the middle starts at mid:
- Left candidate is
bestLeft[mid - k] - Right candidate is
bestRight[mid + k]
This guarantees non-overlap.
The total becomes:
windowSums[left] + windowSums[mid] + windowSums[right]
Track the maximum total and update the answer when necessary.
Step 5: Return the Best Triple
Because of our tie-breaking rules in bestLeft and bestRight, the first optimal solution encountered is automatically lexicographically smallest.
Why it works
The algorithm works because every valid solution can be uniquely represented by its middle interval. Once the middle interval is fixed, the optimal left and right intervals become independent subproblems.
bestLeft guarantees that we always choose the highest-sum valid left interval. bestRight guarantees the same for the right interval. Therefore, every middle position is paired with the best possible surrounding intervals.
Since we evaluate all valid middle intervals, the global optimum must be found.
Python Solution
from typing import List
class Solution:
def maxSumOfThreeSubarrays(self, nums: List[int], k: int) -> List[int]:
n = len(nums)
# Compute all window sums
window_sums = []
current_sum = sum(nums[:k])
window_sums.append(current_sum)
for i in range(k, n):
current_sum += nums[i]
current_sum -= nums[i - k]
window_sums.append(current_sum)
m = len(window_sums)
# best_left[i] = index of best window in [0..i]
best_left = [0] * m
best_index = 0
for i in range(m):
if window_sums[i] > window_sums[best_index]:
best_index = i
best_left[i] = best_index
# best_right[i] = index of best window in [i..m-1]
best_right = [0] * m
best_index = m - 1
for i in range(m - 1, -1, -1):
if window_sums[i] >= window_sums[best_index]:
best_index = i
best_right[i] = best_index
max_total = 0
answer = []
# Enumerate middle window
for mid in range(k, m - k):
left = best_left[mid - k]
right = best_right[mid + k]
total = (
window_sums[left]
+ window_sums[mid]
+ window_sums[right]
)
if total > max_total:
max_total = total
answer = [left, mid, right]
return answer
The implementation begins by computing all fixed-length window sums using a sliding window. This converts the original problem into selecting three indices from the window_sums array.
Next, best_left is constructed by scanning from left to right. At every position, it stores the index of the largest window sum encountered so far. Since we only update on strictly greater sums, earlier indices are preserved during ties.
The best_right array is built similarly but from right to left. Here we use >= so that earlier indices win tie-breaks when scanning backward.
Finally, we iterate through every possible middle interval. For each middle index, we instantly retrieve the optimal non-overlapping left and right intervals using the precomputed arrays. This avoids nested searching and reduces the complexity to linear time.
Go Solution
func maxSumOfThreeSubarrays(nums []int, k int) []int {
n := len(nums)
// Compute window sums
windowSums := []int{}
currentSum := 0
for i := 0; i < k; i++ {
currentSum += nums[i]
}
windowSums = append(windowSums, currentSum)
for i := k; i < n; i++ {
currentSum += nums[i]
currentSum -= nums[i-k]
windowSums = append(windowSums, currentSum)
}
m := len(windowSums)
// bestLeft[i] = best window index in [0..i]
bestLeft := make([]int, m)
bestIndex := 0
for i := 0; i < m; i++ {
if windowSums[i] > windowSums[bestIndex] {
bestIndex = i
}
bestLeft[i] = bestIndex
}
// bestRight[i] = best window index in [i..m-1]
bestRight := make([]int, m)
bestIndex = m - 1
for i := m - 1; i >= 0; i-- {
if windowSums[i] >= windowSums[bestIndex] {
bestIndex = i
}
bestRight[i] = bestIndex
}
maxTotal := 0
answer := []int{}
// Enumerate middle window
for mid := k; mid < m-k; mid++ {
left := bestLeft[mid-k]
right := bestRight[mid+k]
total := windowSums[left] +
windowSums[mid] +
windowSums[right]
if total > maxTotal {
maxTotal = total
answer = []int{left, mid, right}
}
}
return answer
}
The Go implementation closely mirrors the Python version. Slices are used instead of Python lists, and explicit initialization is required with make.
Integer overflow is not a concern because the constraints keep sums comfortably within Go's int range.
One subtle detail is preserving lexicographical ordering. The same tie-breaking logic is used:
>forbestLeft>=forbestRight
This guarantees the lexicographically smallest optimal answer.
Worked Examples
Example 1
Input:
nums = [1,2,1,2,6,7,5,1]
k = 2
Step 1: Window Sums
| Start Index | Subarray | Sum |
|---|---|---|
| 0 | [1,2] | 3 |
| 1 | [2,1] | 3 |
| 2 | [1,2] | 3 |
| 3 | [2,6] | 8 |
| 4 | [6,7] | 13 |
| 5 | [7,5] | 12 |
| 6 | [5,1] | 6 |
windowSums = [3,3,3,8,13,12,6]
Step 2: Build bestLeft
| i | Best Window So Far | bestLeft[i] |
|---|---|---|
| 0 | 0 | 0 |
| 1 | 0 | 0 |
| 2 | 0 | 0 |
| 3 | 3 | 3 |
| 4 | 4 | 4 |
| 5 | 4 | 4 |
| 6 | 4 | 4 |
Step 3: Build bestRight
| i | Best Window From Right | bestRight[i] |
|---|---|---|
| 6 | 6 | 6 |
| 5 | 5 | 5 |
| 4 | 4 | 4 |
| 3 | 4 | 4 |
| 2 | 4 | 4 |
| 1 | 4 | 4 |
| 0 | 4 | 4 |
Step 4: Evaluate Middle Windows
| Mid | Left | Right | Total |
|---|---|---|---|
| 2 | 0 | 4 | 19 |
| 3 | 0 | 5 | 23 |
| 4 | 0 | 6 | 22 |
Best result:
[0,3,5]
Example 2
Input:
nums = [1,2,1,2,1,2,1,2,1]
k = 2
Window Sums
[3,3,3,3,3,3,3,3]
All windows have identical sums.
Because the algorithm preserves earlier indices during ties:
bestLeftalways prefers smaller indicesbestRightalso resolves ties lexicographically
The result becomes:
[0,2,4]
which is the lexicographically smallest valid answer.
Complexity Analysis
| Measure | Complexity | Explanation |
|---|---|---|
| Time | O(n) | Each array is processed a constant number of times |
| Space | O(n) | Additional arrays store window sums and best indices |
The algorithm performs several linear passes:
- One pass to compute window sums
- One pass for
bestLeft - One pass for
bestRight - One pass to evaluate middle intervals
Since all operations inside each pass are constant time, the overall runtime is linear.
The extra space comes from storing:
window_sumsbest_leftbest_right
Each has size proportional to n.
Test Cases
from typing import List
class Solution:
def maxSumOfThreeSubarrays(self, nums: List[int], k: int) -> List[int]:
n = len(nums)
window_sums = []
current_sum = sum(nums[:k])
window_sums.append(current_sum)
for i in range(k, n):
current_sum += nums[i]
current_sum -= nums[i - k]
window_sums.append(current_sum)
m = len(window_sums)
best_left = [0] * m
best_index = 0
for i in range(m):
if window_sums[i] > window_sums[best_index]:
best_index = i
best_left[i] = best_index
best_right = [0] * m
best_index = m - 1
for i in range(m - 1, -1, -1):
if window_sums[i] >= window_sums[best_index]:
best_index = i
best_right[i] = best_index
max_total = 0
answer = []
for mid in range(k, m - k):
left = best_left[mid - k]
right = best_right[mid + k]
total = (
window_sums[left]
+ window_sums[mid]
+ window_sums[right]
)
if total > max_total:
max_total = total
answer = [left, mid, right]
return answer
sol = Solution()
assert sol.maxSumOfThreeSubarrays(
[1,2,1,2,6,7,5,1], 2
) == [0,3,5] # provided example
assert sol.maxSumOfThreeSubarrays(
[1,2,1,2,1,2,1,2,1], 2
) == [0,2,4] # tie-breaking lexicographically
assert sol.maxSumOfThreeSubarrays(
[1,1,1,1,1,1], 1
) == [0,1,2] # smallest possible windows
assert sol.maxSumOfThreeSubarrays(
[9,8,7,6,5,4,3,2,1], 2
) == [0,2,4] # decreasing values
assert sol.maxSumOfThreeSubarrays(
[1,2,3,4,5,6,7,8,9], 2
) == [3,5,7] # increasing values
assert sol.maxSumOfThreeSubarrays(
[4,5,10,6,11,17,4,5,6,1], 1
) == [2,4,5] # k = 1
assert sol.maxSumOfThreeSubarrays(
[5,5,5,5,5,5,5,5,5], 3
) == [0,3,6] # all windows equal
assert sol.maxSumOfThreeSubarrays(
[100,1,1,100,1,1,100], 1
) == [0,3,6] # separated peaks
assert sol.maxSumOfThreeSubarrays(
[1,2,3,1,2,3,1,2,3], 2
) == [0,3,6] # repeated patterns
Test Summary
| Test | Why |
|---|---|
[1,2,1,2,6,7,5,1], k=2 |
Validates the official example |
[1,2,1,2,1,2,1,2,1], k=2 |
Verifies lexicographical tie-breaking |
[1,1,1,1,1,1], k=1 |
Tests minimum window size |
| Decreasing sequence | Ensures optimal windows are found early |
| Increasing sequence | Ensures optimal windows are found late |
k=1 case |
Simplifies windows into individual elements |
| All equal values | Tests tie-handling correctness |
| Distinct peaks | Ensures separated maxima are chosen |
| Repeated patterns | Tests multiple equivalent structures |
Edge Cases
All Window Sums Are Equal
When every window has the same sum, many solutions are equally optimal. A careless implementation might return arbitrary indices depending on traversal order.
This implementation handles the situation correctly by carefully choosing tie-breaking conditions:
bestLeftonly updates on strictly greater sumsbestRightupdates on greater-than-or-equal sums while scanning backward
Together, these rules ensure the lexicographically smallest solution is returned.
Smallest Possible Window Size
When k = 1, every element becomes its own subarray. The problem reduces to selecting three individual elements with maximum total value.
Some implementations accidentally assume windows contain multiple elements and may introduce off-by-one errors. This solution works naturally because the sliding window logic still applies correctly for size 1.
Maximum Overlap Risk
A common bug is accidentally selecting overlapping windows when combining independently optimized intervals.
For example, if the middle interval starts at mid, then:
- Left must come from indices up to
mid - k - Right must come from indices starting at
mid + k
This implementation enforces those boundaries explicitly, guaranteeing that no overlap can occur.