Find the largest sum obtainable from a contiguous run of elements in an array.
Walk the array once, keeping the best sum ending at the current element - that element alone, or that element added to the best run ending before it. The maximum of those is the answer, and the familiar version that resets the running sum to zero is wrong when every element is negative.
What the interviewer is scoring
- Does the candidate ask whether the run may be empty before choosing how to initialise
- Whether the recurrence is stated as a choice between extending and restarting, rather than as a rule about positive sums
- That the all-negative input is tested rather than discovered, since the common reset-to-zero form fails only there
- Whether the candidate can return the bounds of the run and not just its total
- Does the answer say what changes when the array arrives in pieces that must be combined
Answer
Settle the empty case before writing anything
The question as usually asked is ambiguous in one way that decides your
initialisation, so ask it: may the chosen run be empty? If it may, the answer for
an all-negative array is zero, because taking nothing beats taking anything. If it
may not — which is the usual intent, and the one every standard version of this
problem assumes — then the answer for [-5, -2, -9] is -2, the least bad single
element.
That is not pedantry. It is the difference between two initialisations, and the wrong one produces a function that looks correct on every example an interviewer gives verbally and is wrong on a case they will reach for immediately.
Extend or restart, and nothing else
At each element you face exactly one decision: does the best run ending here include the run ending at the previous element, or does it start fresh at this element? You never need to look further back than that, because any longer run ending here contains the run ending at the previous element as a suffix, and you have already computed the best of those.
function maxSubarraySum(nums) {
// Not 0: the run must be non-empty, so seed with a real element.
let best = nums[0];
let endingHere = nums[0];
for (let i = 1; i < nums.length; i += 1) {
// Extend the previous run, or abandon it and start at nums[i].
endingHere = Math.max(nums[i], endingHere + nums[i]);
best = Math.max(best, endingHere);
}
return best;
}
Two variables and one pass. The recurrence is the whole algorithm, and stating it as a choice — extend or restart — is what an interviewer is listening for. A candidate who instead says "keep adding while the sum stays positive" has described the same behaviour by accident and will not be able to defend it on the input below.
The reset to zero, and the input that exposes it
The form almost everyone writes from memory is this:
let sum = 0, best = 0;
for (const n of nums) {
sum += n;
if (sum < 0) sum = 0; // the reset
best = Math.max(best, sum); // best never drops below 0
}
On [-5, -2, -9] it returns 0. There is no empty run allowed, so the correct
answer is -2, and nothing about the code looks wrong — best simply never falls
below its initial value. Trace it: sum goes to -5 and resets to 0, then -2
and resets, then -9 and resets. best was 0 at the start and is 0 at the end.
This is worth dwelling on because of how it fails. It is correct on every array
containing at least one positive element, which is every example anybody offers
casually, so it survives testing by improvisation and dies on the first
adversarial input. Seeding both variables from nums[0] and iterating from index 1
removes the failure entirely, without a special case for negatives.
The related slip is initialising best to 0 while computing endingHere
correctly. The recurrence is then right and the answer is still clamped, which is
harder to spot because the interesting part of the code is doing the right thing.
Recording where the run starts, not just that it ended
Returning the bounds is the usual follow-up, and the off-by-one lives in the
start index rather than the end. The end is easy: it is wherever you improved
best. The start has to be captured at the moment you restart, which is one
branch earlier than the moment you record an improvement.
let best = nums[0], endingHere = nums[0];
let bestStart = 0, bestEnd = 0, currentStart = 0;
for (let i = 1; i < nums.length; i += 1) {
if (endingHere + nums[i] < nums[i]) {
endingHere = nums[i];
currentStart = i; // the restart, recorded here or lost
} else {
endingHere = endingHere + nums[i];
}
if (endingHere > best) {
best = endingHere;
bestStart = currentStart;
bestEnd = i;
}
}
Collapsing the branch back into Math.max is what loses currentStart, because
the maximum tells you the value that won and not which of the two cases produced
it. That is the trade: the compact version cannot report its own answer's
location.
When the array does not fit on one machine
The distributed follow-up is where the linear scan stops being the whole story, and it is a genuine test of whether the candidate understands the recurrence or has memorised it. A chunk cannot just report its own best run, because the true best may straddle a boundary. Each chunk has to return four numbers: its total sum, its best run, the best run touching its left edge, and the best run touching its right edge. Combining two neighbours takes the better of their individual bests and the run formed by joining the left neighbour's right-edge run to the right neighbour's left-edge run.
That decomposition is associative, which is why it parallelises and why the same shape appears in segment trees. A candidate who reaches it has understood that the sequential version is a fold over exactly this state, compressed because a single pass never needs the left-edge value.
Likely follow-ups
- Return the start and end indices of the best run. Where exactly do you record the start?
- The array is split across four machines. What does each one have to send you?
- Allow at most one element to be dropped from the run. How does the state change?
- What if the array is enormous and arrives as a stream you cannot revisit?
Related questions
- Given a set of coin denominations and a target amount, return the fewest coins that make it exactly. Why would you not just take the largest coin first each time?mediumAlso on dynamic-programming4 min
- For every index, give me the product of all the other elements - and you cannot use division.mediumAlso on arrays3 min
- Rotate an array k positions to the right, in place.mediumAlso on arrays4 min
- Return every distinct triplet in the array that sums to zero.mediumAlso on arrays4 min
- Given the heights of a row of bars, work out how much rainwater is trapped between them. Do it in constant extra space.mediumAlso on arrays4 min
- Find the length of the longest strictly increasing subsequence, then get it under quadratic time.hardAlso on dynamic-programming6 min
- For each element of an array, find the first element to its right that is larger. State the invariant your stack maintains.mediumAlso on arrays4 min
- How do you know a problem is dynamic programming?mediumAlso on dynamic-programming5 min