918. Maximum Sum Circular Subarray
Problem Description
You are given a circular integer array nums
of length n
. Your task is to find the maximum possible sum of a non-empty subarray within this circular array.
In a circular array, the elements wrap around - meaning the element after the last position connects back to the first position. Mathematically:
- The next element of
nums[i]
isnums[(i + 1) % n]
- The previous element of
nums[i]
isnums[(i - 1 + n) % n]
A key constraint is that each element can be included at most once in any subarray. This means if you choose a subarray that wraps around the circular array, you cannot reuse elements.
For example, if nums = [1, -2, 3, -2]
:
- A regular subarray could be
[3]
with sum 3 - A circular subarray could be
[3, -2, 1]
(wrapping from index 2 to 0) with sum 2 - But you cannot have
[1, -2, 3, -2, 1]
as it would reuse the element at index 0
The challenge is to handle two scenarios:
- The maximum sum subarray lies entirely within the array without wrapping (standard maximum subarray problem)
- The maximum sum subarray wraps around the circular boundary
The solution cleverly handles both cases by:
- Finding the maximum subarray sum normally (Case 1)
- Finding the minimum subarray sum and subtracting it from the total sum (Case 2 - this gives us the wrapped-around maximum)
The answer is the maximum of these two cases.
Intuition
The key insight is recognizing that in a circular array, there are only two possible patterns for the maximum sum subarray:
Pattern 1: No Wrapping The maximum subarray is contained entirely within the array without using the circular property. This is just the classic Kadane's algorithm problem.
Pattern 2: Wrapping Around The maximum subarray wraps around from the end to the beginning of the array. Here's the clever observation: if the maximum subarray wraps around, then the elements NOT included in this subarray form a contiguous segment in the middle of the array.
Think about it this way: if we're taking elements from both ends (wrapping around), what we're leaving out is a continuous chunk in the middle. The sum of what we take equals total_sum - sum_of_middle_chunk
.
To maximize what we take, we need to minimize what we leave out. Therefore, finding the maximum wrapped subarray sum is equivalent to finding the minimum subarray sum and subtracting it from the total.
For example, with nums = [5, -3, 5]
:
- Total sum = 7
- Minimum subarray =
[-3]
with sum -3 - Maximum circular sum = 7 - (-3) = 10 (which corresponds to taking
[5, 5]
)
The solution maintains prefix sums to efficiently calculate both:
- Maximum subarray sum:
current_prefix_sum - minimum_prefix_sum_seen_so_far
- Minimum subarray sum:
current_prefix_sum - maximum_prefix_sum_seen_so_far
By tracking these values in a single pass, we can find both the regular maximum (Pattern 1) and the wrapped maximum (Pattern 2), then return the larger of the two.
Learn more about Queue, Divide and Conquer, Dynamic Programming and Monotonic Queue patterns.
Solution Approach
The implementation uses a single-pass algorithm with prefix sums to find both the maximum regular subarray and the minimum subarray simultaneously.
Variables Maintained:
pmi
: Minimum prefix sum seen so far (initialized to 0)pmx
: Maximum prefix sum seen so far (initialized to-inf
)s
: Current prefix sum (initialized to 0)smi
: Minimum subarray sum found (initialized toinf
)ans
: Maximum subarray sum for non-wrapping case (initialized to-inf
)
Algorithm Steps:
For each element x
in the array:
-
Update prefix sum:
s += x
- This maintains the running sum from the start of the array
-
Update maximum subarray sum (non-wrapping):
ans = max(ans, s - pmi)
- The maximum subarray ending at current position = current prefix sum - minimum prefix sum before it
- This handles Case 1 where the subarray doesn't wrap
-
Update minimum subarray sum:
smi = min(smi, s - pmx)
- The minimum subarray ending at current position = current prefix sum - maximum prefix sum before it
- This is needed for Case 2 (wrapping scenario)
-
Update prefix sum bounds:
pmi = min(pmi, s)
- Track minimum prefix sum for future calculationspmx = max(pmx, s)
- Track maximum prefix sum for future calculations
Final Answer:
Return max(ans, s - smi)
where:
ans
represents the maximum sum without wrapping (Case 1)s - smi
represents the maximum sum with wrapping (Case 2)s
is the total sum of the arraysmi
is the minimum subarray sum- Their difference gives us the maximum circular subarray sum
Why This Works:
- By maintaining both minimum and maximum prefix sums, we can calculate the best subarray sum ending at any position
- The formula
current_prefix - min_prefix
gives maximum gain - The formula
current_prefix - max_prefix
gives maximum loss (minimum subarray) - Computing both in a single pass makes the algorithm O(n) time with O(1) space
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through the algorithm with nums = [5, -3, 5]
:
Initial State:
pmi = 0
(minimum prefix sum seen)pmx = -inf
(maximum prefix sum seen)s = 0
(current prefix sum)smi = inf
(minimum subarray sum)ans = -inf
(maximum non-wrapping subarray sum)
Step 1: Process nums[0] = 5
- Update prefix sum:
s = 0 + 5 = 5
- Update max subarray:
ans = max(-inf, 5 - 0) = 5
- This represents subarray [5] with sum 5
- Update min subarray:
smi = min(inf, 5 - (-inf)) = inf
(remains inf) - Update prefix bounds:
pmi = min(0, 5) = 0
pmx = max(-inf, 5) = 5
Step 2: Process nums[1] = -3
- Update prefix sum:
s = 5 + (-3) = 2
- Update max subarray:
ans = max(5, 2 - 0) = 5
2 - 0 = 2
represents subarray [5, -3] with sum 2- We keep
ans = 5
as it's larger
- Update min subarray:
smi = min(inf, 2 - 5) = -3
2 - 5 = -3
represents subarray [-3] with sum -3
- Update prefix bounds:
pmi = min(0, 2) = 0
pmx = max(5, 2) = 5
Step 3: Process nums[2] = 5
- Update prefix sum:
s = 2 + 5 = 7
- Update max subarray:
ans = max(5, 7 - 0) = 7
7 - 0 = 7
represents subarray [5, -3, 5] with sum 7
- Update min subarray:
smi = min(-3, 7 - 5) = -3
7 - 5 = 2
represents subarray [-3, 5] with sum 2- We keep
smi = -3
as it's smaller
- Update prefix bounds:
pmi = min(0, 7) = 0
pmx = max(5, 7) = 7
Final Calculation:
- Non-wrapping maximum:
ans = 7
(subarray [5, -3, 5]) - Wrapping maximum:
s - smi = 7 - (-3) = 10
- This represents taking everything except the minimum subarray [-3]
- Which means taking [5] from the end and [5] from the beginning
- This is the circular subarray [5, 5] with sum 10
Result: max(7, 10) = 10
The algorithm correctly identifies that the maximum circular subarray sum is 10, achieved by the wrapping subarray [5, 5] that excludes the middle element -3.
Solution Implementation
1class Solution:
2 def maxSubarraySumCircular(self, nums: List[int]) -> int:
3 # Initialize variables for tracking prefix sums
4 min_prefix = 0 # Minimum prefix sum seen so far
5 max_prefix = -float('inf') # Maximum prefix sum seen so far
6
7 # Variables for tracking the maximum and minimum subarray sums
8 max_subarray_sum = -float('inf') # Maximum subarray sum (Kadane's algorithm)
9 current_sum = 0 # Running prefix sum
10 min_subarray_sum = float('inf') # Minimum subarray sum
11
12 # Iterate through each element in the array
13 for num in nums:
14 # Update the running prefix sum
15 current_sum += num
16
17 # Update maximum subarray sum using Kadane's algorithm
18 # max_subarray_sum = max(current_sum - min_prefix_before_current)
19 max_subarray_sum = max(max_subarray_sum, current_sum - min_prefix)
20
21 # Update minimum subarray sum
22 # min_subarray_sum = min(current_sum - max_prefix_before_current)
23 min_subarray_sum = min(min_subarray_sum, current_sum - max_prefix)
24
25 # Update min and max prefix sums for next iteration
26 min_prefix = min(min_prefix, current_sum)
27 max_prefix = max(max_prefix, current_sum)
28
29 # Return the maximum of:
30 # 1. Maximum subarray sum (handles non-circular case)
31 # 2. Total sum - minimum subarray sum (handles circular case)
32 # The circular case works because removing the minimum subarray
33 # from the total gives us the maximum circular subarray
34 return max(max_subarray_sum, current_sum - min_subarray_sum)
35
1class Solution {
2 public int maxSubarraySumCircular(int[] nums) {
3 // Initialize constants and variables
4 final int INFINITY = 1 << 30; // Large value for initialization (2^30)
5
6 // Variables for tracking prefix sums and their extremes
7 int minPrefixSum = 0; // Minimum prefix sum seen so far
8 int maxPrefixSum = -INFINITY; // Maximum prefix sum seen so far
9
10 // Variables for tracking results
11 int maxSubarraySum = -INFINITY; // Maximum subarray sum (non-circular case)
12 int currentPrefixSum = 0; // Current running prefix sum
13 int minSubarraySum = INFINITY; // Minimum subarray sum
14
15 // Iterate through each element in the array
16 for (int num : nums) {
17 // Update current prefix sum
18 currentPrefixSum += num;
19
20 // Update maximum subarray sum (non-circular case)
21 // This is the maximum difference between current prefix sum and any previous minimum prefix sum
22 maxSubarraySum = Math.max(maxSubarraySum, currentPrefixSum - minPrefixSum);
23
24 // Update minimum subarray sum
25 // This is the minimum difference between current prefix sum and any previous maximum prefix sum
26 minSubarraySum = Math.min(minSubarraySum, currentPrefixSum - maxPrefixSum);
27
28 // Update the minimum and maximum prefix sums seen so far
29 minPrefixSum = Math.min(minPrefixSum, currentPrefixSum);
30 maxPrefixSum = Math.max(maxPrefixSum, currentPrefixSum);
31 }
32
33 // Return the maximum of:
34 // 1. Maximum subarray sum (non-circular case)
35 // 2. Total sum minus minimum subarray sum (circular case)
36 return Math.max(maxSubarraySum, currentPrefixSum - minSubarraySum);
37 }
38}
39
1class Solution {
2public:
3 int maxSubarraySumCircular(vector<int>& nums) {
4 // Initialize constants and variables
5 const int INF = 1 << 30; // Large value for initialization (2^30)
6
7 // Variables for tracking maximum subarray (Kadane's algorithm)
8 int minPrefixSum = 0; // Minimum prefix sum seen so far
9 int maxPrefixSum = -INF; // Maximum prefix sum seen so far
10
11 // Results
12 int maxSubarraySum = -INF; // Maximum subarray sum (non-wrapping case)
13 int minSubarraySum = INF; // Minimum subarray sum (for wrapping case)
14
15 // Current prefix sum
16 int currentPrefixSum = 0;
17
18 // Iterate through all elements
19 for (int num : nums) {
20 // Update current prefix sum
21 currentPrefixSum += num;
22
23 // Update maximum subarray sum (non-wrapping case)
24 // max_subarray = max(prefix[j] - prefix[i]) where i < j
25 maxSubarraySum = max(maxSubarraySum, currentPrefixSum - minPrefixSum);
26
27 // Update minimum subarray sum
28 // min_subarray = min(prefix[j] - prefix[i]) where i < j
29 minSubarraySum = min(minSubarraySum, currentPrefixSum - maxPrefixSum);
30
31 // Update minimum and maximum prefix sums for future iterations
32 minPrefixSum = min(minPrefixSum, currentPrefixSum);
33 maxPrefixSum = max(maxPrefixSum, currentPrefixSum);
34 }
35
36 // Return the maximum of:
37 // 1. Non-wrapping case: maxSubarraySum
38 // 2. Wrapping case: total sum - minimum subarray sum
39 return max(maxSubarraySum, currentPrefixSum - minSubarraySum);
40 }
41};
42
1function maxSubarraySumCircular(nums: number[]): number {
2 // Initialize prefix minimum and maximum values
3 let prefixMin: number = 0;
4 let prefixMax: number = -Infinity;
5
6 // Initialize result variables
7 let maxSubarraySum: number = -Infinity; // Maximum subarray sum (could be non-circular)
8 let currentSum: number = 0; // Running sum of all elements
9 let minSubarraySum: number = Infinity; // Minimum subarray sum (for circular case)
10
11 // Iterate through each element in the array
12 for (const num of nums) {
13 // Update the running sum
14 currentSum += num;
15
16 // Update maximum subarray sum using Kadane's algorithm
17 // (currentSum - prefixMin) gives the maximum sum ending at current position
18 maxSubarraySum = Math.max(maxSubarraySum, currentSum - prefixMin);
19
20 // Update minimum subarray sum
21 // (currentSum - prefixMax) gives the minimum sum ending at current position
22 minSubarraySum = Math.min(minSubarraySum, currentSum - prefixMax);
23
24 // Update prefix minimum and maximum for next iteration
25 prefixMin = Math.min(prefixMin, currentSum);
26 prefixMax = Math.max(prefixMax, currentSum);
27 }
28
29 // Return the maximum of:
30 // 1. maxSubarraySum: best non-circular subarray
31 // 2. currentSum - minSubarraySum: best circular subarray (total sum minus minimum subarray)
32 return Math.max(maxSubarraySum, currentSum - minSubarraySum);
33}
34
Time and Space Complexity
The time complexity is O(n)
, where n
is the length of the array nums
. This is because the algorithm iterates through the array exactly once with a single for loop, performing constant-time operations (O(1)
) for each element including addition, comparison, and variable updates.
The space complexity is O(1)
. The algorithm only uses a fixed number of variables (pmi
, pmx
, ans
, s
, smi
) to track the minimum prefix sum, maximum prefix sum, maximum subarray sum, current sum, and minimum subarray sum respectively. These variables do not scale with the input size, resulting in constant space usage.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Edge Case: All Negative Numbers
When all array elements are negative, the circular case calculation (total_sum - min_subarray_sum
) would select an empty subarray, giving a sum of 0. However, the problem requires a non-empty subarray.
Example: nums = [-3, -2, -3]
- Maximum non-circular sum:
-2
- Minimum subarray sum:
-8
(entire array) - Total sum:
-8
- Circular case:
-8 - (-8) = 0
(incorrect, as this represents an empty subarray)
Solution: Add a check to ensure we don't select the entire array as the minimum subarray:
# Check if minimum subarray is the entire array
if min_subarray_sum == current_sum:
# All elements are included in min subarray, so circular case is empty
return max_subarray_sum
else:
return max(max_subarray_sum, current_sum - min_subarray_sum)
2. Initialization of max_prefix
Setting max_prefix = -float('inf')
initially can cause issues because we're looking for the maximum prefix sum before the current element. Starting with negative infinity means the first element will always compute min_subarray_sum
incorrectly.
Solution: Initialize max_prefix = 0
instead, representing an empty prefix:
max_prefix = 0 # Start with empty prefix, not -infinity
3. Misunderstanding Variable Updates Order
Updating min_prefix
and max_prefix
before calculating the subarray sums would include the current element's contribution in both the prefix and the subarray calculation, leading to incorrect results.
Incorrect order:
min_prefix = min(min_prefix, current_sum) # Wrong: Updated too early
max_subarray_sum = max(max_subarray_sum, current_sum - min_prefix)
Correct order:
max_subarray_sum = max(max_subarray_sum, current_sum - min_prefix)
min_prefix = min(min_prefix, current_sum) # Update after using it
4. Single Element Array
With arrays of length 1, the circular and non-circular cases are identical, but the minimum subarray calculation might behave unexpectedly if max_prefix
starts at negative infinity.
Solution: Either handle single-element arrays separately or ensure proper initialization:
if len(nums) == 1:
return nums[0]
Which of the two traversal algorithms (BFS and DFS) can be used to find whether two nodes are connected?
Recommended Readings
Queue Intro Think of the last time you stood in line to buy movie tickets The first person in line gets their ticket first and any newcomers join the end of the line This system where the first person to arrive is the first to be served is a queue in real
https assets algo monster cover_photos dnd svg Divide and Conquer The main idea of divide and conquer is to split the main problem into two smaller components assuming that each one of the components had already been solved recursively and then try to solve the bigger problem using the solutions
What is Dynamic Programming Prerequisite DFS problems dfs_intro Backtracking problems backtracking Memoization problems memoization_intro Pruning problems backtracking_pruning Dynamic programming is an algorithmic optimization technique that breaks down a complicated problem into smaller overlapping sub problems in a recursive manner and uses solutions to the sub problems to construct a solution
Want a Structured Path to Master System Design Too? Don’t Miss This!