Files
codetutor/backend/data/questions/maximum-sum-circular-subarray.yaml

217 lines
9.7 KiB
YAML

title: Maximum Sum Circular Subarray
slug: maximum-sum-circular-subarray
difficulty: medium
leetcode_id: 918
leetcode_url: https://leetcode.com/problems/maximum-sum-circular-subarray/
categories:
- arrays
- dynamic-programming
patterns:
- slug: dynamic-programming
is_optimal: true
function_signature: "def max_subarray_sum_circular(nums: list[int]) -> int:"
test_cases:
visible:
- input: { nums: [1, -2, 3, -2] }
expected: 3
- input: { nums: [5, -3, 5] }
expected: 10
- input: { nums: [-3, -2, -3] }
expected: -2
hidden:
- input: { nums: [1] }
expected: 1
- input: { nums: [-1] }
expected: -1
- input: { nums: [3, -1, 2, -1] }
expected: 4
- input: { nums: [3, -2, 2, -3] }
expected: 3
- input: { nums: [1, 2, 3, 4, 5] }
expected: 15
description: |
Given a **circular integer array** `nums` of length `n`, return *the maximum possible sum of a non-empty **subarray** of* `nums`.
A **circular array** means the end of the array connects to the beginning of the array. Formally, the next element of `nums[i]` is `nums[(i + 1) % n]` and the previous element of `nums[i]` is `nums[(i - 1 + n) % n]`.
A **subarray** may only include each element of the fixed buffer `nums` at most once. Formally, for a subarray `nums[i], nums[i + 1], ..., nums[j]`, there does not exist `i <= k1`, `k2 <= j` with `k1 % n == k2 % n`.
constraints: |
- `n == nums.length`
- `1 <= n <= 3 * 10^4`
- `-3 * 10^4 <= nums[i] <= 3 * 10^4`
examples:
- input: "nums = [1,-2,3,-2]"
output: "3"
explanation: "Subarray [3] has maximum sum 3."
- input: "nums = [5,-3,5]"
output: "10"
explanation: "Subarray [5,5] has maximum sum 5 + 5 = 10. The subarray wraps around from the end to the beginning."
- input: "nums = [-3,-2,-3]"
output: "-2"
explanation: "Subarray [-2] has maximum sum -2. When all elements are negative, we must pick the least negative one."
explanation:
intuition: |
Imagine the array as a circular track where you can start anywhere and collect consecutive elements. You want to find the stretch that gives you the maximum total.
The key insight is that there are only **two possible cases** for the maximum subarray in a circular array:
**Case 1: The maximum subarray does NOT wrap around** — It's a normal contiguous subarray somewhere in the middle. This is exactly what Kadane's algorithm solves.
**Case 2: The maximum subarray WRAPS around** — It uses elements from the end AND the beginning. Think of it like this: if the best subarray wraps around, it means we're *excluding* some elements in the middle. Those excluded elements form a contiguous subarray themselves!
Here's the beautiful insight: if we exclude a contiguous subarray from the total sum, we want to exclude the **minimum sum subarray** to maximise what's left. So:
- `Wrap-around max = Total sum - Minimum subarray sum`
By computing both the maximum subarray (Kadane's) and the minimum subarray (inverted Kadane's), we can handle both cases and return the larger result.
One edge case: if all elements are negative, the minimum subarray is the entire array, making the wrap-around sum equal to zero. But we must return a non-empty subarray, so we take the regular maximum (the least negative element).
approach: |
We solve this using **Kadane's Algorithm with a twist** — running it twice to find both maximum and minimum subarrays.
**Step 1: Initialise tracking variables**
- `total_sum`: Accumulate the sum of all elements
- `max_sum`: Track the maximum subarray sum (standard Kadane's)
- `current_max`: Current maximum ending at this position
- `min_sum`: Track the minimum subarray sum (inverted Kadane's)
- `current_min`: Current minimum ending at this position
&nbsp;
**Step 2: Iterate through the array once**
- Add each element to `total_sum`
- For maximum subarray: `current_max = max(num, current_max + num)`, then update `max_sum`
- For minimum subarray: `current_min = min(num, current_min + num)`, then update `min_sum`
&nbsp;
**Step 3: Handle the two cases**
- **Case 1 (no wrap):** The answer is `max_sum` from Kadane's
- **Case 2 (wrap around):** The answer is `total_sum - min_sum`
&nbsp;
**Step 4: Handle the all-negative edge case**
- If `max_sum < 0`, all elements are negative
- In this case, `total_sum - min_sum = 0` (excluding everything), which is invalid
- Return `max_sum` directly (the least negative element)
&nbsp;
**Step 5: Return the result**
- Return `max(max_sum, total_sum - min_sum)`
common_pitfalls:
- title: Forgetting the All-Negative Case
description: |
When all elements are negative, the minimum subarray is the entire array, so `total_sum - min_sum = 0`. But the problem requires a **non-empty** subarray, so returning `0` is wrong.
For example, with `nums = [-3, -2, -3]`:
- `total_sum = -8`
- `min_sum = -8` (the whole array)
- `total_sum - min_sum = 0` — incorrect!
The correct answer is `-2` (the least negative element). Always check if `max_sum < 0` and return it directly in that case.
wrong_approach: "Return max(max_sum, total_sum - min_sum) without checking for all-negative"
correct_approach: "If max_sum < 0, return max_sum directly"
- title: Using Brute Force with Circular Wrapping
description: |
Trying to enumerate all circular subarrays by doubling the array or using modular arithmetic leads to O(n^2) complexity:
- For each starting index `i`, try all ending indices
- This is way too slow for `n = 3 * 10^4`
The key insight is recognising that a wrap-around subarray is equivalent to excluding a middle subarray. This transforms the problem into finding both max and min subarrays in a single O(n) pass.
wrong_approach: "Enumerate all circular subarrays"
correct_approach: "Use the total_sum - min_sum trick for wrap-around case"
- title: Running Kadane's Twice Separately
description: |
You might think you need two separate passes through the array — one for maximum and one for minimum. But both can be computed in a **single pass** since they're independent calculations at each position.
Combining them saves a constant factor and keeps the code cleaner.
wrong_approach: "Two separate loops through the array"
correct_approach: "Single loop computing both max and min simultaneously"
key_takeaways:
- "**Circular to linear transformation**: A wrap-around subarray equals `total_sum - (middle excluded subarray)`. Finding the max wrap-around is equivalent to finding the min middle subarray."
- "**Inverted Kadane's**: To find the minimum subarray, apply Kadane's logic with `min` instead of `max`."
- "**Edge case awareness**: Always consider what happens when all elements share the same sign (all negative or all positive)."
- "**Foundation for harder problems**: This technique of transforming circular problems into linear ones applies to many variants."
time_complexity: "O(n). We traverse the array exactly once, performing constant-time operations at each element."
space_complexity: "O(1). We only use a fixed number of variables (`total_sum`, `max_sum`, `current_max`, `min_sum`, `current_min`) regardless of input size."
solutions:
- approach_name: Modified Kadane's Algorithm
is_optimal: true
code: |
def max_subarray_sum_circular(nums: list[int]) -> int:
# Track both max and min subarrays simultaneously
total_sum = 0
max_sum = nums[0]
current_max = 0
min_sum = nums[0]
current_min = 0
for num in nums:
# Accumulate total for wrap-around calculation
total_sum += num
# Standard Kadane's for maximum subarray
current_max = max(num, current_max + num)
max_sum = max(max_sum, current_max)
# Inverted Kadane's for minimum subarray
current_min = min(num, current_min + num)
min_sum = min(min_sum, current_min)
# Edge case: all elements negative
# total_sum - min_sum would be 0, but we need non-empty subarray
if max_sum < 0:
return max_sum
# Return the better of: no-wrap (max_sum) or wrap (total - min)
return max(max_sum, total_sum - min_sum)
explanation: |
**Time Complexity:** O(n) — Single pass through the array.
**Space Complexity:** O(1) — Only five tracking variables used.
We compute both the maximum and minimum subarray sums in one pass. The maximum handles Case 1 (no wrap), and `total_sum - min_sum` handles Case 2 (wrap around). We take the larger, with special handling when all elements are negative.
- approach_name: Brute Force
is_optimal: false
code: |
def max_subarray_sum_circular(nums: list[int]) -> int:
n = len(nums)
max_sum = nums[0]
# Try every starting position
for i in range(n):
current_sum = 0
# Try every length (1 to n)
for length in range(1, n + 1):
# Use modulo for circular indexing
current_sum += nums[(i + length - 1) % n]
max_sum = max(max_sum, current_sum)
return max_sum
explanation: |
**Time Complexity:** O(n^2) — Nested loops trying all starting positions and lengths.
**Space Complexity:** O(1) — Only tracking current and max sums.
This approach explicitly tries every possible circular subarray by iterating through all starting positions and lengths. While correct, it's too slow for the given constraints and will result in TLE. Included to illustrate why the Kadane's approach is necessary.