Maximum Subarray problem in DSA: Brute Force to Kadane’s Algorithm
-
Last Updated: July 13, 2026
-
By: javahandson
-
Series
Learn Java in a easy way
Learn the Maximum Subarray problem in DSA with a clear walkthrough: brute force, Kadane’s Algorithm, divide and conquer, dry runs, and a full complexity comparison. As a developer I will be using Java code but pseudo-codes, explanation and dry runs are language neutral and you can understand the concept easily. So don’t skip it because language is just a syntax.
The Maximum Subarray problem in DSA is a small question with a big payoff. It looks tricky at first glance. Once you meet Kadane’s Algorithm, though, the whole thing clicks, and you carry that trick into many harder problems.
Here is the task in plain words. Somebody hands you an array of numbers, and some of them may be negative. You must find the contiguous slice with the largest sum. The slice has to be one unbroken run, so skipping around is not allowed.
In our last article we solved Contains Duplicate by remembering which values we had already seen. Now the focus shifts from membership to a running total, and that shift opens up a different family of problems.
We build up in three stages. Brute force comes first, because a correct slow answer beats a clever wrong one. Kadane’s one-pass trick comes next, arriving in two steps so the leap feels small. Divide and conquer closes the set, since interviewers love asking for it as a follow-up.
Here is the road map for the rest of the guide.
Let us pin down the rules first. A clear problem statement keeps you out of trouble later, and it costs about thirty seconds.
Every version of this question comes down to the same handful of rules.
[-2, 1, -3, 4, -1, 2, 1].Worth knowing about the original version of this question: the array can hold up to about 100,000 numbers, and each value sits between minus 10,000 and plus 10,000. Multiply those out and the largest possible total is around one billion, which still fits comfortably in a 32-bit integer. So overflow is not a worry here, though it would be if the limits were looser.
Take [-2, 1, -3, 4, -1, 2, 1]. The answer is 6, and it comes from the slice [4, -1, 2, 1].
Look at that winning slice for a second. It contains a negative number. Most beginners expect the best run to hold only positive values, yet the -1 earns its place because the 2 and the 1 sitting behind it more than pay for it.
That is the whole tension in this problem. A negative number is worth carrying when what follows makes up for it, and worth dropping when it does not. Every approach below is really just a different way of answering that one question.
One word in the problem statement does a lot of heavy lifting: contiguous. The slice must be an unbroken run of neighbours.
Suppose you could skip elements freely. Then the answer would be trivial, because you would simply add up every positive number and ignore the rest. On our example that shortcut gives 1 plus 4 plus 2 plus 1, which is 8, and 8 is not the answer.
Getting 8 instead of 6 is the classic sign that somebody solved the subsequence problem by mistake. Read the word contiguous carefully, and say it back to your interviewer to confirm.
| 💡 Interview Insight Interviewers often ask why an all-negative array like [-3, -1, -2] still has an answer. Because a subarray needs at least one element, the best you can do is the single largest value, which is -1. Naming this edge case early shows you read the constraints carefully. |
Three small ideas carry this whole problem. All of them are easy, and all of them show up again in later array questions.
A running sum is just a total you keep updating as you walk the array. Add each new value, and the total reflects everything in the current run.
Think of a shop till through the day. Sales push it up, refunds pull it down, and at any moment the number in front of you sums everything so far. You never re-add the earlier receipts.
Because the total already holds the past, extending a run costs one addition. That cheapness is what lets a single pass replace a nest of loops.
The second idea is even simpler. While the running sum climbs and dips, a separate variable quietly remembers the highest total you have ever reached.
Picture a high-water mark on a harbour wall. The tide rises and falls all day, and the mark stays put at the highest point. Nothing that happens afterwards can lower it.
So you need two numbers, not one. Merge them and you lose the record the moment the current run turns bad, which is the single most common bug in this problem.
Now the idea that makes everything fast. Standing on any number, you face exactly one decision.
Why is that safe? Because a run ending at the current position either includes the previous run or it does not. Those are the only two shapes available, so checking both covers everything.
Here is the rule of thumb underneath it. Carrying a negative total forward can only shrink whatever comes next, so you drop it. Carrying a positive total forward can only help, so you keep it.
The first idea most people reach for is direct. Try every possible slice, add each one up, and keep the largest total you find.
best = -infinity
for start from 0 to n-1:
sum = 0
for end from start to n-1:
sum = sum + nums[end]
best = max(best, sum)
return bestThe outer loop fixes a starting point. The inner loop stretches the slice one element at a time and grows the running sum. After each step we compare that sum against our best.
public class MaximumSubarrayBrute {
public static int maxSubArray(int[] nums) {
int best = Integer.MIN_VALUE;
for (int start = 0; start < nums.length; start++) {
int sum = 0;
for (int end = start; end < nums.length; end++) {
sum += nums[end];
best = Math.max(best, sum);
}
}
return best;
}
public static void main(String[] args) {
int[] nums = { -2, 1, -3, 4, -1, 2, 1 };
System.out.println(maxSubArray(nums)); // Output: 6
}
}Let us go slow and picture what the loops are really doing. A subarray is just a slice with a left edge and a right edge. Brute force says: try every left edge, and for each one, try every right edge. That covers all possible slices.
Think of it with your hands. Put your left finger on one number. Then drag your right finger across the array, adding up values as you go. Every time the right finger moves, you have a new slice, so you check its total.
Now let us tie each part of the loop to that picture.
Here is the detail that trips people up. We never rebuild a sum from scratch.
As the right finger moves one step, we add exactly one more number to the total we already had. A naive version would loop over the whole slice again to total it up, which adds a third layer of work.
That small reuse is worth a lot. It saves an enormous amount of repeated addition, and it is the reason the cost lands at n squared rather than n cubed.
This version is easy to trust, because it literally checks everything. Nothing gets skipped, so it cannot miss the answer. Its only weakness is speed, which we fix next.
Trace a short array, nums = [1, -3, 4], and follow the slices in the order the loops produce them.
| Left edge | Right edge | Slice | Sum | Best so far |
|---|---|---|---|---|
| 0 | 0 | [1] | 1 | 1 |
| 0 | 1 | [1, -3] | -2 | 1 |
| 0 | 2 | [1, -3, 4] | 2 | 2 |
| 1 | 1 | [-3] | -3 | 2 |
| 1 | 2 | [-3, 4] | 1 | 2 |
| 2 | 2 | [4] | 4 | 4 |
Six slices for three numbers, and the winner turns out to be the very last one. The single element [4] beats every longer slice, because dragging the -3 along always costs more than it returns.
Notice the shape of the work too. The left edge at 0 produced three slices, the next produced two, and the last produced one. Add those up and you get 6, which is the familiar half-of-n-squared pattern.
Let us count the work in plain terms. The left edge can sit on any of the n numbers. For each of those spots, the right edge walks across the rest of the array, which is again up to n steps. So the total work grows like n times n.
In Big-O terms that is O(n squared) time. At 10 numbers you get around 50 steps, which is nothing. At 100,000 numbers you get about five billion, and the program crawls.
Space is much friendlier. Two extra variables cover everything, no matter how large the array grows, so the space cost is O(1). For small arrays this approach is perfectly fine. For large ones, we need something faster.
Now for the version interviewers really want. Kadane’s Algorithm walks the array just once. Instead of rebuilding every slice, it carries one running total and makes a smart choice at each number. To really get it, let us build up to the final code in two steps.
Imagine walking along the array, adding numbers to a running total as you go. At some point that total might dip below zero. When it does, dragging it forward only hurts you. A negative total can never help a future slice, so you may as well throw it away and start again.
Here is a money analogy that makes it click. Say you save some money each day, but some days you lose money instead. Should your savings ever go negative, it is smarter to reset to zero and start saving fresh than to carry that debt forward. Kadane’s does exactly this with array sums.
So the plan has two moving parts. We keep a running total for the slice we are building, and we reset it to zero the moment it turns negative. Alongside that, a separate best value remembers the highest total we have ever reached.
Let us write that plan out literally, with a plain check for the reset. This version matches the analogy word for word, so it is the easiest one to read.
currentSum = 0
bestMax = nums[0]
for i from 0 to n-1:
currentSum = currentSum + nums[i] // add the current number
bestMax = max(bestMax, currentSum) // record before resetting
if currentSum < 0: // total turned negative
currentSum = 0 // throw it away, start fresh
return bestMaxpublic class MaximumSubarrayReset {
public static int maxSubArray(int[] nums) {
int currentSum = 0;
int bestMax = nums[0];
for (int i = 0; i < nums.length; i++) {
currentSum += nums[i]; // add the current number
bestMax = Math.max(bestMax, currentSum);
if (currentSum < 0) { // total turned negative
currentSum = 0; // throw it away, start fresh
}
}
return bestMax;
}
public static void main(String[] args) {
int[] nums = { -2, 1, -3, 4, -1, 2, 1 };
System.out.println(maxSubArray(nums)); // Output: 6
}
}Read it slowly and it mirrors the savings story exactly.
Order matters enormously in those last two steps. Record the best first, then reset. Reset before recording and you can wipe out a total you never got credit for.
Let us trace this version by hand on nums = [-2, 1, -3, 4, -1, 2, 1]. Watch the running total climb, then snap back to zero whenever it turns negative. We begin with the total at 0 and the record at -2.
| i | num | Running total | Record | Negative? Reset? |
|---|---|---|---|---|
| 0 | -2 | 0 + -2 = -2 | -2 | Yes, reset to 0 |
| 1 | 1 | 0 + 1 = 1 | 1 | No |
| 2 | -3 | 1 + -3 = -2 | 1 | Yes, reset to 0 |
| 3 | 4 | 0 + 4 = 4 | 4 | No |
| 4 | -1 | 4 + -1 = 3 | 4 | No |
| 5 | 2 | 3 + 2 = 5 | 5 | No |
| 6 | 1 | 5 + 1 = 6 | 6 | No |
Notice step 2. The total drops to -2, so we reset to zero right after recording. That reset is why step 3 starts clean at 4 instead of dragging the -2 forward. From there the run climbs to 6, which is our answer.
Step 4 deserves a glance as well. The total falls from 4 to 3, yet it stays positive, so no reset happens. Keeping that slightly bruised run alive is exactly what lets the 2 and the 1 push it up to 6. Reset too eagerly and you lose the winning slice.
The reset version is clear, but Kadane’s classic form folds that check into a single line. Watch how the two ideas turn out to be the same thing.
In the reset version, a negative total gets thrown away, so the next number starts a run of just itself. A positive total survives, so the next number lands on top of it. That is exactly a choice between two values: the number alone, or the number plus the running total.
We can write that choice as one maximum. Should the running total be negative, adding it to the current number gives less than the number alone, so the maximum picks the number and effectively resets. Should the running total be positive, the sum wins, so the run extends. The check disappears into the comparison.
// The reset logic ... currentSum += nums[i]; if (currentSum < 0) currentSum = 0; // reset when negative // ... becomes this single line: currentMax = Math.max(nums[i], currentMax + nums[i]);
Both do the same job. The one-liner decides restart-or-extend in a single comparison, rather than adding first and cleaning up afterwards.
currentMax = nums[0]
bestMax = nums[0]
for i from 1 to n-1:
currentMax = max(nums[i], currentMax + nums[i])
bestMax = max(bestMax, currentMax)
return bestMaxpublic class MaximumSubarrayKadane {
public static int maxSubArray(int[] nums) {
int currentMax = nums[0];
int bestMax = nums[0];
for (int i = 1; i < nums.length; i++) {
currentMax = Math.max(nums[i], currentMax + nums[i]);
bestMax = Math.max(bestMax, currentMax);
}
return bestMax;
}
public static void main(String[] args) {
int[] nums = { -2, 1, -3, 4, -1, 2, 1 };
System.out.println(maxSubArray(nums)); // Output: 6
}
}Now let us walk through the final version slowly. Two values do all the work, and knowing their jobs first makes the code obvious.
With those two roles clear, the lines make sense.
Notice why two separate values are essential. The current run can rise, fall, and even restart at a small number. Track only that and you lose the big total you hit earlier. The record acts like a photograph of your best moment, safe from whatever happens next.
We touch each number exactly once and do a couple of comparisons per number. So the work grows in a straight line with the array size, which is O(n) time.
Memory never grows at all. Two integers cover any input length, so the space cost is O(1). That single clean pass is what makes Kadane’s Algorithm shine, and it beats brute force on both counts at the same time.
| 💡 Interview Insight A common follow-up asks why we seed with the first element instead of 0. If you start the record at 0, an all-negative array wrongly returns 0, since no run can beat an empty total. Seeding with the first element keeps the answer correct for every input. |
Now let us trace the compact version on the same array, nums = [-2, 1, -3, 4, -1, 2, 1]. It should reach the same answer as the reset version, just through the maximum instead of a reset. We follow both the current run and the record as they change.
| i | num | Current run = max(num, previous + num) | Record |
|---|---|---|---|
| 0 | -2 | -2 (start) | -2 |
| 1 | 1 | max(1, -2 + 1) = 1 | 1 |
| 2 | -3 | max(-3, 1 + -3) = -2 | 1 |
| 3 | 4 | max(4, -2 + 4) = 4 | 4 |
| 4 | -1 | max(-1, 4 + -1) = 3 | 4 |
| 5 | 2 | max(2, 3 + 2) = 5 | 5 |
| 6 | 1 | max(1, 5 + 1) = 6 | 6 |
Look at step 3 closely. The running total had dipped to -2, so adding 4 gives only 2. Yet 4 by itself is larger, so the run restarts at 4. Compare that with the reset version, where the total had already snapped to 0, so 0 plus 4 also gave 4. Both land on the same value, which proves the two versions really are one idea in two costumes.
From there the run climbs to 6, which becomes our final answer.

