Maximum Subarray
Introduction
A sequence of integers — some positive, some negative — and a single objective: find the contiguous run with the largest sum. The puzzle is famous, asked in countless on-sites, and the obvious solutions are quadratic. The interesting ones are not, and the gap between “obvious” and “optimal” is the entire reason this problem keeps appearing.
The lesson is bigger than the algorithm. Most interview optimizations make a slow approach faster. The optimization here works the other way: you don’t speed up the brute force, you ask a different question whose answer happens to be linear. That move — re-stating the question so the answer falls out — is the heart of dynamic programming, and recognizing when it applies is what this chapter trains.
In dynamic programming, the optimization isn’t faster code — it’s a sharper question.
If you’re early in dynamic-programming prep, read the chapter linearly — each approach builds on the previous. If you can already write the optimal answer in your sleep, jump to the correctness section under Approach 03 and the follow-ups table; both contain something even seasoned candidates miss.
What the interviewer is actually testing
- Naming the obvious solution and not stopping there. A brute-force answer is correct but uninteresting. The signal is whether you spot that there’s a better way without being prompted.
- Reasoning about subproblem structure. The breakthrough comes from re-framing the question in terms of “best subarray ending here” rather than “best subarray overall.” That re-framing is the DP move; it shows up in countless other problems.
- Constant-space discipline. The linear-time answer fits in extra memory once you notice that yesterday’s subproblem solution is the only one tomorrow needs. Spotting that collapse is a senior signal.
- Edge cases and variations. All-negative inputs, the minimum-sum variant, the longest-positive-run variant — each is a one-line tweak. Knowing them shows you understand the move, not just memorized the code.
Problem statement
Given an array of integers (positive, negative, or zero), find the contiguous subarray with at least one element whose sum is the largest possible. Return that sum.
Formally: find indices that maximize .
A = [-2, 1, -3, 4, -1, 2, 1, -5, 4] 6 A = [-3, -1, -4, -2] -1 A = [1, 2, 3] 6 Clarifying questions — and why each one matters
Clarification questions
- Q Can the array contain negative numbers?
- A Yes — the whole problem becomes trivial otherwise. If everything is non-negative, the answer is just the sum of the entire array; if everything is non-positive, the answer is the largest single element. Confirm the spec.
- Q Can the subarray be empty?
- A Standard formulation: no — the subarray must contain at least one element. If the spec allows an empty subarray, the answer for an all-negative input becomes 0, which changes a single line of code. Always ask.
- Q Do we return the sum, or the indices, or the subarray itself?
- A Default is just the sum. If the interviewer asks for the indices, track the start and end of the running subarray plus the start and end of the best-so-far. No asymptotic change — just two extra variables.
- Q Are duplicates allowed?
- A Yes, with no special handling. Duplicates don't affect the algorithm; the running sum simply incorporates them like any other value.
- Q What's the upper bound on n, and on the range of A[i]?
- A Typical: n ≤ 10⁵, |A[i]| ≤ 10⁴. That's well within an O(n) budget. If n is much larger, the linear sweep still wins; only the brute force struggles. If values are large, watch for overflow — use 64-bit accumulators if asked.
- Q Do we need to handle a circular array (wrap-around)?
- A That's a separate problem (LC 918). The wrap-around subarray case requires a second pass with a sign-flipped array — see follow-ups.
A mental framework — re-frame the question
The brute-force question is “what’s the largest sum across all contiguous subarrays?”. There are subarrays, so any approach that enumerates them is at least quadratic.
The DP move is to re-frame: “for each ending position , what’s the largest sum of any subarray that ends exactly at ?”. There are only ending positions, and — crucially — the answer for ending position has a cheap relationship to the answer for ending position :
Either we extend the previous run by adding , or the previous run wasn’t worth keeping and we start a new one at . The decision at each cell is binary, and the global answer is just the maximum over all the per-position values. That’s the entire algorithm.
This re-framing — replacing a “best across all ” question with a “best ending at each ” recurrence — is one of the most-reused moves in dynamic programming. It works whenever extending a partial solution is a constant-time operation given the previous value.
Discussion
Brute force — every subarray, prefix-sum accelerated
Enumerate every contiguous subarray, sum each via prefix sums, track the maximum. Quadratic, correct, your starting line.
The interview narration
Say this out loud:
“The straightforward solution enumerates every pair with and sums . There are such pairs, and a prefix-sum array makes each sum — so this runs in time, space. I’ll write it as a baseline and then look for something faster.”
How it works, step by step
A naive triple loop computes each subarray sum from scratch in — that gives overall, which is wasteful since the same prefixes are recomputed over and over. Instead:
- Build a prefix-sum array
prefixwhereprefix[i] = A[0] + A[1] + ... + A[i]. One linear pass; time and space. - Enumerate every starting index
iand every ending indexj ≥ i. For each pair, computesum = prefix[j] − (i > 0 ? prefix[i - 1] : 0). That’s the sum ofA[i..j]in . - Track the maximum sum seen across all pairs.
The loop visits exactly pairs, doing arithmetic at each. Total: time, extra space.
The code
#include <climits>#include <vector>
int maxSubArray(const std::vector<int>& A) { const int n = static_cast<int>(A.size()); if (n == 0) return 0;
std::vector<int> prefix(n); prefix[0] = A[0]; for (int i = 1; i < n; ++i) { prefix[i] = prefix[i - 1] + A[i]; }
int best = INT_MIN; for (int i = 0; i < n; ++i) { for (int j = i; j < n; ++j) { const int sum = prefix[j] - (i > 0 ? prefix[i - 1] : 0); if (sum > best) best = sum; } } return best;}Prefix sums make each subarray sum constant-time, so the double loop is honest O(n²). The starting `INT_MIN` ensures the first comparison wins for any input, including all-negative arrays.
Why it’s rejected
For , this is operations — about a minute of wall-clock time. Production code would never ship this, and no interviewer accepts it as the final answer. But it establishes the problem shape and serves as a reference implementation to verify the optimized versions against.
What it gets right
- Correct on every sign mix. Initializing
besttoINT_MINand comparing actual sums (never absolute values) means all-negative, all-positive, and mixed inputs all return the right answer with no special-casing. - The prefix-sum technique itself is reusable. It’s the right move for any range-sum-related problem; coding it once cleanly pays off in Subarray Sum Equals K, Range Sum Query, and the rest of the prefix-sum family.
Divide and conquer — the cross-boundary trick
Recursively find the best subarray in each half, then handle the case that spans the midpoint with a linear scan. T(n) = 2T(n/2) + O(n), so O(n log n).
The insight
Split the array at the midpoint. The best contiguous subarray of the whole array must be one of three things:
- Entirely within the left half — a recursive subproblem.
- Entirely within the right half — a recursive subproblem.
- Crossing the midpoint — the only case that actually needs new work.
The cross-boundary case is the one that makes this approach interesting. To compute it, scan from the midpoint leftward, accumulating the running sum and recording the maximum (this gives the best subarray ending exactly at the midpoint). Then scan from mid + 1 rightward with the same trick (best subarray starting at mid + 1). Sum the two. Linear time per level.
The recurrence is the classic “balanced split with linear combine” pattern: .
How it works, step by step
For a subrange [lo, hi]:
- Base case
lo == hi: returnA[lo]itself (a single element is its own best subarray). - Pick the midpoint
mid = lo + (hi - lo) / 2. The(hi - lo) / 2form avoids overflow whenlo + hiwould exceedINT_MAXfor huge ranges. - Recurse left:
leftBest = solve(lo, mid). - Recurse right:
rightBest = solve(mid + 1, hi). - Cross-boundary scan:
- Walk from
middown tolo, accumulating the running sum, tracking its maximum. Call thatleftSum. - Walk from
mid + 1up tohi, accumulating, tracking the maximum. Call thatrightSum. - The best cross-boundary subarray sums to
leftSum + rightSum.
- Walk from
- Combine: return
max(leftBest, rightBest, crossBest).
The leftSum and rightSum scans must each include the midpoint cell on their side (so they capture subarrays that genuinely span the boundary). Initializing them to INT_MIN and walking inward solves both edge cases (a single negative cell on one side) without special-casing.
The code
#include <algorithm>#include <climits>#include <vector>
namespace {
int crossSum(const std::vector<int>& A, int lo, int mid, int hi) { int leftBest = INT_MIN; int sum = 0; for (int i = mid; i >= lo; --i) { sum += A[i]; if (sum > leftBest) leftBest = sum; }
int rightBest = INT_MIN; sum = 0; for (int i = mid + 1; i <= hi; ++i) { sum += A[i]; if (sum > rightBest) rightBest = sum; }
return leftBest + rightBest;}
int solve(const std::vector<int>& A, int lo, int hi) { if (lo == hi) return A[lo]; const int mid = lo + (hi - lo) / 2; const int leftBest = solve(A, lo, mid); const int rightBest = solve(A, mid + 1, hi); const int crossBest = crossSum(A, lo, mid, hi); return std::max({leftBest, rightBest, crossBest});}
} // namespace
int maxSubArray(const std::vector<int>& A) { if (A.empty()) return 0; return solve(A, 0, static_cast<int>(A.size()) - 1);}Two recursive calls plus a linear cross-boundary scan. The std::max({a, b, c}) initializer-list overload handles the three-way comparison cleanly.
When divide-and-conquer beats Kadane
Almost never, for this specific problem. Kadane’s wins on every input, in both time and space. The reason to know divide-and-conquer is that the cross-boundary trick generalizes: the same template solves the closest-pair-of-points problem, the maximum-product subarray problem, and several others where Kadane’s “extend or restart” framing doesn’t apply directly.
Complexity
- Time: . The recurrence resolves to this by the master theorem.
- Space: for the recursion stack. No auxiliary data structures.
Kadane's algorithm — one variable, one decision per cell Recommended
Walk the array once. At each cell, decide whether to extend the running subarray or restart from the current cell. Track the maximum encountered.
The insight, stated cleanly
Instead of asking “what’s the best subarray of the whole array?”, ask the question per-position: “what’s the best subarray that ends exactly at index ?”. There are only such positions, and the answer for one has a one-line relationship to the answer for the previous:
- The best subarray ending at is either just (start fresh) or added to the best subarray ending at (extend the previous run).
- We pick whichever of the two is larger.
The global answer is the maximum across all positions. Walk the array once, update a running endingHere, update a running best. Two integers, one pass.
Initial: endingHere = best = A[0] = -2.
Three transitions worth naming
The widget above runs the full sweep. Three moments are particularly instructive on the canonical input [-2, 1, -3, 4, -1, 2, 1, -5, 4]. Pause on each — together they show every behavior the algorithm exhibits.
The first restart. At step 3 the running sum is (it’s been struggling through the negative early values). When arrives, the algorithm faces a choice: extend , or restart at alone. Restart wins. The window collapses to a single cell; from here on, the running run starts fresh.
The peak. By step 6 the algorithm has extended through [4, -1, 2, 1] and endingHere reaches 6 — the largest value seen so far. best updates accordingly. This is the moment the answer for the whole input is determined; everything after this either ties or fails to beat it.
Extend vs. restart, the close call. At step 7 a arrives. Extend would give ; restart would give alone. Extend still wins, but only barely. The running sum drops sharply but stays positive — the algorithm is ready to capitalize if a positive value follows. (It does, at step 8, but not enough to dethrone the existing best.)
The code
#include <algorithm>#include <vector>
int maxSubArray(const std::vector<int>& A) { if (A.empty()) return 0; int best = A[0]; int endingHere = A[0]; for (std::size_t i = 1; i < A.size(); ++i) { endingHere = std::max(A[i], endingHere + A[i]); best = std::max(best, endingHere); } return best;}Two variables, one pass. The std::max calls are the entire decision logic; everything else is bookkeeping.
A few details worth naming aloud:
endingHere = std::max(A[i], endingHere + A[i])is the recurrence in one line. The choice between restart and extend hides insidestd::max.best = std::max(best, endingHere)must run afterendingHereis updated — otherwise the current cell’s contribution is missed.- Initialization to
A[0](not 0) is what makes all-negative inputs work. If we initializedbestto 0, an all-negative input would return 0 — which is wrong for the standard formulation requiring a non-empty subarray.
Correctness, precisely
Invariant. After processing index :
endingHereequals the largest sum of any contiguous subarray that ends at index .bestequals the largest sum of any contiguous subarray that ends at any index in .
Proof sketch. Induction on .
Base: . The only subarray ending at index 0 is itself; its sum is . Both endingHere and best are initialized to . ✓
Inductive step: Assume the invariant holds at index . At index , any subarray ending at either starts at (sum = ) or starts before (sum = some subarray ending at ). The latter is maximized when we extend the best subarray ending at — which is endingHere. So is the new endingHere. The new best is , which equals the largest sum across subarrays ending at any index in . ✓
Therefore best at the end of the loop is the largest sum of any contiguous subarray of .
Edge cases
- All positive: every step extends;
endingHeregrows monotonically;bestequals the full sum at the end. - All negative: every step restarts;
endingHereequalsA[i]at each step;besttracks the largest single element. ✓ - All zero:
endingHereandbestboth stay at 0 throughout. ✓ - Single element: the loop body never runs;
best = A[0]. ✓ - Mixed with one massive positive surrounded by negatives: the positive dominates;
bestrecords it on its single cell.
Complexity
- Time: . One pass, two
std::maxcalls per cell. - Space: . Two integers, no auxiliary storage.
Approach comparison
| Approach | Time | Space | When to reach for it |
|---|---|---|---|
| Brute force (prefix-sum) | Baseline in narration; never in production. | ||
| Divide-and-conquer | When the interviewer wants D&C; teaches the cross-boundary trick. | ||
| Kadane’s algorithm | Default. What you ship. |
Default answer: Kadane. State the recurrence — “best ending here is either A[i] alone or A[i] plus best ending at i−1” — and the algorithm writes itself.
Follow-ups to anticipate
| The interviewer asks… | What to do |
|---|---|
| ”Return the indices of the optimal subarray, not just the sum.” | Track two more variables: windowLo (start of the current running subarray) and bestLo/bestHi. Update windowLo to i on a restart, and on every best update, copy windowLo and i into bestLo/bestHi. Same . |
| ”Find the minimum sum subarray instead.” | Flip every max to min and initialize accordingly. The mechanics don’t change. |
| ”Find the longest contiguous subarray where every element is positive.” | Different question, same template. length = A[i] > 0 ? length + 1 : 0; track the maximum length seen. |
| ”Circular array — the subarray can wrap around the end.” | LC 918. Two passes: standard Kadane gives the best non-wrapping answer; then run Kadane on -A[i] to find the minimum subarray, and total - min is the best wrapping answer. Take the max of the two (with a guard for all-negative inputs). |
| ”Maximum product subarray.” | LC 152. Track both maxEnding and minEnding at every step. A negative number multiplied by the current minEnding can produce the next maxEnding — the worst running product becomes the best when a negative arrives. The Kadane recurrence becomes a swap-and-update of two values. |
| ”Subarray sum equals K.” | LC 560. Different problem — uses a hash map of prefix sums. Worth naming as the “prefix-sum + hash” cousin. |
| ”What if numbers are streaming and we want a running answer?” | Kadane is already streaming-friendly. Each new value triggers one update; the running best is the current answer. No reset needed. |
| ”Optimal subarray length must be at least k.” | Sliding-window adaptation. Maintain a window of size at least k via a min-prefix-sum of prefix[i - k]. |
Pattern recognition — DP on running state
The move this problem teaches — re-frame a “best across all ” question as a recurrence over per-position best values — is a recurring shape in dynamic programming:
- House Robber. “Best loot ending at house , given you can’t rob adjacent houses.” One-line recurrence: .
- Best Time to Buy/Sell Stock. Track the running minimum price; the best profit ending at day is
prices[i] − runningMin. Same shape. - Longest Increasing Subsequence. “Length of the longest LIS ending at , considering all earlier positions with .” Same per-position re-framing; the inner scan over earlier positions makes it (or with a binary-searched tails array).
- Maximum Sum Increasing Subsequence. Hybrid of LIS and this problem; the recurrence considers all valid predecessors.
- Edit Distance. “Best alignment ending at the suffix pair .” Two-dimensional but the same per-position re-framing.
The reflex: when a problem asks for the best across structures, ask whether you can re-frame it as the best ending at each of positions. If yes, the algorithm is at most per position — and the resulting algorithm is almost always exactly overall.
What to say out loud — the delivery script
Ten beats:
- “Let me restate: given an array of integers, find the contiguous subarray with the largest sum.”
- Ask: “can A contain negatives? is the subarray required to be non-empty? sum or indices?”
- “Brute force enumerates every pair and sums each subarray — naïvely, with prefix sums. Let me name that and look for something better.”
- “There’s a divide-and-conquer angle that gets us to — but the linear-time answer is what most interviewers expect.”
- “Re-frame the question: instead of ‘best subarray of the whole array,’ ask ‘best subarray ending at each index.’ That’s subproblems, each with a one-line recurrence.”
- “At index : either start fresh at , or extend the best subarray ending at . Pick whichever is larger.”
- Write the code — five lines plus the loop. “
endingHere = max(A[i], endingHere + A[i]).best = max(best, endingHere). Initialize both to — that’s what makes all-negative inputs work.” - Trace one or two key transitions. Pick a cell where extending wins narrowly, or where restarting reverses the trend.
- “Complexity: time, space. The recurrence only ever needs the previous cell’s value, so the auxiliary array collapses to a single variable.”
- “Variations: minimum subarray flips both
maxcalls tomin. Circular subarray needs two passes. Returning indices needs two extra trackers. The core recurrence is the same.”
Summary
The largest-sum subarray problem is the rite of passage for dynamic programming. The interview signal isn’t producing the linear-time answer — it’s articulating the re-framing that makes it possible, and recognizing that the per-position recurrence collapses to a single variable.
The wider lesson is the shape of the optimization. Many interview problems ask for the best across an exponential or quadratic space of structures; many of those problems collapse to a linear-time recurrence the moment you ask the question per position rather than across all structures. House Robber, Best Time to Buy/Sell Stock, the longest-positive-run variant, the minimum-sum variant — they all yield to the same move. Internalize it here; the rest of the DP family becomes substantially less intimidating.