Avismara Hugoppalu
← All musings

Data Structures & Algorithms

Carry history or start fresh: Kadane in one decision

Maximum subarray as a single per-step choice — and why sliding window can't do it: the same symptom needs the opposite fix, so there's no consistent shrink rule.

May 20, 2026 · 5 min read

TL;DR
Maximum subarray needs no window and no table. At each element you make exactly one decision: extend the running subarray, or abandon it and start fresh here. cur = max(nums[i], cur + nums[i]) encodes that decision in one line — if the running sum has gone negative, dropping it is strictly better, so the max restarts for you. Track a separate best-so-far for the answer. Everything else is bookkeeping. The deeper lesson is the decision itself: at each step, does carrying history help, or does it only weigh you down?

Maximum-subarray gets taught as a clever trick with a name attached. I'd rather strip the name off and look at the only thing that does work: a single per-element decision. This is a sibling to solving everything with DP — Kadane is what DP collapses to when the state at each step is one number and the choice is binary.

The one decision

Stand at index i with a running subarray ending just before it. You have two moves and exactly two:

  • Extend: append nums[i] to what you've been building. The sum becomes cur + nums[i].
  • Abandon: throw away everything behind you and let nums[i] be a fresh start. The sum becomes nums[i].

You want the larger of the two ending here, so:

cur = max(nums[i], cur + nums[i])
best = max(best, cur)  // the answer can end at any index, so track it separately

The restart is not a special case you handle. It falls out of the max. If cur went negative, then cur + nums[i] < nums[i], and the max picks nums[i] — you've started fresh without writing an if. A negative prefix is dead weight; carrying it can only drag the next sum down, so the algorithm drops it the instant it stops helping. That single line is the whole algorithm. best exists only because the best subarray can end anywhere, not necessarily at the last index.

There are two variables and they answer two different questions. cur is "the best subarray that ends exactly here." best is "the best subarray seen anywhere so far." Conflating them is the classic Kadane bug — you return cur and silently report only subarrays that reach the end of the array.

Worth noticing when Kadane even earns its keep: if every number is positive, extending is always better than abandoning, so the answer is trivially the whole array. The decision only becomes interesting once negatives are in the mix — they're what make "should I keep carrying this?" a real question.

Trace it by hand

Hand-tracing is what made this click for me; the one-liner reads as obvious only after you've watched it restart. Take [-2, 1, -3, 4, -1, 2, 1, -5, 4].

i   nums[i]   cur + nums[i]   cur = max(nums[i], cur+nums[i])   best
0    -2        —              -2                                 -2
1     1        -1             1   (abandon: -1 < 1)              1
2    -3        -2             -2  (extend: still > -3)           1
3     4         2             4   (abandon: 2 < 4)               4
4    -1         3             3                                  4
5     2         5             5                                  5
6     1         6             6                                  6
7    -5         1             1                                  6
8     4         5             5                                  6

Two restarts, at i=1 and i=3, both exactly where the running sum had gone non-positive and the element alone was the better bet. Answer 6, the subarray [4, -1, 2, 1]. The cur column is the decision log; best just remembers the high-water mark.

Kadane · carry or restartstep 1/9
-2
1
-3
4
-1
2
1
-5
4
↻ start freshcur = -2best = -2best subarray = [00]

At each element: cur = max(x, cur + x). When carrying history would hurt, the max restarts you automatically.

Why not a sliding window?

This is the question I find most useful to sit with, because answering it teaches both patterns by negative space. A window slides — it advances a left pointer to shrink. So when a running sum drops, the window's only available move is "drop the leftmost element." Kadane's move, by contrast, is "drop everything behind me." Those are not the same move, and the difference is the whole story.

Here's the probe. Two arrays produce the same symptom — the running sum just dropped — but demand opposite fixes:

arraythe bad elementcorrect fix when the sum sags
[-2, 3, -1, 4]on the left (-2)drop from the left — shrink the window
[3, -1, -2, 4]in the middle (-1, -2)restart entirely — abandon, don't shrink

A window can only do the first. When the dead weight sits in the middle of your best run, shrinking from the left throws away the good prefix 3 while keeping the rot. The correct response — abandon the whole prefix and start at 4 — is a move a window structurally cannot make.

This is the sliding window post's monotonicity point seen from the other side. A window is legal only when the tracked quantity moves in one direction as the window grows, so a sagging sum unambiguously means "shrink." With negatives mixed in, the sum is not monotonic in window growth: the same symptom maps to two opposite correct actions depending on where the bad element is. No consistent shrink rule exists, so no window can be correct. Kadane sidesteps the whole problem by never shrinking — it only ever extends or resets to zero history.

Before
Sliding window
moves a left pointer to shrink
needs the tracked quantity monotone in window size
one fix for a sagging sum: drop from the left
breaks on negatives — bad element may be in the middle
After
Kadane
extends or fully restarts, never shrinks
needs no monotonicity at all
one fix for a sagging sum: abandon the whole prefix
negatives are the point — they trigger the restart

The idea is broader than the name

I used to wonder whether maximum-subarray was the only place this trick lived. It isn't — the name is narrow, the lens is broad. The transferable question is: at each step, does carrying history help, or should I start fresh? Once you hold it that way, the same decision shows up wearing different clothes.

  1. Maximum product subarray
    Same carry-or-restart decision, but a negative number flips max and min — a huge negative product becomes a huge positive one the moment you multiply by another negative. So you track both the running max and the running min ending here, because today's smallest could be tomorrow's largest. The decision is identical; the state just doubled.
  2. Best time to buy and sell stock
    Walk the prices tracking the lowest seen so far. At each day you decide: is this price a better fresh start (a lower buy) than the one I'm holding? If so, reset the buy point. It's "abandon the carried history when a better starting point appears" — Kadane's restart in a different costume.

The connection to DP as recursion with a cache is exact: Kadane is the degenerate case where the optimal solution ending at i depends only on the optimal solution ending at i-1, through one binary choice. No table, because the only state worth remembering is a single previous number. That's why it reads as a loop and not a recurrence — the memo has one slot.

The takeaway
Don't memorize Kadane as a formula. Hold it as a decision you re-make at every element: carry the running subarray forward, or abandon it and start here — cur = max(nums[i], cur + nums[i]), with a separate best because the answer can end anywhere. The reason it can't be a window is the reason worth keeping: with negatives, a sagging sum has two opposite correct fixes depending on where the bad element sits, so no shrink rule is consistent — Kadane just never shrinks. And the decision outlives the name. Max-product, buy-and-sell, and a whole class of one-state problems are the same question: at this step, does history help or hurt?

Problems to practice

Problems to practice