The sketch shows the same trace. The array sits on top, each step lists the choice, and the running values sit on the right. Many people find this picture easier than the table, so use whichever clicks for you.
Now the case everybody gets wrong. Trace nums = [-3, -1, -2], where nothing positive exists anywhere.
| i | num | Current run = max(num, previous + num) | Record |
|---|---|---|---|
| 0 | -3 | -3 (start) | -3 |
| 1 | -1 | max(-1, -3 + -1) = -1 | -1 |
| 2 | -2 | max(-2, -1 + -2) = -2 | -1 |
The answer is -1, which is the single largest value in the array. That makes sense, because a slice must hold at least one element, so the least painful choice is the smallest loss on its own.
Watch what the maximum does here. Every step picks the bare number over the extended run, because adding a negative to a negative always makes things worse. So the algorithm restarts on every single step, and the record simply tracks the largest element.
Interviewers who already know you can write Kadane’s often ask for this one next. It is slower, and that is not the point. The point is whether you can reason about a problem by splitting it in half.
Cut the array down the middle. The best slice must now be one of exactly three things.
Those three cases cover every possibility, so the answer is simply the largest of them. The first two come free from recursion, and only the third needs real work.
Handling the crossing case is easier than it sounds. Start at the middle and walk left, keeping a running total and remembering the best you reach. Then start just right of the middle and walk right the same way. Add those two bests together and you have the finest slice that spans the divide.
Why must you walk outward from the middle rather than scan freely? Because a crossing slice has no choice about its inner edges. It must touch the middle, so both halves grow outward from that fixed point.
solve(nums, lo, hi):
if lo == hi:
return nums[lo] // one element, nothing to split
mid = lo + (hi - lo) / 2
left = solve(nums, lo, mid)
right = solve(nums, mid + 1, hi)
return max(left, right, crossing(nums, lo, mid, hi))
crossing(nums, lo, mid, hi):
sum = 0, bestLeft = -infinity
for i from mid down to lo: // walk outward, left of centre
sum = sum + nums[i]
bestLeft = max(bestLeft, sum)
sum = 0, bestRight = -infinity
for i from mid+1 up to hi: // walk outward, right of centre
sum = sum + nums[i]
bestRight = max(bestRight, sum)
return bestLeft + bestRightpublic class MaximumSubarrayDivide {
public static int maxSubArray(int[] nums) {
return solve(nums, 0, nums.length - 1);
}
private static int solve(int[] nums, int lo, int hi) {
if (lo == hi) {
return nums[lo];
}
int mid = lo + (hi - lo) / 2;
int left = solve(nums, lo, mid);
int right = solve(nums, mid + 1, hi);
int cross = crossing(nums, lo, mid, hi);
return Math.max(Math.max(left, right), cross);
}
private static int crossing(int[] nums, int lo, int mid, int hi) {
int sum = 0;
int bestLeft = Integer.MIN_VALUE;
for (int i = mid; i >= lo; i--) {
sum += nums[i];
bestLeft = Math.max(bestLeft, sum);
}
sum = 0;
int bestRight = Integer.MIN_VALUE;
for (int i = mid + 1; i <= hi; i++) {
sum += nums[i];
bestRight = Math.max(bestRight, sum);
}
return bestLeft + bestRight;
}
public static void main(String[] args) {
int[] nums = { -2, 1, -3, 4, -1, 2, 1 };
System.out.println(maxSubArray(nums)); // Output: 6
}
}One detail is easy to miss. Both outward walks seed their best at negative infinity rather than zero. Seeding at zero would allow an empty half, and a crossing slice must take at least one element from each side.
Each call splits its range in two and then scans the whole range once for the crossing case. Splitting gives about log n levels of recursion, and every level scans n elements in total. Multiply those and you land at O(n log n) time.
Space is O(log n), which is the depth of the recursion stack rather than any array we allocate. So this approach costs more time than Kadane’s and more memory too.
Learn it anyway. Interviewers ask for it because the split-into-three-cases habit unlocks a whole category of problems, and because they want to see whether you can reason about recursion under pressure.
All three give the correct answer. They simply charge different prices for it.
| Point | Brute Force | Kadane’s | Divide and Conquer |
|---|---|---|---|
| Time | O(n squared) | O(n) | O(n log n) |
| Extra space | O(1) | O(1) | O(log n) |
| Passes over the array | Many | One | About log n |
| Recursion | None | None | Yes |
| Work at 100,000 values | About 5 billion steps | 100,000 steps | About 1.7 million steps |
| Ease of writing | Very easy | Easy once it clicks | Fiddly |
| Good for interviews | As a starting point | As the final answer | As the follow-up |
Kadane’s wins on every measurable axis here, which is unusual. Normally a faster algorithm costs you memory, and this one does not.
So walk that path out loud in an interview. Mention brute force to show you understand the problem, move to Kadane’s to reach the best complexity, and keep divide and conquer in your pocket for the follow-up. That journey from slow to fast is the part interviewers actually grade.
| 💡 Interview Insight If the interviewer asks you to also return the subarray itself, not just the sum, track the start and end positions alongside the current run. Reset the tentative start whenever the run restarts. Mentioning this extension out loud signals real depth. |
Solve the base problem quickly and a follow-up usually arrives. These two show up most often, and both build on the single pass you already have.
A bare number is a little abstract. Real tools want the actual slice. The fix costs three extra variables: a tentative start that moves whenever the run restarts, plus a start and end that freeze whenever the record improves.
public class MaximumSubarrayWithBounds {
public static int[] bestSlice(int[] nums) {
int currentMax = nums[0];
int bestMax = nums[0];
int tentativeStart = 0;
int start = 0;
int end = 0;
for (int i = 1; i < nums.length; i++) {
if (nums[i] > currentMax + nums[i]) {
currentMax = nums[i];
tentativeStart = i; // the run restarts here
} else {
currentMax = currentMax + nums[i];
}
if (currentMax > bestMax) {
bestMax = currentMax;
start = tentativeStart; // freeze the winning bounds
end = i;
}
}
return new int[] { bestMax, start, end };
}
public static void main(String[] args) {
int[] nums = { -2, 1, -3, 4, -1, 2, 1 };
int[] result = bestSlice(nums);
System.out.println(result[0]); // Output: 6
System.out.println(result[1]); // Output: 3
System.out.println(result[2]); // Output: 6
}
}Notice that the tentative start and the confirmed start are different things. The run may restart several times without ever beating the record, so a tentative start that never pays off simply gets overwritten later.
Run it on our example and you get a sum of 6 spanning positions 3 through 6, which is the slice [4, -1, 2, 1]. That matches the dry run exactly.
This is the follow-up that catches the most people, and section 6.2 already traced it. The correct answer is the largest single value, because a slice must hold at least one element.
Seeding both values with the first element handles it automatically, with no special case anywhere. Seed the record at 0 instead and the code confidently returns 0, a total no slice can actually produce.
Watch for a related trap in the reset version too. The reset pushes the running total to zero, so if you record the best after resetting rather than before, an all-negative array again reports 0. Order of operations is doing quiet work in both versions.
One good habit closes this off. Whenever an algorithm seeds a best-so-far, ask what happens when no candidate ever beats the seed. If the seed itself is not a legal answer, you have a bug waiting.
A handful of small traps catch beginners on this problem. Read them once and you will dodge most of them.
Overflow deserves one honest sentence. On the original constraints the largest possible total is about one billion, which fits in a 32-bit integer with room to spare. Loosen those limits and a wider integer type becomes the right call.
Let us tie everything into one small program. Instead of printing a bare number, it reads a list of daily gains and losses and reports the best stretch in a sentence a human can read.
import java.util.Arrays;
public class BestStreakReport {
public static void report(int[] daily) {
if (daily == null || daily.length == 0) {
System.out.println("No days recorded.");
return;
}
int currentMax = daily[0];
int bestMax = daily[0];
int tentativeStart = 0;
int start = 0;
int end = 0;
for (int i = 1; i < daily.length; i++) {
if (daily[i] > currentMax + daily[i]) {
currentMax = daily[i];
tentativeStart = i;
} else {
currentMax += daily[i];
}
if (currentMax > bestMax) {
bestMax = currentMax;
start = tentativeStart;
end = i;
}
}
if (bestMax < 0) {
System.out.println("Every stretch lost money. Least bad day: day "
+ start + " with " + bestMax);
} else {
int[] best = Arrays.copyOfRange(daily, start, end + 1);
System.out.println("Best stretch: days " + start + " to " + end
+ " " + Arrays.toString(best) + ", total " + bestMax);
}
}
public static void main(String[] args) {
report(new int[] { -2, 1, -3, 4, -1, 2, 1 });
report(new int[] { -3, -1, -2 });
report(new int[] { 5 });
}
}Best stretch: days 3 to 6 [4, -1, 2, 1], total 6 Every stretch lost money. Least bad day: day 1 with -1 Best stretch: days 0 to 0 [5], total 5
Three inputs, three different shapes of answer. That is the point of the exercise.
Notice how little changed from the compact version. The scan is identical. We only added a guard at the top, three position variables inside, and friendlier messages at the bottom.
Real code usually looks like this. The clever part stays tiny, and the surrounding lines exist to make the result readable and safe. Getting comfortable with that split will help you far beyond this one question.
A: Somebody hands you an array of integers, and some may be negative. Your task is to find the contiguous slice with the largest sum and return that sum. The slice must be one unbroken run, so skipping elements is not allowed.
A: Kadane’s walks the array once. At each number it picks the larger of the number alone or the number added to the running total, then updates a best-so-far value. That gives O(n) time and O(1) space.
A: One single pass through the array gives O(n) time. Memory stays at O(1), because only two running values are needed no matter how long the array grows.
A: Seeding with zero breaks all-negative arrays, since no run can beat an empty total, so the code wrongly returns zero. Seeding with the first element keeps the result correct for every input.
A: The current run rises, falls, and sometimes restarts at a small value. A separate record keeps the best total you ever reached, so a later collapse cannot erase it.
A: Subarrays are contiguous, so their elements are neighbours. Subsequences may skip elements freely. Solving the subsequence version by accident gives 8 on our example instead of the correct 6.
A: The answer is the largest single value, because a slice must hold at least one element. On [-3, -1, -2] the result is -1, and correct seeding handles that with no special case.
A: Split the array at the middle, then take the largest of three candidates: the best slice on the left, the best on the right, and the best one crossing the centre. Total cost is O(n log n) time.
A: Yes. Track a tentative start and move it whenever the run restarts, then freeze the start and end positions whenever the record improves. That returns the actual slice alongside the sum.
A: Try a single element, an all-negative array, an all-positive array, one where the winning slice contains a negative, and one where the answer sits at the very end. Those five inputs catch nearly every bug here.
Let us wrap up what we covered. The Maximum Subarray problem in DSA looks small, and it is. Yet Kadane’s Algorithm hiding inside it is a real skill you will reuse constantly.
We started with brute force, trying every left edge against every right edge. Correct, honest, and slow at O(n squared). Its dry run showed six slices for three numbers, with a single element taking the crown.
Kadane’s came next, and we built it in two steps. First the literal version, which resets the running total the moment it turns negative. Then the compact form, which folds that reset into one comparison between restarting and extending. Both are the same idea, and both cost O(n) time with O(1) space.
Divide and conquer closed the set. Split at the middle, solve both halves, measure the best crossing slice, and take the largest of the three. Slower at O(n log n), and worth knowing because interviewers ask for it.
Along the way we handled the usual follow-ups. Reporting the slice itself takes three extra variables. All-negative arrays need no special case at all, provided you seed from the first element rather than zero.
So take the pattern, not just the answer. Walk once, carry a running total, drop it when it stops helping, and photograph your best moment as you pass it. Once that habit feels natural, a whole family of array and dynamic programming problems opens up.