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])$$
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:
- 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)$.
- 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
Associative Monoid Promotion for Parallel / Segment-Tree Kadane
(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
cmov Execution & Parallel Monoid ReductionInside 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!
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.
Invariant & State
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);
}
global_max = 0 & Maximum Product Subarray Sign FlipsTwo classic bugs arise in Kadane variants:
- Initializing
global_max = 0: When the problem requires a non-empty subarray and all elements are negative (e.g.,[-8, -3, -5]), initializingglobal_max = 0returns0instead of-3. Always initializecur_max = global_max = A[0]and loop from indexi = 1. - 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_maxandcur_minat every step: when $A[i] < 0$,std::swap(cur_max, cur_min)before multiplying.