Imagine you're in a crowded city, and someone asks: “Which two people are standing closest together?” You'd have to measure the distance between every pair. With 20 people, that's 190 measurements. With 1,000 people? Nearly half a million. With a million? About 500 billion.
n points = n(n-1)/2 pairs
This is the closest pair problem. You have n points scattered on a 2D plane and you need to find the two that are nearest to each other. The brute force approach checks every possible pair — that's n * (n-1) / 2 distance calculations, which is O(n^2).
We've spent this whole module learning that divide-and-conquer can beat brute force. Split the problem, solve the halves, combine. But geometry makes things tricky. When you split an array, the halves are cleanly separated. When you split a plane of points, a pair might have one point on each side. The boundary is porous.
Let's start by feeling the pain of brute force. How many pairs do you actually need to check?
Let's feel the pain. You have a set of points scattered on a plane, and you need to find the closest pair. The brute force approach measures the distance between every possible pair. With each point you add, the number of pairs grows quadratically. Try to find the closest pair manually — and notice how many measurements you're making that obviously can't be the answer. Points on opposite corners, points that are clearly far apart — brute force checks them all anyway.
That was a lot of measuring. And most of those pairs weren't even close — points on opposite corners of the plane, obviously far apart, but brute force checks them anyway because it has no structure to exploit.
Here's the D&C idea: sort the points by x-coordinate, draw a vertical line through the median, and solve each half recursively. The closest pair in the left half gives you some distance. The closest pair in the right half gives you another distance. Take the smaller of the two — call it d.
Tap a pair line to compare distances
If the true closest pair lives entirely in one half, you've already found it. But what about pairs that straddle the boundary — one point on the left, one on the right? Those are the ones neither recursive call can see. And in the worst case, every left point could pair with every right point, which is O(n^2) again. The combine step seems to erase all the savings.
Unless the geometry itself limits how many cross-boundary pairs actually matter. Let's split some points and see where the trouble is.
Draw the vertical dividing line, solve each half recursively, and get a closest-pair distance d from each side. Take the smaller d. Now the hard part: what about pairs that straddle the boundary — one point on each side? In the worst case, every left point could pair with every right point, making the combine step O(n^2). Split the points below and see where the cross-boundary pairs actually fall. Is it really as bad as it looks?
d.Left half best: 12.0 — Right half best: 31.6
Notice what happened: after solving both halves, you got a distance d. Any cross-boundary pair that's closer than d must have both points within distance d of the dividing line — otherwise they'd be too far apart horizontally to beat d.
Tap a point to see its distance to the dividing line
That narrows the search to a strip of width 2d centered on the dividing line. Only the bright points above matter — everything dimmed out is too far from the boundary to participate in a cross-boundary pair closer than d.
But the strip could still contain lots of points. If n/2 points happen to cluster near the boundary, you'd still have O(n^2) pairs to check inside the strip. How do we avoid falling right back into brute force?
Here's where the geometry does something magical. Think about it: within each half, no two points are closer than d (because the recursive call already found the closest pair in each half). That means points on each side of the strip are spread out — they can't be packed too tightly.
How tightly, exactly? Consider a d x 2d rectangle centered on the dividing line. Divide it into 8 cells of size d/2 x d/2:
Tap a cell to see why it holds at most 1 point
The diagonal of each cell is (d/2) * sqrt(2), which is about 0.71d — less than d. That means any two points in the same cell would be closer than d, contradicting the recursive guarantee. So each cell holds at most one point. Eight cells, at most eight points.
For any given point in the strip, you only need to check its nearest 7 neighbors (sorted by y-coordinate) to find any cross-boundary pair closer than d. Not n neighbors. Not n/2 neighbors. Exactly 7, regardless of how many points there are.
Let's build that packing argument interactively — place points and discover the limit yourself.
Here's the claim: in a d x 2d rectangle straddling the dividing line, you can fit at most 8 points (given that no two points on the same side are closer than d). That means each point in the strip only needs to check 7 neighbors. Try to break this limit. Place points in the rectangle and see if you can squeeze more than 8 in while respecting the distance constraint. The geometry won't let you — and that constraint is what makes the entire algorithm work.
Points placed: 0 / 8
You just proved it: 8 cells, at most 8 points, so each point checks at most 7 neighbors. The recursive guarantee (no pair closer than d within each half) creates a packing constraint that limits how densely points can cluster near the boundary. That constraint is what makes the combine step efficient.
Tap each cell to place a point. The geometry won't let you fit a 9th — that's the whole proof.
Let's verify this by scanning through the strip point by point.
You've proved the packing limit: at most 8 points in each rectangle, so each point checks at most 7 neighbors. Now let's see this in action on a real point set. Sort the strip points by y-coordinate, then scan through them. For each point, you only look at the next 7 points in sorted order — if the closest cross-boundary pair exists, this window is guaranteed to find it. Watch the scan and verify that no closer pair slips through.
Point 1 / 1
At most how many neighbors does each strip point need to check?
Every point checks at most 7 neighbors. That's O(n) work total for the combine step — exactly what you need for the overall algorithm to hit O(n log n).
Combine step breakdown
The recurrence is the familiar T(n) = 2T(n/2) + O(n): two subproblems of half the size, plus O(n) combine work. Same as merge sort, same as maximum subarray. But the reason the combine step is O(n) is completely different. In merge sort, it's because interleaving two sorted arrays is linear. Here, it's because geometry limits the interaction density near the boundary.
This is a pattern worth remembering: when you're designing a D&C combine step and it looks like it might be O(n^2), ask whether the problem's structure limits how many elements can actually interact across the boundary. In geometric problems, distance constraints create packing limits. In sorted sequences, order constraints prune comparisons. The combine step is often cheaper than it appears — if you exploit the right structural property.
Let's connect the full algorithm to code.
Time to connect the full strategy to code. The algorithm has four distinct phases: sort by x-coordinate, recursively solve each half, build the strip of candidate points within distance d of the dividing line, and scan the strip checking only 7 neighbors per point. Each phase maps to a specific block of code. Build the implementation and watch how each phase contributes to the overall O(n log n) complexity.
0 / 4 matched
You've just built an O(n log n) algorithm for a problem where O(n^2) seemed unavoidable. The key wasn't a clever data structure or a tricky formula — it was the realization that the recursive guarantees themselves (no pair closer than d within each half) constrain the combine step.
This is the deepest lesson about divide-and-conquer design: the recursive solutions don't just answer subproblems. They create invariants that make the combine step tractable. In closest pair, the invariant is a distance lower bound. In merge sort, the invariant is sortedness. In maximum subarray, the invariant is knowing the best answer within each half.
We've now seen four different combine-step patterns: boundary scan (maximum subarray), full merge (merge sort), one-sided elimination (quickselect), and geometry-pruned strip (closest pair). Each does O(n) work, but for completely different structural reasons. Next, we'll see D&C applied not to a new problem, but to an old one — counting inversions by piggybacking on merge sort.