180 lines
7.2 KiB
YAML
180 lines
7.2 KiB
YAML
title: Maximum Subarray
|
|
slug: maximum-subarray
|
|
difficulty: medium
|
|
leetcode_id: 53
|
|
leetcode_url: https://leetcode.com/problems/maximum-subarray/
|
|
categories:
|
|
- arrays
|
|
- dynamic-programming
|
|
patterns:
|
|
- dynamic-programming
|
|
|
|
function_signature: "def max_subarray(nums: list[int]) -> int:"
|
|
|
|
test_cases:
|
|
visible:
|
|
- input: { nums: [-2, 1, -3, 4, -1, 2, 1, -5, 4] }
|
|
expected: 6
|
|
- input: { nums: [1] }
|
|
expected: 1
|
|
- input: { nums: [5, 4, -1, 7, 8] }
|
|
expected: 23
|
|
hidden:
|
|
- input: { nums: [-1] }
|
|
expected: -1
|
|
- input: { nums: [-2, -1] }
|
|
expected: -1
|
|
- input: { nums: [1, 2, 3, 4] }
|
|
expected: 10
|
|
- input: { nums: [-1, -2, -3, -4] }
|
|
expected: -1
|
|
|
|
description: |
|
|
Given an integer array `nums`, find the subarray with the largest sum, and return *its sum*.
|
|
|
|
A **subarray** is a contiguous non-empty sequence of elements within an array.
|
|
|
|
constraints: |
|
|
- `1 <= nums.length <= 10^5`
|
|
- `-10^4 <= nums[i] <= 10^4`
|
|
|
|
examples:
|
|
- input: "nums = [-2,1,-3,4,-1,2,1,-5,4]"
|
|
output: "6"
|
|
explanation: "The subarray [4,-1,2,1] has the largest sum 6."
|
|
- input: "nums = [1]"
|
|
output: "1"
|
|
explanation: "The subarray [1] has the largest sum 1."
|
|
- input: "nums = [5,4,-1,7,8]"
|
|
output: "23"
|
|
explanation: "The entire array [5,4,-1,7,8] has the largest sum 23."
|
|
|
|
explanation:
|
|
intuition: |
|
|
Imagine you're walking along a number line, collecting values. At each step, you face a simple decision: should you keep accumulating, or cut your losses and start fresh?
|
|
|
|
Think of it like this: you're tracking a "running sum" as you traverse the array. If your running sum becomes negative, it's **dragging you down** — any future subarray would be better off starting fresh without that negative baggage.
|
|
|
|
The key insight of **Kadane's Algorithm** is that at each position `i`, you have exactly two choices:
|
|
1. **Extend** the previous subarray by adding `nums[i]` (if the previous sum helps)
|
|
2. **Start fresh** at `nums[i]` (if the previous sum was negative)
|
|
|
|
Mathematically: `current_sum = max(nums[i], current_sum + nums[i])`
|
|
|
|
We also track the maximum sum seen so far, because the optimal subarray might end before the last element.
|
|
|
|
approach: |
|
|
We solve this using **Kadane's Algorithm**:
|
|
|
|
**Step 1: Initialise tracking variables**
|
|
|
|
- `current_sum = nums[0]`: The sum of the current subarray ending at position i
|
|
- `max_sum = nums[0]`: The maximum sum we've found so far
|
|
- Starting with `nums[0]` (not 0) handles all-negative arrays correctly
|
|
|
|
|
|
|
|
**Step 2: Iterate from index 1 to the end**
|
|
|
|
- For each element `nums[i]`:
|
|
- **Decide**: extend or restart? `current_sum = max(nums[i], current_sum + nums[i])`
|
|
- If `current_sum + nums[i] >= nums[i]`, extend (previous sum is non-negative)
|
|
- If `current_sum + nums[i] < nums[i]`, restart (previous sum was negative)
|
|
- **Update maximum**: `max_sum = max(max_sum, current_sum)`
|
|
|
|
|
|
|
|
**Step 3: Return the result**
|
|
|
|
- Return `max_sum` — the largest subarray sum encountered
|
|
- Note: we don't return `current_sum` because the optimal subarray might have ended earlier
|
|
|
|
|
|
|
|
This elegant algorithm makes the locally optimal choice at each step (extend or restart), which leads to the globally optimal solution.
|
|
|
|
common_pitfalls:
|
|
- title: Initialising max_sum to 0
|
|
description: |
|
|
If all elements are negative (e.g., `[-3, -2, -1]`), the maximum subarray sum is `-1`, not `0`.
|
|
|
|
Initialising `max_sum = 0` would incorrectly return `0` for such arrays, as if an empty subarray were valid (it's not — the problem requires a non-empty subarray).
|
|
|
|
Always initialise with `nums[0]` to guarantee a valid answer.
|
|
wrong_approach: "max_sum = 0"
|
|
correct_approach: "max_sum = nums[0]"
|
|
|
|
- title: Only Returning current_sum at the End
|
|
description: |
|
|
The maximum subarray doesn't necessarily include the last element! Consider `[4, -1, 2, 1, -5]`: the maximum sum is `6` (subarray `[4, -1, 2, 1]`), but `current_sum` at the end is `1`.
|
|
|
|
You must track `max_sum` throughout the traversal and update it whenever `current_sum` exceeds it.
|
|
wrong_approach: "return current_sum"
|
|
correct_approach: "Track max_sum and return it"
|
|
|
|
- title: Confusing Subarray with Subsequence
|
|
description: |
|
|
A **subarray** must be contiguous — you cannot skip elements. This is why Kadane's algorithm works: at each position, the subarray either extends from the previous position or starts fresh.
|
|
|
|
A **subsequence** allows skipping elements, which would require a different approach entirely.
|
|
wrong_approach: "Thinking you can skip elements"
|
|
correct_approach: "Subarray = contiguous sequence"
|
|
|
|
key_takeaways:
|
|
- "**Kadane's Algorithm**: The classic O(n) solution for maximum subarray — memorise it!"
|
|
- "**Local vs global optimum**: Sometimes making locally optimal choices (extend or restart) yields the global optimum"
|
|
- "**Initialisation matters**: Using `nums[0]` instead of `0` handles edge cases correctly"
|
|
- "**Pattern recognition**: This 'extend or restart' logic applies to many contiguous subarray problems"
|
|
|
|
time_complexity: "O(n). Single pass through the array, with O(1) work at each position."
|
|
space_complexity: "O(1). Only two variables (`current_sum` and `max_sum`) regardless of input size."
|
|
|
|
solutions:
|
|
- approach_name: Kadane's Algorithm
|
|
is_optimal: true
|
|
code: |
|
|
def max_subarray(nums: list[int]) -> int:
|
|
# Initialise with first element (handles all-negative arrays)
|
|
current_sum = nums[0]
|
|
max_sum = nums[0]
|
|
|
|
# Process remaining elements
|
|
for i in range(1, len(nums)):
|
|
# Decision: extend previous subarray or start fresh?
|
|
# Extend if previous sum helps, otherwise restart
|
|
current_sum = max(nums[i], current_sum + nums[i])
|
|
|
|
# Update maximum if current subarray is better
|
|
max_sum = max(max_sum, current_sum)
|
|
|
|
return max_sum
|
|
explanation: |
|
|
**Time Complexity:** O(n) — Single pass through the array.
|
|
|
|
**Space Complexity:** O(1) — Only two variables needed.
|
|
|
|
At each position, we decide whether to extend the previous subarray or start a new one. We track the maximum sum seen throughout the traversal. This greedy choice at each step produces the globally optimal result.
|
|
|
|
- approach_name: Brute Force
|
|
is_optimal: false
|
|
code: |
|
|
def max_subarray(nums: list[int]) -> int:
|
|
n = len(nums)
|
|
max_sum = nums[0]
|
|
|
|
# Try every possible starting point
|
|
for i in range(n):
|
|
current_sum = 0
|
|
# Try every possible ending point from i
|
|
for j in range(i, n):
|
|
current_sum += nums[j]
|
|
max_sum = max(max_sum, current_sum)
|
|
|
|
return max_sum
|
|
explanation: |
|
|
**Time Complexity:** O(n²) — Nested loops checking all subarrays.
|
|
|
|
**Space Complexity:** O(1) — Only tracking sums.
|
|
|
|
This approach tries every possible subarray by fixing a start index and extending to each possible end index. While correct, it's too slow for large inputs (up to 10 billion operations for n = 10^5).
|