Divide & Conquer8 min read

Closest Pair of Points

Find the two nearest cities on a map β€” without measuring every pair
naive:Slow Β· \(O(n^2)\)divide & conquer:Fast Β· \(O(n \log n)\)space:Linear Β· \(O(n)\)

Imagine you have a map with hundreds of cities scattered across it, and you want to find the two closest cities. The lazy approach: measure every possible pair. With 1,000 cities that's 500,000 measurements. With a million cities? Half a trillion. Way too slow.

The smart approach uses divide and conquer β€” splits the map in half, solves each half separately, then cleverly merges β€” and it finishes in \(O(n \log n)\) time, the same speed as sorting. Here's how.

The Naive Way β€” \(O(n^2)\)

Check every pair (i, j) and keep track of the shortest distance seen. Simple, correct, and painfully slow for large inputs. We need something smarter.

Divide & Conquer β€” the Big Idea

Step 1 β€” Divide: Sort all points by x-coordinate. Draw a vertical line through the median, splitting the set into a left half L and a right half R.

Step 2 β€” Conquer: Recursively find the closest pair in L (distance Ξ΄_L) and in R (distance Ξ΄_R). Let Ξ΄ = min(Ξ΄_L, Ξ΄_R). We now know the best answer within each half.

Step 3 β€” Combine: Can a pair that straddles the dividing line beat Ξ΄? Only if both points are within Ξ΄ of the line β€” otherwise their horizontal gap alone exceeds Ξ΄. This thin vertical strip is all we need to check.

The Magic of the Strip β€” Why Only 7 Neighbors?

Consider the strip: points within distance Ξ΄ of the dividing line, sorted by y-coordinate. Within any Ξ΄ Γ— 2Ξ΄ rectangle in the strip, at most 8 points can exist. Why? Each half of that rectangle is a Ξ΄ Γ— Ξ΄ square. Any two points in the same half are β‰₯ Ξ΄ apart (we already found the best in each half). You can fit at most 4 points in a Ξ΄ Γ— Ξ΄ square under that constraint. Two squares β†’ at most 8 points total.

So for each point in the strip, you only need to compare it against the next 7 points in y-order. That makes the strip scan \(O(n)\) β€” not \(O(n^2)\)!

Getting to \(O(n \log n)\)

If you sort the strip by y at each recursive level, the combine step costs \(O(n \log n)\), giving T(n) = 2T(n/2) + \(O(n \log n)\) β†’ \(O(n \log^2 n)\). But pre-sort all points by y once before recursing (just like merge sort maintains a sorted list during merging), and the strip scan becomes \(O(n)\). Now T(n) = 2T(n/2) + \(O(n)\) β†’ \(O(n \log n)\). Same trick as merge sort!

Note: The strip can only have 8 points in any Ξ΄ Γ— 2Ξ΄ box β€” that's the packing argument. It turns what looks like an \(O(n^2)\) strip scan into \(O(n)\). The geometry does the heavy lifting.

Closest Pair β€” Divide & Conquer

import math
def dist(p, q):
return math.hypot(p[0]-q[0], p[1]-q[1])
def strip_closest(strip, d):
best = d
strip.sort(key=lambda p: p[1])
for i in range(len(strip)):
j = i + 1
while j < len(strip) and strip[j][1] - strip[i][1] < best:
best = min(best, dist(strip[i], strip[j]))
j += 1
return best
def closest(pts):
pts.sort()
def rec(P):
if len(P) <= 3:
best = float('inf')
for i in range(len(P)):
for j in range(i+1, len(P)):
best = min(best, dist(P[i], P[j]))
return best
mid = len(P) // 2
midX = P[mid][0]
d = min(rec(P[:mid]), rec(P[mid:]))
strip = [p for p in P if abs(p[0] - midX) < d]
return min(d, strip_closest(strip, d))
return rec(pts)
points = [(2,3),(5,1),(1,6),(8,9),(3,7),(4,2)]
print(f"{closest(points):.4f}") # 2.2361
Output
2.2361

Complexity

🐒 Naive all-pairs check
Every pair of n points checked β€” unusable for large n
Quadratic \(O(n^2)\)
πŸ“ D&C without pre-sort
Re-sorting the strip by y at each level adds an extra log factor
Near-linearithmic \(O(n \log^2 n)\)
πŸš€ D&C with pre-sorted y list
Maintain global y-order across recursive calls; strip scan is \(O(n)\)
Linearithmic \(O(n \log n)\)
πŸ“¦ Space
Scratch arrays for sorted lists and strip candidates
Linear auxiliary \(O(n)\)
πŸ”Ž Strip scan per point
Packing argument: Ξ΄ Γ— 2Ξ΄ box holds ≀ 8 points
Constant \(O(1)\) β€” at most 7 neighbors

Quick check

Why do we only check points within Ξ΄ of the dividing line in the strip?

Continue reading