Pebellslearn by building
0%
RoadmapsMatrixCoursesSandboxSign in
Computational Geometry  ›  Lesson

Closest Pair of Points

Closest Pair 32 minSplit the points by x, solve each half recursively, then check only a narrow strip around the dividing line for a pair closer than the best either half found alone — the strip trick turns an O(n^2) scan into O(n log n)
You're building a piece ofMeridian — service areas & coverage
This piece — closest_pair(): Finds the two closest stops in a batch in O(n log n), for spacing and merge checks.
Scenario Meridian wants to know, out of a whole batch of stops, which two sit closest together — useful for flagging a possible duplicate stop or checking two hubs aren't placed dangerously close.
Your task
Build closest_pair(points). Return [pair, distance], where pair is the two closest points (as a 2-element list) and distance is their actual straight-line distance, rounded to 4 decimal places. Points arrive as plain [x, y] lists, not tuples, so deduplicate with sorted({tuple(p) for p in points}) rather than sorted(set(points)) — a list can't go inside a set(). If 3 or fewer remain, brute-force every pair. Otherwise split at the middle into left and right halves, recurse on each, and keep whichever half's result has the smaller squared distance as best so far. Then build the strip: every point within best_distance of the dividing x, sorted by y. Compare each strip point only to the NEXT UP TO 7 points after it in that sorted strip; if any pair there beats best so far, it wins. Example: closest_pair([(0,0), (6,1), (3,4), (9,0), (1,1), (8,8), (5,5), (2,7)]) -> [[[0,0],[1,1]], 1.4142].

Closest Pair of Points

In short: finding the two closest points among n by checking every pair is O(n²). Divide and conquer brings it to O(n log n): sort by x, split in half, find the closest pair in each half recursively, and keep the smaller distance as best. The only question left is whether some pair straddles the dividing line and beats best — and that can only happen within a narrow strip of width 2 * best around the line. Sort that strip by y, and for each point in it, only the next 7 points in y-order could possibly be close enough to matter. That bound turns an O(n²) strip check into O(n), so the whole algorithm is O(n log n): one sort, then a recursion whose merge step is linear.

A vertical dividing line splitting a cluster of points into a left half and a right half, with a shaded strip of width 2 delta straddling the line, two points inside the strip connected by a dashed line labelled as the pair that beats both halves' separate best distances

Two stops near the depot

Eight of Meridian's stops are scattered around a depot. Which two sit closest together?

step through it
1import math
2 
3def dist2(a, b):
4 return (a[0] - b[0]) ** 2 + (a[1] - b[1]) ** 2
5 
6def brute_force_closest(points):
7 best = None
8 best_d = float("inf")
9 n = len(points)
10 for i in range(n):
11 for j in range(i + 1, n):
12 d = dist2(points[i], points[j])
13 if d < best_d:
14 best_d = d
15 best = (points[i], points[j])
16 return best, best_d
17 
18def closest_in_strip(strip_sorted_by_y, best_d, best_pair):
19 n = len(strip_sorted_by_y)
20 for i in range(n):
21 for j in range(i + 1, min(i + 8, n)):
22 d = dist2(strip_sorted_by_y[i], strip_sorted_by_y[j])
23 if d < best_d:
24 best_d = d
25 best_pair = (strip_sorted_by_y[i], strip_sorted_by_y[j])
26 return best_pair, best_d
27 
28def closest_pair_rec(points_sorted_by_x):
29 n = len(points_sorted_by_x)
30 if n <= 3:
31 return brute_force_closest(points_sorted_by_x)
32 mid = n // 2
33 mid_x = points_sorted_by_x[mid][0]
34 left, right = points_sorted_by_x[:mid], points_sorted_by_x[mid:]
35 pair_l, d_l = closest_pair_rec(left)
36 pair_r, d_r = closest_pair_rec(right)
37 best_pair, best_d = (pair_l, d_l) if d_l < d_r else (pair_r, d_r)
38 delta = math.sqrt(best_d)
39 strip = [p for p in points_sorted_by_x if abs(p[0] - mid_x) < delta]
40 strip.sort(key=lambda p: p[1])
41 return closest_in_strip(strip, best_d, best_pair)
42 
43def closest_pair(points):
44 pts = sorted(set(points))
45 pair, d2 = closest_pair_rec(pts)
46 return pair, round(math.sqrt(d2), 4)
47 
48stops = [(0, 0), (6, 1), (3, 4), (9, 0), (1, 1), (8, 8), (5, 5), (2, 7)]
49print(closest_pair(stops))

(0,0) and (1,1) turn out to be the closest pair — a distance of sqrt(2) ≈ 1.4142 — and the brute-force base case finds them directly, because with 8 points the recursion's two halves (4 points each) both bottom out at the n <= 3 base case almost immediately.

