I first encountered the maximum-subarray problem framed around a stock trading scenario, and I think that framing is what makes it click for me: given a series of daily price changes, I want to find the contiguous stretch of days that gives me the best possible profit if I buy at the start and sell at the end of that stretch. It sounds like a small, specific puzzle, but it turns out to be a wonderful vehicle for teaching the divide-and-conquer paradigm, because the “combine” step requires a genuinely clever insight rather than a purely mechanical merge.
History and Background
The maximum-subarray problem, particularly the linear-time solution known as Kadane’s algorithm, is generally credited to Jay Kadane, who came up with the idea in 1984 in response to a talk by Ulf Grenander on a related problem in pattern analysis. The divide-and-conquer version of the solution, which I focus on in this file, is presented in Cormen, Leiserson, Rivest, and Stein’s Introduction to Algorithms as a pedagogical example — not because it’s the fastest known way to solve the problem, but because it’s an excellent showcase for how the divide-and-conquer paradigm handles cases where the “combine” step needs real cleverness, since the best subarray might cross the midpoint of the array rather than living entirely in one half.
Problem Statement
Given an array of numbers (which can include negative numbers), I want to find the contiguous subarray with the largest possible sum. Formally, given $A[1..n]$, I want indices $i$ and $j$ (with $i \leq j$) that maximize:
$$ \sum_{k=i}^{j} A[k] $$
Core Concepts
- Contiguous subarray: A subarray formed by a consecutive run of elements, not just any subset.
- Crossing subarray: A subarray that spans across the midpoint of the array when I split it in half — this is the tricky case that the divide-and-conquer combine step must handle.
- Divide-and-conquer decomposition: I split the array into left and right halves, solve each half recursively, and then handle the case where the best subarray straddles the midpoint.
- Prefix/suffix maximum sums: To handle the crossing case efficiently, I compute the best possible sum ending exactly at the midpoint (extending leftward) and the best possible sum starting exactly after the midpoint (extending rightward), then add them together.
How It Works
- If the array has only one element, that element itself is the maximum subarray (base case).
- Otherwise, I find the midpoint and split the array into a left half and a right half.
- I recursively find the maximum subarray entirely within the left half.
- I recursively find the maximum subarray entirely within the right half.
- I find the maximum subarray that crosses the midpoint, by scanning leftward from the midpoint to find the best prefix sum, and scanning rightward from the midpoint to find the best suffix sum, then adding them.
- I return whichever of the three candidates (left, right, or crossing) has the largest sum.
Working Principle
The key mechanism that makes this work is the observation that any subarray of the full array falls into exactly one of three categories relative to the midpoint: it lies entirely in the left half, entirely in the right half, or it crosses the midpoint. The first two cases are handled naturally by recursion. The third case can’t be handled recursively because it isn’t a subproblem of either half individually — instead, I compute it directly using a linear scan outward from the midpoint in both directions, since any crossing subarray must include the midpoint element (or the elements immediately adjacent to it), so its maximum value is found by greedily extending outward as far as it remains profitable.
Mathematical Foundation
For the crossing case, I define:
$$ \text{LeftSum} = \max_{i \leq mid} \sum_{k=i}^{mid} A[k], \qquad \text{RightSum} = \max_{j > mid} \sum_{k=mid+1}^{j} A[k] $$
The maximum crossing subarray sum is:
$$ \text{CrossSum} = \text{LeftSum} + \text{RightSum} $$
The overall maximum subarray sum is:
$$ \text{MaxSubarray}(A, low, high) = \max\big(\text{MaxSubarray}(A, low, mid),\ \text{MaxSubarray}(A, mid+1, high),\ \text{CrossSum}\big) $$
The recurrence describing the running time is:
$$ T(n) = 2T\left(\frac{n}{2}\right) + \Theta(n) $$
This is because the two recursive calls each handle half the array, and the crossing-subarray computation takes $\Theta(n)$ time (a linear scan in each direction from the midpoint). Applying the Master Method with $a=2$, $b=2$, $f(n) = \Theta(n)$: since $n^{\log_2 2} = n^1$ matches $f(n) = n$, this is Case 2, giving:
$$ T(n) = \Theta(n \log n) $$
Diagrams
flowchart TD
A["Array A[low..high]"] --> B{"low == high?"}
B -- Yes --> C["Return single element as max subarray"]
B -- No --> D["mid = (low+high)/2"]
D --> E["Recursively find max subarray in A[low..mid]"]
D --> F["Recursively find max subarray in A[mid+1..high]"]
D --> G["Find max crossing subarray through mid"]
E --> H["Return best of the three candidates"]
F --> H
G --> H
Pseudocode
FIND-MAX-CROSSING-SUBARRAY(A, low, mid, high):
leftSum = -infinity
sum = 0
for i = mid downto low:
sum = sum + A[i]
if sum > leftSum:
leftSum = sum
maxLeft = i
rightSum = -infinity
sum = 0
for j = mid+1 to high:
sum = sum + A[j]
if sum > rightSum:
rightSum = sum
maxRight = j
return (maxLeft, maxRight, leftSum + rightSum)
FIND-MAXIMUM-SUBARRAY(A, low, high):
if high == low:
return (low, high, A[low]) // base case: single element
mid = floor((low + high) / 2)
(leftLow, leftHigh, leftSum) = FIND-MAXIMUM-SUBARRAY(A, low, mid)
(rightLow, rightHigh, rightSum) = FIND-MAXIMUM-SUBARRAY(A, mid+1, high)
(crossLow, crossHigh, crossSum) = FIND-MAX-CROSSING-SUBARRAY(A, low, mid, high)
if leftSum >= rightSum and leftSum >= crossSum:
return (leftLow, leftHigh, leftSum)
elif rightSum >= leftSum and rightSum >= crossSum:
return (rightLow, rightHigh, rightSum)
else:
return (crossLow, crossHigh, crossSum)
Step-by-Step Example
Let me trace through the array A = [-2, 1, -3, 4, -1, 2, 1, -5, 4] (indices 0 to 8).
- Splitting the array recursively, I eventually reach the base cases (single elements).
- Working back up, on the right side, I find
[4, -1, 2, 1]gives sums that build up to a strong subarray. - The crossing computations at various midpoints check whether extending across the split boundary beats staying within one half.
- Ultimately, the algorithm identifies
[4, -1, 2, 1](indices 3 to 6) as the maximum subarray, with sum $4 + (-1) + 2 + 1 = 6$.
I can verify this is indeed the best: no other contiguous run in this array sums higher than 6 — for example, [4] alone gives 4, [-2,1,-3,4,-1,2,1] gives 2, and [4,-1,2,1,-5,4] gives 5, all less than 6.
Time Complexity
- Best, average, and worst case: All are $\Theta(n \log n)$, since the algorithm’s structure (splitting into halves, doing linear work at each level to check crossings) is fixed regardless of the actual values in the array.
- This is worse than Kadane’s algorithm, which solves the same problem in $\Theta(n)$ time using a single linear scan, but the divide-and-conquer version remains valuable for illustrating the paradigm.
Space Complexity
The recursive calls create a call stack of depth $O(\log n)$, and since each call only needs a constant amount of extra space beyond its recursive calls (aside from the input array itself), the auxiliary space complexity is $O(\log n)$.
Correctness Analysis
Correctness follows from the exhaustive case analysis: since every subarray of $A[low..high]$ is either entirely within the left half, entirely within the right half, or crosses the midpoint, and the algorithm correctly computes the best subarray for each of these three cases, taking the maximum of the three guarantees I’ve found the true overall maximum. The crossing case is proven correct because any subarray crossing the midpoint can be decomposed uniquely into a suffix of the left half plus a prefix of the right half, and maximizing each side independently (both anchored at the midpoint) and adding them together does correctly maximize their sum, since the two parts don’t interact with each other once fixed at the midpoint boundary.
Advantages
- A strong pedagogical example of divide-and-conquer where the combine step requires genuine algorithmic insight, not just simple merging.
- Generalizes well to other “best contiguous region” problems, such as 2D versions used in image processing (finding the brightest rectangular subregion).
- The technique of computing prefix/suffix extremes at a boundary is broadly reusable in other algorithms.
Disadvantages
- Asymptotically slower than Kadane’s linear-time algorithm, making it less practical for real-world use when a faster alternative exists.
- More complex to implement correctly than Kadane’s algorithm, given the extra bookkeeping for tracking indices across three cases.
- Higher constant factors and additional space usage due to recursion, compared to Kadane’s simple iterative approach.
Applications
- Financial analysis, such as identifying the best window of time to buy and sell a stock based on historical daily price changes.
- Signal processing, for identifying the most significant contiguous segment of a signal.
- Bioinformatics, in problems related to finding regions of interest in genomic sequences.
- Teaching contexts, as a stepping stone toward understanding more advanced divide-and-conquer algorithms with non-trivial combine steps.
Implementation in C
#include <stdio.h>
#include <limits.h>
/* Finds the maximum crossing subarray sum through the midpoint.
Updates crossLow and crossHigh to the boundary indices found. */
int maxCrossingSubarray(int A[], int low, int mid, int high, int *crossLow, int *crossHigh) {
int leftSum = INT_MIN, sum = 0;
int maxLeft = mid;
for (int i = mid; i >= low; i--) {
sum += A[i];
if (sum > leftSum) {
leftSum = sum;
maxLeft = i;
}
}
int rightSum = INT_MIN;
sum = 0;
int maxRight = mid + 1;
for (int j = mid + 1; j <= high; j++) {
sum += A[j];
if (sum > rightSum) {
rightSum = sum;
maxRight = j;
}
}
*crossLow = maxLeft;
*crossHigh = maxRight;
return leftSum + rightSum;
}
/* Recursively finds the maximum subarray sum in A[low..high].
Stores the resulting boundary indices in *resultLow and *resultHigh. */
int maxSubarray(int A[], int low, int high, int *resultLow, int *resultHigh) {
if (low == high) {
*resultLow = low;
*resultHigh = high;
return A[low];
}
int mid = (low + high) / 2;
int leftLow, leftHigh, rightLow, rightHigh, crossLow, crossHigh;
int leftSum = maxSubarray(A, low, mid, &leftLow, &leftHigh);
int rightSum = maxSubarray(A, mid + 1, high, &rightLow, &rightHigh);
int crossSum = maxCrossingSubarray(A, low, mid, high, &crossLow, &crossHigh);
if (leftSum >= rightSum && leftSum >= crossSum) {
*resultLow = leftLow;
*resultHigh = leftHigh;
return leftSum;
} else if (rightSum >= leftSum && rightSum >= crossSum) {
*resultLow = rightLow;
*resultHigh = rightHigh;
return rightSum;
} else {
*resultLow = crossLow;
*resultHigh = crossHigh;
return crossSum;
}
}
int main() {
int A[] = {-2, 1, -3, 4, -1, 2, 1, -5, 4};
int n = sizeof(A) / sizeof(A[0]);
int resultLow, resultHigh;
int maxSum = maxSubarray(A, 0, n - 1, &resultLow, &resultHigh);
printf("Maximum subarray sum: %d\n", maxSum);
printf("Subarray indices: [%d, %d]\n", resultLow, resultHigh);
printf("Subarray: ");
for (int i = resultLow; i <= resultHigh; i++)
printf("%d ", A[i]);
printf("\n");
return 0;
}
Sample Input and Output
For the array [-2, 1, -3, 4, -1, 2, 1, -5, 4], the program outputs:
Maximum subarray sum: 6
Subarray indices: [3, 6]
Subarray: 4 -1 2 1
Optimization Techniques
- Switch to Kadane’s algorithm: Since Kadane’s algorithm solves the same problem in $\Theta(n)$ time with $O(1)$ extra space, it’s almost always the better choice in production code; the divide-and-conquer version is mostly of educational value.
- Early termination in crossing scan: In practice, if I know the values are all non-negative, the maximum subarray is just the whole array, so I can skip the algorithm entirely with a quick check.
- Iterative conversion: Converting the recursive divide-and-conquer solution to an iterative one using an explicit stack can reduce function-call overhead in performance-sensitive contexts.
Common Mistakes
- Forgetting to correctly track the indices of the maximum subarray, not just its sum, which is important if I need to know exactly which elements to report.
- Off-by-one errors when computing the midpoint or the boundaries of the crossing scan (mid vs mid+1).
- Assuming the maximum subarray is always non-empty and forgetting to handle the case where all elements are negative — in that case, the maximum subarray is just the single largest (least negative) element.
- Using
intoverflow-prone variables for very large arrays with large values — in production code, I’d want to use a wider integer type for the running sums.
Further Reading
- Cormen, Leiserson, Rivest, Stein, Introduction to Algorithms, Chapter 4: https://mitpress.mit.edu/9780262046305/introduction-to-algorithms/
- Bentley, J., “Programming Pearls: Algorithm Design Techniques,” Communications of the ACM, 1984 (origin of Kadane’s algorithm discussion): https://dl.acm.org/doi/10.1145/358.315
- Wikipedia, “Maximum subarray problem”: https://en.wikipedia.org/wiki/Maximum_subarray_problem
- GeeksforGeeks, “Maximum Subarray Sum using Divide and Conquer algorithm”: https://www.geeksforgeeks.org/maximum-subarray-sum-using-divide-and-conquer-algorithm/
