How many subarrays?

Let's start with a deceptively simple question. You have an array of 8 numbers — some positive, some negative — and you want the contiguous subarray with the largest sum.

Seems manageable, right? Just check all the subarrays and pick the winner.

But think about what “all subarrays” actually means. A subarray starting at index 0 could end at index 0, 1, 2, ... up to 7. That's 8 options. Starting at index 1 gives 7 more. Starting at index 2 gives 6. You're looking at 8 + 7 + 6 + ... + 1 = 36 subarrays for just 8 elements.

Drag the slider and watch that number grow:

n =8
subarrays to check:36
😎Easy

For 1,000 elements? That's roughly 500,000 subarrays. For 1,000,000? About 500 billion.

The brute force approach computes the sum of every single one. Let's see what that feels like with just 8 elements — and whether you can spot the answer before the machine finishes grinding through them all.

Find the maximum

Time to grind through all 36 subarrays. The explorer will step through every possible contiguous slice, compute its sum, and track the current best. Your job: watch for the moment the winner appears — and notice how many more subarrays you're forced to check even after the answer is already obvious. That's the cost of having no structure to exploit.

0
1
2
3
4
5
6
7

Tap a start cell, then tap an end cell to check a subarray

Brute force works, but...

Even with only 8 elements, that was a lot of checking. And you probably noticed something frustrating: most of the subarrays you examined were clearly bad choices. Subarrays that started or ended with a big negative number were never going to win. You might have wanted to skip them — but the brute force approach can't. It has to check every single possibility because it has no structure to exploit.

Here's the question: is there a way to solve this without looking at all 36 subarrays?

What if you could somehow split the array in half, solve each half separately, and then combine the answers? If the best subarray lives entirely in the left half, the left-half solution catches it. If it lives entirely in the right half, the right-half solution catches it.

Drag the divider below to see how different split points create different subproblems. An even split gives logarithmic depth — a lopsided split degenerates toward linear.

3
0
-1
1
2
2
5
3
-3
4
4
5
1
6
-2
7
split
[4]
Left: 4|Right: 4
Split ratio:4:4
Recursion depth:~3 levels
Perfectly balanced — logarithmic depth, minimum total work.

But there's a wrinkle. What if the best subarray crosses the middle? It starts somewhere in the left half and ends somewhere in the right half. Neither half-solution would find it alone. Tap each region below to see the three possibilities:

Something extra needs to happen at the boundary.

Let's look at the recursion tree for this idea. Something is missing from it — can you find what?

Zoom into the tree

Here's a recursion tree for the divide-and-conquer idea you just explored. Each node represents a subproblem — a slice of the array. The tree splits at the midpoint and recurses on both halves. But something is missing from this picture. The leaves and internal nodes handle the left-only and right-only cases, but where does the boundary-crossing case get resolved? Zoom in and find the gap.

4-38271-65
[0..7][0..3][0..1][0]4[1]-3[2..3][2]8[3]2[4..7][4..5][4]7[5]1[6..7][6]-6[7]5

Tap the pulsing node to zoom in and figure out its answer

Three ingredients

Look at what you just built. The recursion tree had three distinct things happening:

  1. The splitting part — You broke the array into two halves at the midpoint. No comparisons, no cleverness, just a clean cut down the middle.
  2. The recursive part — You handed each half off to the same algorithm and trusted it to come back with the right answer. This is the "just assume it works" step that makes recursion feel like magic.
  3. The stitching-back-together part — You had to handle the boundary-crossing case. Neither half could see subarrays that span the midpoint, so something extra was needed to catch them.

That third ingredient is where the real cleverness lives. The first two are almost mechanical — split at the midpoint, recurse on both halves. But the stitching step is the part that's different for every algorithm of this kind, and it's the part that determines whether the whole approach actually saves work. Toggle each candidate below to see what happens when you leave one out:

max(11, 8, 19) =19

All three candidates considered — the true maximum.

For maximum subarray, the stitching step scans outward from the midpoint in both directions, finding the best suffix of the left half and the best prefix of the right half. That's O(n) work at each level of recursion — you touch every element once per level, but never more. Since the array halves at each level, there are only O(log n) levels total. Multiply them together: O(n log n).

Look at how much that saves compared to brute force:

O(n^2) brute force
O(n log n) D&C

Let's make sure you can identify these three ingredients in the wild. Three code snippets. Three labels. One of them is an impostor that doesn't belong to this pattern at all.

Name the parts

Every divide-and-conquer algorithm has three ingredients: the split, the recursion, and the stitch. You've seen them in action — now see if you can identify them in code. Below are three code snippets. Two belong to the D&C pattern and one is an impostor that uses a different strategy entirely. Drag the correct label onto each snippet. If you can name the parts on sight, you'll recognize the pattern instantly in new problems.

1 / 3

Merge Sort

0function mergeSort(arr, lo, hi):
1 mid = (lo + hi) / 2
2 mergeSort(arr, lo, mid)
3 mergeSort(arr, mid+1, hi)
4 merge(arr, lo, mid, hi)

From insight to code

Recognizing the parts is one thing. Writing them is another.

The divide and conquer steps are formulaic — split at the midpoint, recurse on both halves. The combine step is the hard part. For maximum subarray, you need to find the best subarray that crosses the midpoint, then take the maximum of three candidates: left-only, right-only, and crossing.

Let's put that into code. You've seen the idea — now construct the implementation.

Write the combine

The divide step is formulaic — split at the midpoint. The conquer step is “trust the recursion.” The combine step is where the actual thinking lives: you need to find the best subarray that crosses the midpoint, then take the maximum of three candidates. This is the code that makes the whole algorithm work — and the part where most people make mistakes. Build it line by line.

Write maxCrossing — the piece you discovered was missing:

function maxCrossing(arr, lo, mid, hi) { // Expand LEFT from mid let leftSum = -Infinity, sum = 0 for (let ) sum += arr[i]; leftSum = max(leftSum, sum) // Expand RIGHT from mid+1 let rightSum = -Infinity; sum = 0 for (let ) sum += arr[i]; rightSum = max(rightSum, sum) return }

The pattern

You've just built your first divide-and-conquer solution from the ground up. Now let's name the three ingredients you used — because they show up in every algorithm of this type:

  1. Divide — Split the problem into smaller subproblems. Here, you cut the array at the midpoint.
  2. Conquer — Solve each subproblem recursively. Here, you found the max subarray in each half.
  3. Combine — Merge the sub-solutions into a solution for the original problem. Here, you handled the boundary-crossing case with an O(n) scan.

You started with a problem that seemed to require checking every possibility — O(n^2) work. By splitting the array in half, solving each half recursively, and doing O(n) work to combine the answers, you dropped the total to O(n log n).

That's the fundamental bet of divide-and-conquer: the combine step costs less than exhaustive search at each level, and splitting creates only O(log n) levels. When the bet pays off, you get dramatic speedups. When it doesn't — when the combine step itself is O(n^2) — you've just shuffled the same work around without saving anything.

The pattern to internalize: every time you see a problem that requires examining all pairs, or all subsets, or all orderings, ask yourself — can I split this in half, solve both halves, and stitch the answers together with less work than brute force? If the answer is yes, the logarithmic depth of the recursion tree is your payoff. If the answer is no, you need a different strategy entirely.

But this was just one flavor. The combine step here was a boundary scan — finding the best subarray that crosses the midpoint. In the next lesson, we'll see an algorithm where the combine step is the entire algorithm, and the divide step is almost embarrassingly simple. Same three-part structure, completely different balance of work.