When the closest pair straddles the split

Split a batch of stops in half by x, and the closest pair isn't always inside one half — sometimes the real answer sits right across the dividing line, one point in each half:

python

(10, 10) is the last point in the left half, (11, 10) is the first point in the right half — neither half's own brute-force search ever compares them to each other, because they're never both on the same side of the split. Each half thinks its best pair is 14+ apart, but the true closest pair is right on the seam, a distance of exactly 1. Only the strip step, which looks at points from BOTH halves near the dividing line, can catch a pair like this.

Why the strip only needs 7 neighbours

It seems like the strip step should still cost O(n²) in the worst case — until you notice that every point in the strip already has a neighbour no closer than best (otherwise best would have been beaten already, by one half or the other). That spacing guarantee means at most 8 strip points can fit inside any delta x 2*delta box, so checking each point against the next 7 in y-order is enough; anyone further down the sorted strip is provably too far away in y alone to beat best.

What does this print?
best_d = 2   # squared distance, i.e. a real distance of sqrt(2)
delta = 2 ** 0.5
print(round(delta, 4))

Quick practice

What does this print?
def dist2(a, b):
    return (a[0] - b[0]) ** 2 + (a[1] - b[1]) ** 2

print(dist2((0, 0), (3, 4)) < dist2((0, 0), (1, 1)))
What does this print?
points = [(0, 0), (1, 1), (2, 7), (3, 4), (5, 5), (6, 1), (8, 8), (9, 0)]
mid = len(points) // 2
print(points[mid])

🎯 Your turn

Build closest_pair(points): return [pair, distance], the two closest points and their real distance rounded to 4 decimal places.

  • closest_pair([(0,0), (6,1), (3,4), (9,0), (1,1), (8,8), (5,5), (2,7)]) → [[(0,0), (1,1)], 1.4142]
  • closest_pair([(0,0), (10,10)]) → [[(0,0), (10,10)], 14.1421]

Hint: points arrives as plain [x, y] lists, so dedupe with sorted({tuple(p) for p in points}), not sorted(set(points)) — a list isn't hashable. Base case n <= 3: brute force. Else split at n // 2, recurse on each half, keep the smaller squared distance, then check the delta-wide strip around the split (sorted by y, each point against the next 7) for anything closer still. Convert the winning pair's two points back to lists before returning.

Press ▶ Run, try the Live App chips, then ✓ Check.

Practice — level up

0/5 solved

Solve each in its own editor — Run to try, Check to grade. Stuck? Reveal a Hint, or the full Solution + walkthrough.

Warm-upCompare distances without sqrt

Write squared_distance(a, b), returning (a[0]-b[0])**2 + (a[1]-b[1])**2 — the squared straight-line distance. With integer coordinates this is always an exact integer, so comparing two squared distances is just as valid as comparing the real distances, with no sqrt and no rounding. squared_distance((0,0), (3,4)) -> 25.

closest-pair
DrillThe O(n^2) baseline

Write brute_force_closest(points), checking every pair and returning [pair, squared_distance] for the closest one, using squared_distance. This is both the base case for small inputs AND the yardstick the divide-and-conquer version has to beat. brute_force_closest([(0,0), (6,1), (1,1), (9,0)]) -> [[(0,0), (1,1)], 2].

closest-pair
BuildSplit the sorted points in half

Write split_at_median(points_sorted_by_x), returning [left, right]: the first half and second half of the list, split at len // 2. Assume the input is already sorted by x. split_at_median([(0,0), (1,1), (2,7), (3,4), (5,5), (6,1), (8,8), (9,0)]) -> [[(0,0), (1,1), (2,7), (3,4)], [(5,5), (6,1), (8,8), (9,0)]].

divide-and-conquerclosest-pair
BossWhich points are close to the dividing line?

Write points_in_strip(points, mid_x, delta), returning the points whose x-coordinate is within delta of mid_x (strictly, abs(x - mid_x) < delta), sorted by y. points_in_strip([(0,0), (6,1), (3,4), (9,0), (1,1), (8,8), (5,5), (2,7)], 5, 3) -> [(6,1), (3,4), (5,5)].

closest-pair
CapstoneAre any two stops placed too close?

Write safe_spacing(points, min_allowed), returning True only if the TRUE closest-pair distance (not squared) across all of points is at least min_allowed. safe_spacing([(0,0), (6,1), (3,4), (9,0), (1,1), (8,8), (5,5), (2,7)], 1.0) -> True.

closest-pair
closest_pair.py
Call the function you wrote — just like your app's frontend would. Edit the inputs and hit Call.
closest_pair(points) → listThe closest pair of points and their straight-line distance, rounded to 4 decimal places.
try:
returned
Press Run to execute your code.
Press Check to run the tests for this lesson.