DSA DSA In Depth
0%
lesson kadanesubarrayinvariants

Maximum Subarray (Kadane's Algorithm) & Circular Array Extensions

7 min read Module 1.2: Contiguous Arrays, Bit Manipulation & Pointer Idioms Updated Oct 23, 2026
📄 Cheat Sheet
Maximum Subarray Problem & Kadane's Dynamic Programming Invariant
CS Dojo / MIT 6.006 Maximum Subarray Problem & Kadane's Dynamic Programming Invariant Watch on YouTube ↗
0:00 / --:--

From 2D Image Estimation to $O(N)$ 1D Dynamic Programming

Originally posed by Ulf Grenander in 1977 for maximum-likelihood pattern detection in 2D digitized images, the Maximum Contiguous Subarray problem originally took $O(N^3)$ brute force, $O(N^2)$ via prefix sums, and $O(N \log N)$ via divide-and-conquer before Jay Kadane (CMU) discovered the $O(N)$ invariant in 1984. Notice the structural connection to prefix sums:

$$\max_{0 \le L \le R < N} \sum_{k=L}^{R} A[k] = \max_{0 \le R < N} \left( P[R+1] - \min_{0 \le L \le R} P[L] \right)$$

Maintaining the running minimum prefix sum $\min_{0 \le L \le R} P[L]$ as $R$ scans left-to-right is mathematically identical to Kadane's DP recurrence: let $E[i]$ be the maximum sum of a non-empty contiguous subarray ending at index $i$. Whenever $E[i-1] < 0$, prepending the best suffix of $A[0 \dots i-1]$ to $A[i]$ strictly reduces $A[i]$'s value, so we discard the negative prefix and start a fresh subarray at $i$:

$$E[i] = \max(A[i],\; E[i-1] + A[i]), \qquad G[i] = \max(G[i-1],\; E[i])$$

Duality Theorem: Maximum Circular Subarray = $\max(\text{Kadane}_{\max},\; \text{TotalSum} - \text{Kadane}_{\min})$

In a circular array (ring buffer) where index $N-1$ wraps around to index $0$, any valid non-empty contiguous circular subarray falls into one of two mutually exclusive topological cases:

  1. Non-Wrapping Interval $A[L \dots R]$: The optimal subarray does not cross the $N-1 \to 0$ boundary. Its sum is found by standard $\text{Kadane}_{\max}(A)$.
  2. Wrapping Interval $A[0 \dots R] \cup A[L \dots N-1]$ (with $R + 1 < L$): The chosen elements wrap around the boundary, which means the excluded middle elements $A[R+1 \dots L-1]$ form a contiguous non-wrapping subarray! Maximizing the wrapping sum is equivalent to minimizing the excluded middle subarray via $\text{Kadane}_{\min}(A)$: $$\text{MaxWrapSum} = \sum_{i=0}^{N-1} A[i] - \min_{0 \le L \le R < N} \sum_{k=L}^{R} A[k]$$

Critical Edge Case: If all elements in $A$ are negative ($A[i] < 0$), then $\text{Kadane}_{\min}(A) = \text{TotalSum}$, so $\text{TotalSum} - \text{Kadane}_{\min}(A) = 0$, corresponding to an empty wrapping subarray. Therefore, if $\text{Kadane}_{\max}(A) < 0$ (or equivalently $\text{TotalSum} == \text{Kadane}_{\min}$), return $\text{Kadane}_{\max}(A)$ directly!

Comparative Algorithmic Approaches to Maximum Subarray

Algorithm / Formulation Time Complexity Auxiliary Space Parallelizable / Associative? Primary Use Case
Kadane's Local DP ($E[i], G[i]$) $O(N)$ $O(1)$ Serial loop-carried dependency Sequential 1D & Circular arrays
Prefix Sum Running Min ($P[R+1] - \min P[L]$) $O(N)$ $O(1)$ Reducible via parallel prefix scan Length-constrained subarrays (with Deque)
Divide & Conquer / Segment Tree Node $O(N)$ build, $O(\log N)$ update $O(N)$ tree Yes! Monoid tuple $(sum, pref, suff, best)$ Dynamic point updates + range max-subarray queries
2D Kadane (Row-Pair Compression) $O(R^2 \cdot C)$ $O(C)$ Outer row pairs embarrassingly parallel Maximum sum submatrix in 2D grids

Visualizing Circular Subarray Complement Duality

Case 1: Non-Wrapping Max Subarray (Standard Kadane_max) Prefix Max Contiguous Subarray (Kadane_max) Suffix Case 2: Wrapping Max Subarray = TotalSum - Minimum Contiguous Middle Subarray (Kadane_min) Wrapped Head [0 .. R] Excluded Min Subarray (Kadane_min) Wrapped Tail [L .. N-1] Guard: If all A[i] < 0, TotalSum == Kadane_min (empty wrap); return Kadane_max directly!

Associative Monoid Promotion for Parallel / Segment-Tree Kadane

flowchart LR L["Left Half Tuple
(sum_L, pref_L, suff_L, best_L)"] --> M["Combine Operator (O(1))
sum = sum_L + sum_R
pref = max(pref_L, sum_L + pref_R)
suff = max(suff_R, suff_L + sum_R)
best = max({best_L, best_R, suff_L + pref_R})"] R["Right Half Tuple
(sum_R, pref_R, suff_R, best_R)"] --> M
Hardware Reality: Branchless cmov Execution & Parallel Monoid Reduction

Inside Kadane's inner loop, cur_max = std::max(x, cur_max + x) and global_max = std::max(global_max, cur_max) compile on x86-64 to two additions and two conditional move (cmovg) instructions with zero branch instructions—avoiding branch predictor pollution regardless of sign oscillations. However, because cur_max depends on the previous iteration's cur_max, a single scalar Kadane loop is latency-bound by the 2-cycle integer add+cmov dependency chain. To saturate 8-wide AVX2 SIMD lanes or multi-core threads, divide the array into $P$ chunks and compute the 4-tuple monoid $(\text{sum}, \text{max\_prefix}, \text{max\_suffix}, \text{max\_subarray})$ on each chunk independently in parallel!

2D Prefix Inclusion-Exclusion, Difference Array & Kadane Subarray
O(1) Range Query / O(N) Kadane

Trace 2D prefix-sum inclusion-exclusion P[r2][c2] - P[r1-1][c2] - P[r2][c1-1] + P[r1-1][c1-1], O(1) difference-array range updates, and Kadane circular max subarray.

Step 1 / 7
Custom Input:
Timeline

Invariant & State

Kadane Circular Duality: A wrapping subarray equals the complement of a contiguous middle subarray, so max_wrap = TotalSum - minSubarray.
📊 Live Magnitude & Pointer Stage — 2D Prefix Inclusion-Exclusion, Difference Array & Kadane Subarray Active Pivot Match/Sorted
Prefix 5 [0] Min Sub -3 [1] Suffix 5 [2] Total(7 Circul… [3]
[0]5Prefix
[1]-3Min Subarray = -3
[2]5Suffix
[3]Circular Wrap: [5] + [5] = 10Total(7) - Min(-3) = 10
Standard Kadane Max
7 ([5, -3, 5])
Minimum Subarray
-3 ([-3])
Circular Max (Total - Min)
7 - (-3) = 10
[Step 1] Computed both maxSubarray=7 and minSubarray=-3; circular wrap achieves Total - Min = 10.

Production Implementation: Circular Kadane & Index-Tracking Non-Wrapping Kadane

#include <algorithm>
#include <cstdint>
#include <vector>

struct SubarrayResult {
  std::int64_t max_sum;
  std::size_t best_left;
  std::size_t best_right;
};

// Returns maximum non-empty subarray sum along with exact [best_left, best_right] indices
SubarrayResult kadane_with_indices(const std::vector<int>& nums) noexcept {
  std::int64_t cur_sum = nums[0];
  std::int64_t max_sum = nums[0];
  std::size_t cur_left = 0, best_left = 0, best_right = 0;

  for (std::size_t i = 1; i < nums.size(); ++i) {
    if (cur_sum < 0) {
      cur_sum = nums[i];
      cur_left = i;
    } else {
      cur_sum += nums[i];
    }
    if (cur_sum > max_sum) {
      max_sum = cur_sum;
      best_left = cur_left;
      best_right = i;
    }
  }
  return {max_sum, best_left, best_right};
}

// Single-pass O(N) Time, O(1) Space Maximum Circular Subarray
std::int64_t max_subarray_circular(const std::vector<int>& nums) noexcept {
  std::int64_t total_sum = nums[0];
  std::int64_t cur_max = nums[0], global_max = nums[0];
  std::int64_t cur_min = nums[0], global_min = nums[0];

  for (std::size_t i = 1; i < nums.size(); ++i) {
    const std::int64_t x = nums[i];
    total_sum += x;
    cur_max = std::max(x, cur_max + x);
    global_max = std::max(global_max, cur_max);
    cur_min = std::min(x, cur_min + x);
    global_min = std::min(global_min, cur_min);
  }

  // If all numbers are negative, global_max < 0 and total_sum == global_min.
  // Returning total_sum - global_min would erroneously return 0 (the empty subarray).
  if (global_max < 0) {
    return global_max;
  }
  return std::max(global_max, total_sum - global_min);
}
Production Pitfall: Initializing global_max = 0 & Maximum Product Subarray Sign Flips

Two classic bugs arise in Kadane variants:

  1. Initializing global_max = 0: When the problem requires a non-empty subarray and all elements are negative (e.g., [-8, -3, -5]), initializing global_max = 0 returns 0 instead of -3. Always initialize cur_max = global_max = A[0] and loop from index i = 1.
  2. Maximum Product Subarray: Multiplication is not monotonic under sign flips—multiplying a very negative running product by a negative number $A[i] < 0$ turns it into the largest positive product! For Maximum Product Subarray, you must simultaneously track both cur_max and cur_min at every step: when $A[i] < 0$, std::swap(cur_max, cur_min) before multiplying.
Check yourself Question 1 of 4
Concept Check All-Negative Edge Case in Maximum Circular Subarray recall

To find the maximum non-empty subarray sum in a circular array $A$, you compute `total_sum`, `global_max` (standard Kadane max), and `global_min` (standard Kadane min), and return `max(global_max, total_sum - global_min)`.

⚡ Interactive Challenge · Staff Level

Algorithmic Design & Complexity Check

Analyze the invariant, asymptotic bound, and hardware behavior before selecting.

Why does standard scalar Kadane (`cur = max(x, cur + x)`) fail when a maximum length $K$ is imposed, and how is $O(N)$ time achieved?

Key Takeaways

Kadane's algorithm reduces the O(N^2) subarray search to O(N) by maintaining E[i] = max(A[i], E[i-1] + A[i]), the best subarray constrained to end at index i.
In a circular array, the maximum subarray is max(Kadane_max, TotalSum - Kadane_min), guarded by returning Kadane_max when all elements are negative.
Although standard Kadane has a serial dependency, promoting each segment to the 4-tuple (sum, max_pref, max_suff, max_sub) makes the operator associative for Segment Trees and SIMD/multi-core parallelism.
Always initialize Kadane's accumulators to A[0] (not 0) when non-empty subarrays are required, and track both min and max states for Maximum Product Subarray.
Was this lesson helpful? Spot an issue or have a suggestion? Let us know on GitHub.
Report an issue