Given an integer array nums, find the subarray with the largest sum, and return its sum.
1 ≤ nums.length ≤ 105-104 ≤ nums[i] ≤ 104We're going to derive the recurrence from scratch ā not just memorize Kadane's algorithm. By the end, you'll know why the recurrence takes this shape and how to recognize the same pattern in other problems.
A subarray is a contiguous segment of the array. Unlike a subsequence, you cannot skip elements.
We want the subarray with the maximum sum. Let's think about what that means one element at a time:
Imagine you're scanning left to right. When you reach position i, you look at nums[i] and ask:
"Should I extend the best subarray ending before me,
or should I start fresh right here?"
This is the only choice at each position ā and it's what makes DP work.
Let's make this concrete with [-2, 1, -3, 4, -1, 2, 1, -5, 4]:
At index 0: only one option ā take -2. Best subarray ending at 0 has sum -2.
At index 1 (value 1): extend with -2 ā sum -1, or start fresh ā sum 1. Start fresh wins.
At index 3 (value 4): extend the best ending at 2 (-2) ā sum 2, or start fresh ā sum 4. Start fresh wins.
At index 4 (value -1): extend 4 ā sum 3, or start fresh ā sum -1. Extend wins.
When is it always better to start fresh? When the previous best is negative ā adding a negative to the current number can only make it smaller. When the previous best is positive, extending always improves the sum.
This gives us a simple rule:
if previous_sum > 0 ā extend it. Otherwise ā start fresh.
Define dp[i] = maximum sum of a subarray ending at index i.
Break this down:
i. The best subarray ending at i is just nums[i] alone.i-1 and append nums[i]. This is only beneficial when dp[i-1] > 0.max(nums[i], dp[i-1] + nums[i]) encodes both cases in one line ā extend wins when dp[i-1] > 0, start fresh wins otherwise.To find the best subarray ending at i, we only need:
nums[i] ā the current element (given)dp[i-1] ā the best subarray ending at the previous position (already computed)We don't need to know which subarray produced dp[i-1] ā the sum alone is enough. This is the hallmark of 1D DP: each state depends only on the immediately previous state.
i-1 to compute the answer for subarrays ending at i in O(1). The answer for the whole array is just the maximum over all dp[i].
Choose an example array and step through the DP computation:
Unlike interval DP (palindromic substring), this is a 1D DP. The traversal is straightforward:
dp[i] depends only on dp[i-1]. There's no dependency on future indices or shorter intervals. You can fill the array in a single left-to-right pass ā O(n) time.
dp[i] depends only on dp[i-1], we can drop the array entirely and keep a single variable. This is Kadane's algorithm ā same recurrence, O(1) space.
dp[i] semanticscurrentMax = dp[i] at step iThe problem's follow-up asks for a divide and conquer solution. Here's the intuition:
For any subarray, one of three things is true:
Cases 1 and 2 are recursive. Case 3 can be computed in O(n) by finding the max suffix sum of the left half and max prefix sum of the right half, then adding them.
| Problem | Recurrence | Pattern |
|---|---|---|
| Maximum Subarray | dp[i] = max(nums[i], dp[i-1] + nums[i]) |
1D, depends on dp[i-1] |
| Maximum Product Subarray | dpMax[i] = max(nums[i], dpMax[i-1]*nums[i], dpMin[i-1]*nums[i]) |
1D, track both min and max |
| Maximum Sum Circular Subarray | Kadane + Kadane on negated array + wrap-around |
1D + circular trick |
| Best Time to Buy/Sell Stock | dp[i] = max(dp[i-1], price[i] - minSoFar) |
1D, depends on prefix min |
| House Robber | dp[i] = max(dp[i-1], dp[i-2] + nums[i]) |
1D, depends on dp[i-1] and dp[i-2] |
The common thread: 1D DP with a "continue-or-restart" decision. The recurrence is always dp[i] = max(fresh_start, extend_previous) ā the only difference is what "extend" means in each context.
dp[i] as the best answer for subarrays that must end at i. This constraint is what unlocks the recurrence.dp[i] depends only on dp[i-1], you have O(1) space. If it depends on dp[i-2] or a window, you need more.dp[n-1] or max(dp[0..n-1])? ā For "ending at" DP, the answer is typically the maximum over all dp[i], not necessarily the last one.max_sum = n destroys the global maximum BLOCKINGEvery time cur_sum < 0, you reset max_sum = n without comparing against the existing best. This means if the previously found global maximum is larger than the fresh-start element, you throw it away.
Counterexample: [2, -3, 1]
| Step | idx | nums[idx] | cur_sum (before) | Branch | max_sum (after) | Correct max |
|---|---|---|---|---|---|---|
| init | 0 | 2 | ā | ā | 2 | 2 |
| 1 | 1 | -3 | 2 | else (extend) | 2 | 2 |
| 2 | 2 | 1 | -1 | if (fresh) | 1 | 2 |
At idx=2, cur_sum = -1 < 0 ā enters the fresh branch ā max_sum = 1. The correct answer is 2 (the element at index 0 alone). Your code returns 1.
ā ļø This bug is easy to miss because it only fires when: (a) cur_sum goes negative, AND (b) the fresh-start element is smaller than a previous best. Many test cases never trigger (a) because they're overwhelmed with positive numbers.
max_sum = max(max_sum, n) ā the global max should never decrease
a_start / a_end corrupted by the same overwrite BLOCKINGWhen Bug #1 fires, the tracking variables a_start and a_end are reset to the fresh-start position even though that new subarray doesn't beat the old best. This means:
print at the bottom reports the wrong subarraymax_sum ā a classic invariant violationIn the [2, -3, 1] counterexample: a_start=2, a_end=2 but max_sum should be 2 (from index 0). The tracking says [1] is the answer, which contradicts the correct answer [2].
a_start/a_end when the fresh-start actually beats the existing max_sum:if n > max_sum: a_start = a_end = idx; max_sum = n
print statements in production submissionLeetCode doesn't penalize print(), but in a real interview or code review, debug scaffolding left in the final answer signals carelessness. If the problem asked for the subarray itself, the prints would be a substitute for a proper return value ā which isn't what was asked.
n shadows conventional meaningn is universally used for array length. Using it for the current element is confusing ā especially when len(nums) appears nearby. An interviewer will ask: "what does n represent here?" Use num or x.
You test if cur_sum < 0 (the bad case) first and else (the good case) second. The canonical Kadane tests if cur_sum > 0 (extend) / else (fresh). Your version is functionally equivalent but reads as "when things are bad, do X; otherwise, Y" ā which forces the reader to invert the logic mentally. Always put the affirmative branch first:
else:This is subtle but it's exactly the kind of thing a strict reviewer flags:
The comment is indented to the same level as else: (12 spaces), while new_sum is at 16 spaces. Python technically accepts this ā comments are ignored by the parser and don't affect block structure. But every human reader will do a double-take wondering if this is a syntax error. The comment should be inside the block:
a_start never updated during extensionWhen the else branch finds new_sum > max_sum, you update a_end = idx but never touch a_start. This happens to work because a_start was set when you entered the current subarray (the last time you started fresh). But this is an implicit invariant ā the code doesn't state it, so a future refactor could break it. Explicitly tracking where the current subarray begins (separate from where the best subarray begins) is clearer:
LeetCode 53 asks for the sum only. Adding a_start, a_end, and print() to reconstruct the subarray is a feature the problem doesn't ask for. In an interview, this is neutral-to-positive if you get it right. But when tracking variables contain bugs (Issue #2, #7), the extra complexity becomes a liability ā it introduces failure modes for something that wasn't required. Ship the simplest correct solution first, then extend.
max_sum is never decremented ā always max(max_sum, ...)cur_sum > 0 (extend) first, else (fresh) seconda_start/a_end: the problem asks for the sum, not the subarrayn ā num: no shadowing, clear meaningprint(): clean production codemax_sum update: single max(max_sum, cur_sum) after each step ā no duplicated logic across branchesOr even shorter ā the one-liner recurrence that's standard in interviews:
You clearly understand Kadane's core idea ā the extend-vs-fresh decision. The explicit if/else branching proves you've internalized the recurrence. The attempt to track subarray bounds shows ambition beyond the minimum requirement.
But Bug #1 is a classic DP mistake: updating the global optimum inside a conditional branch without a max() guard. This pattern ā "I know I found a new candidate, so it must be better" ā is exactly how DP solutions fail in interviews. The fix is mechanical: never let max_sum decrease. Every write to it must pass through max() or a comparison guard.
max_sum = max(max_sum, cur_sum) out of the branches. One line, unconditional, after each step. Guarantees correctness.[2, -3, 1], [5, -10, -1], [-2, 1, -3, 4]. These expose the "fresh start destroys best" bug.max_sum must always equal the max of all subarrays ending at or before the current index. If tracking variables disagree, one of them is wrong.cur_sum = max(num, cur_sum + num) says in one line what your if/else says in six. Both are correct, but the one-liner signals fluency.
What you fixed: ā
Bug #1 (max_sum overwrite) ā killed. ā
Unified extend/fresh into a single ternary. ā
max_sum now only updates via a guard comparison.
What regressed: ā a_start is never updated at all ā it was at least set in the fresh branch of #1, now it's frozen at 0 forever.
a_start is dead code ā never written after initialization BLOCKING regression from #1In submission #1, a_start was at least set inside the if cur_sum < 0 branch (even if it had the overwrite bug). In this submission, a_start is never updated at all. It stays frozen at 0 forever.
Why this is worse than #1: In #1, the tracking was sometimes right (when the overwrite bug didn't fire). Here, it's always wrong for any input where the best subarray doesn't start at index 0.
Trace through the main example: [-2, 1, -3, 4, -1, 2, 1, -5, 4]
| idx | num | cur_sum (before) | new_sum | new best? | a_start (after) | a_end (after) | max_sum | printed subarray |
|---|---|---|---|---|---|---|---|---|
| 0 | -2 | ā | ā | ā | 0 | 0 | -2 | [-2] |
| 1 | 1 | -2 | 1 (fresh) | ā | 0 | 1 | 1 | [-2, 1] ā WRONG |
| ... | ... | ... | ... | ... | 0 | 6 | ... | ... |
| final | ā | ā | ā | ā | 0 | 6 | 6 | [-2,1,-3,4,-1,2,1] sum=2 ā WRONG |
The print at the bottom reports the subarray [-2, 1, -3, 4, -1, 2, 1] (indices 0ā6, sum = 2), but max_sum = 6. The correct subarray is [4, -1, 2, 1] (indices 3ā6). The tracking variables disagree with the returned answer.
curr_start separately from best_startYou need two concepts: where the current running subarray begins (updates on every fresh start), and where the best subarray begins (only updates when a new max is found):
a_start is dead code with a misleading nameA variable named a_start that is initialized, never modified, and always reads as 0 is a red flag. It signals either: (a) the tracking logic was removed during refactoring, or (b) the variable was added with intent that was never implemented. Either way, dead code in a 15-line function is hard to justify.
print output is now actively misleadingIn submission #1, the prints were sometimes wrong. In submission #2, they're always wrong (unless the best subarray happens to start at index 0). Debug output that contradicts the returned answer is worse than no output ā it would send anyone debugging this code down the wrong path. Simply remove the prints.
n shadowing from #1Unchanged. n = nums[idx] still shadows the array-length convention. Use num or x.
n if cur_sum < 0 else cur_sum + n ā the ternary tests the negative case first. While compact, cur_sum + num if cur_sum > 0 else num reads left-to-right as "extend if beneficial, else fresh" and matches how a human thinks about the problem.
# if cur_sum is negative - better to start a fresh array describes behavior that happens implicitly inside the ternary, but the ternary also handles the non-negative case with an else. The comment only describes half the logic. Either remove it or say what the line actually does: # extend if beneficial, otherwise restart.
The collapse of 6 lines of if/else into one ternary is the right instinct. Compare:
This is cleaner, less error-prone, and closer to the canonical form. The direction is right ā now just finish the job.
If you really want subarray tracking (not required by the problem, but good practice):
You fixed the big bug and made the code more concise ā that's real progress. The ternary is the right refactoring direction. The max_sum guard is correct now.
But you dropped the a_start update entirely. When you refactor an if/else into a ternary, check whether every side effect from the original branches was preserved. The fresh branch in #1 set a_start ā when that branch disappeared into the ternary, the assignment disappeared too.
curr_start (where the current subarray begins) vs. best_start (where the best subarray begins). These update at different times ā confusing them is the root of this bug.a_start is now dead. Either remove it (if you're only returning the sum) or wire it up correctly (if you're tracking). A dead variable is worse than no variable.cur_sum = cur_sum + num if cur_sum > 0 else num; max_sum = max(max_sum, cur_sum). That's the whole algorithm in two lines.