Maximum Subarray Sum, Including the Circular Case
Pattern: Kadane’s Algorithm (Best Subarray) · Difficulty: Medium · Asked at: Amazon, Microsoft, LinkedIn, Apple
Classic version: LeetCode 53 · LeetCode 918
Problem
max_subarray(nums): the largest sum of any non-empty contiguous subarray.max_subarray_circular(nums): the same, but the array is circular (a subarray may wrap from the end to the start; each element is used at most once).
Examples
max_subarray([-2, 1, -3, 4, -1, 2, 1, -5, 4]) → 6 # [4, -1, 2, 1]
max_subarray([-3, -1, -2]) → -1
max_subarray_circular([5, -3, 5]) → 10 # [5, 5] wrapping
max_subarray_circular([-3, -2, -3]) → -2
Starter code
def max_subarray(nums: list[int]) -> int:
pass
def max_subarray_circular(nums: list[int]) -> int:
pass
Hints
Hint 1
At each index, the best subarray ending here either extends the best one ending at the previous index or starts fresh: cur = max(x, cur + x).
Hint 2
A wrapping subarray is the whole array minus a non-wrapping middle chunk. Maximising it = total − (minimum subarray). Watch the all-negative case.
Where this shows up in data engineering
“Best contiguous period” questions: the most profitable stretch of days, the longest/largest run of positive net flow, the worst drawdown (minimum subarray). The circular version models daily cycles (a period can span midnight). Kadane is also a gentle introduction to DP state design: what must you remember about the past?
Solution
def max_subarray(nums):
best = cur = nums[0]
for x in nums[1:]:
cur = max(x, cur + x) # extend or restart
best = max(best, cur)
return best
def max_subarray_circular(nums):
total = 0
cur_max = cur_min = 0
best_max, best_min = nums[0], nums[0]
for x in nums:
cur_max = max(x, cur_max + x)
best_max = max(best_max, cur_max)
cur_min = min(x, cur_min + x)
best_min = min(best_min, cur_min)
total += x
# all negative: the "wrap" would be empty, which isn't allowed
return best_max if best_max < 0 else max(best_max, total - best_min)
Tests
Your solution should pass these:
assert max_subarray([-2, 1, -3, 4, -1, 2, 1, -5, 4]) == 6
assert max_subarray([1]) == 1
assert max_subarray([5, 4, -1, 7, 8]) == 23
assert max_subarray([-3, -1, -2]) == -1
assert max_subarray_circular([1, -2, 3, -2]) == 3
assert max_subarray_circular([5, -3, 5]) == 10
assert max_subarray_circular([-3, -2, -3]) == -2
assert max_subarray_circular([3, -1, 2, -1]) == 4
Explanation
Kadane as DP: cur = best sum of a subarray ending at the current index. Either extend the previous best (cur + x) or start over (x); a negative running sum never helps the future. The answer is the max cur seen. O(n) time, O(1) space.
Circular: an optimal subarray either doesn’t wrap (plain Kadane) or wraps, in which case the excluded part is a contiguous middle subarray, and to maximise what’s kept you minimise what’s excluded: total - min_subarray.
The trap: if every number is negative, total - min_subarray is 0 (an empty selection), which is invalid. Return the plain maximum then.
Returning the indices: record start when you restart (x > cur + x) and update (best_start, end) when best improves.
Follow-up questions
Return the start and end index of the best subarray.
Track a candidate start that resets to i whenever x > cur + x; when best improves, save (start, i). Ties: decide whether you want the earliest or shortest and document it.
Maximum drawdown of a cumulative P&L series?
Track the running peak; drawdown at each point = peak − value; answer = max drawdown. Equivalent to the minimum subarray of the daily changes.