Closest Pair
Find the two closest of points. Naive is ; three better methods exist.
Method 1: sweep line with a set — the one to write
, about 20 lines, and it reuses std::set.
ll closestPair(vector<P> p) {
sort(p.begin(), p.end()); // by x, then y
set<pair<ll, ll>> box; // active points, keyed by (y, x)
ll best = LLONG_MAX;
int left = 0;
for (int i = 0; i < (int)p.size(); i++) {
ll d = (ll)ceil(sqrt((long double)best));
while (left < i && p[i].x - p[left].x > d) { // drop points too far left
box.erase({p[left].y, p[left].x});
left++;
}
auto lo = box.lower_bound({p[i].y - d, LLONG_MIN});
auto hi = box.upper_bound({p[i].y + d, LLONG_MAX});
for (auto it = lo; it != hi; ++it) best = min(best, norm2(P{it->second, it->first} - p[i]));
box.insert({p[i].y, p[i].x});
}
return best; // SQUARED distance
}Why it is : the active set is a vertical strip of width , and within any window at most 6 points can be pairwise at distance (the same packing bound proved below for the divide-and-conquer strip). So each point examines neighbours, and d only shrinks, so the strip never widens.
Returning the squared distance keeps everything exact.
Proof:
closestPair's correctnessInvariant. After processing index ,
bestis the minimum squared distance among all pairs within , andboxholds exactly the points (those not yet provably too far in to matter) keyed by .
For the new point , any earlier point closer than must have both (hence still inbox, not yet dropped by thewhileloop, since points are processed in -order) and (hence in the queried -range ) — so thelower_bound/upper_boundrange contains every candidate that could possibly improvebest, and nothing closer is missed. Every candidate examined has its exact squared distance computed vianorm2, sobestonly ever holds a true achieved distance, never an underestimate — giving a tight final answer when the loop completes. Dropping points with is safe because such a point is already farther than the current best in alone, so it can never be part of a future closer pair (as only shrinks over time, this stays true for the rest of the sweep). (The -candidates-per-query bound is the packing argument proved for the strip below, applied to a window instead of .)
Method 2: divide and conquer
Split at the median , recurse, then check the strip of width around the split line, sorted by . The seven-point lemma bounds the strip scan at :
Merge the -orders during the recursion rather than re-sorting — see Bentley-Shamos below for the full implementation.
Method 3: randomised incremental — expected
- Shuffle the points.
- Maintain the best distance and a hash grid with cell size .
- Insert points one at a time, checking only the 9 neighbouring cells.
- When shrinks, rebuild the grid.
ll closestRandomized(vector<P> p) {
shuffle(p.begin(), p.end(), rng);
ll best = norm2(p[1] - p[0]);
unordered_map<ll, vector<int>> grid;
ll cell = max(1LL, (ll)sqrt((long double)best));
auto key = [&](ll x, ll y) { return x * 1000003LL + y; };
auto rebuild = [&](int upto) {
grid.clear();
cell = max(1LL, (ll)sqrt((long double)best));
for (int i = 0; i < upto; i++) grid[key(p[i].x / cell, p[i].y / cell)].push_back(i);
};
rebuild(2);
for (int i = 2; i < (int)p.size(); i++) {
ll gx = p[i].x / cell, gy = p[i].y / cell;
bool shrunk = false;
for (ll dx = -1; dx <= 1; dx++)
for (ll dy = -1; dy <= 1; dy++)
for (int j : grid[key(gx + dx, gy + dy)]) {
ll dd = norm2(p[i] - p[j]);
if (dd < best) { best = dd; shrunk = true; }
}
if (shrunk) rebuild(i + 1);
else grid[key(gx, gy)].push_back(i);
}
return best;
}Proof:
closestRandomizedis correct, and expectedCorrectness. The grid’s cell size is always (current), so two points within
bestof each other must lie in the same cell or adjacent cells (their coordinate difference in each axis is less than the cell size, so their cell indices differ by at most 1) — exactly the neighbourhood checked. Every pair actually compared uses the exactnorm2, sobestis always a true achieved distance; after processing all points, no closer pair exists, since every point, at the moment it was inserted, was checked against every already-inserted point close enough to matter (those in the current neighbourhood under the cell size valid at that time — and crucially, a rebuild re-buckets all previously inserted points under the new, smaller cell size immediately afterbestshrinks, so no stale bucketing from a now-too-large cell size persists).
Expected . The total cost is the sum over of (the scan) plus, when a rebuild is triggered, . A rebuild is triggered when point improves on the best distance found among the first points — equivalently, when is one of the (at most 2, typically) points defining the closest pair among a random prefix of size . By the same backward-analysis argument as Welzl’s algorithm, for a uniformly random insertion order this has probability , so the expected rebuild cost at step is , and summing over steps gives expected total.
Comparison
| Method | Time | Notes |
|---|---|---|
| Brute force | fine to | |
Sweep + set | the default | |
| Divide and conquer | classic; also gives all near pairs — Bentley-Shamos | |
| Randomised + grid | expected | fastest, needs a good hash |
| KD-tree | expected | reusable for other queries — 09-kd-tree-and-spatial-structures |
| Delaunay | the closest pair is a Delaunay edge — 20-delaunay-and-voronoi |
Variants
| Variant | Approach |
|---|---|
| Farthest pair | it is a hull diameter — rotating calipers |
| Closest pair in 3D | the same sweep with a 2D active structure, |
| Closest pair under / | rotate 45° (Manhattan) or sweep directly — 17-manhattan-geometry |
| Closest pair with insertions | the randomised grid handles it naturally |
| All pairs within distance | grid buckets of side , then check 9 cells |
| closest pairs | a heap over the candidates found by the sweep |
| Closest pair between two sets | the same sweep, ignoring same-set pairs |
The bucket trick
When the coordinate range is bounded and the answer is expected to be small, a plain grid of side answers “all pairs within ” in . It is the workhorse for collision detection and -body simulation, and often simpler than any of the above.
Tested: closestPair and closestRandomized both checked against brute-force on random point sets (including many duplicate and collinear points), and against each other.
Bentley-Shamos
The divide and conquer approach to the closest pair of points, in — the model for a whole family of “divide the plane, recurse, merge with a strip” geometric algorithms.
Closest pair by divide and conquer
- Sort the points by once, up front.
- Split at the median into left half and right half .
- Recurse: let .
- The strip. Any closer pair must straddle the split line, so both points lie within distance of it. Collect those points, sorted by .
- The seven-point lemma. For each point in the strip, only the next at most 7 points in -order can be within — because a rectangle can hold at most 8 points pairwise apart. So the strip scan is , not .
Keeping the -order by merging during the recursion (rather than re-sorting) is what keeps the merge step linear.
pair<ll, vector<P>> closestRec(const vector<P>& px) { // px sorted by x; returns {d^2, points sorted by y}
int n = px.size();
if (n <= 3) {
ll best = LLONG_MAX;
for (int i = 0; i < n; i++) for (int j = i + 1; j < n; j++) best = min(best, norm2(px[i] - px[j]));
vector<P> py = px;
sort(py.begin(), py.end(), [](P a, P b) { return a.y != b.y ? a.y < b.y : a.x < b.x; });
return {best, py};
}
int mid = n / 2;
ll midx = px[mid].x;
vector<P> lx(px.begin(), px.begin() + mid), rx(px.begin() + mid, px.end());
auto [dl, ly] = closestRec(lx);
auto [dr, ry] = closestRec(rx);
ll d = min(dl, dr);
vector<P> py(n);
merge(ly.begin(), ly.end(), ry.begin(), ry.end(), py.begin(),
[](P a, P b) { return a.y != b.y ? a.y < b.y : a.x < b.x; });
vector<P> strip;
for (auto& p : py) if ((p.x - midx) * (p.x - midx) < d) strip.push_back(p);
for (size_t i = 0; i < strip.size(); i++)
for (size_t j = i + 1; j < strip.size() && (strip[j].y - strip[i].y) * (strip[j].y - strip[i].y) < d; j++)
d = min(d, norm2(strip[i] - strip[j]));
return {d, py};
}
ll closestDivideConquer(vector<P> p) {
sort(p.begin(), p.end());
return closestRec(p).first;
}Complexity: : the recursion does work per level (the merge and the strip scan) over levels.
Proof: the strip bound, and the algorithm's correctness
The -per-point bound. Fix a point in the strip and consider the window of other strip points with . Split the -side points in this window (all within of the split line in , and within in by the window) by a grid of cells, cells covering a box. Any two points in the same cell are at most apart; since any two points of are known to be apart (as ), no cell can hold two -points — so at most 4 -points lie in the window. By the identical argument, at most 4 -points lie in the window. So at most other strip points can be within -distance of , and since the inner loop scans in increasing and stops once the -gap reaches , it examines (at most 8) candidates per point — for the whole strip. (This is the classical “seven/eight-point” packing bound; the exact best constant doesn’t matter for the asymptotics, only that it is .)
Why the inner loop’s early-exit bound (7, not 8) works via sorted : scanning points in increasing -order from a fixed strip point , the loop can stop as soon as , since every later point is even farther in and the rectangle argument above bounds how many points can be within in before exceeding it — giving amortized comparisons per point, for the whole strip.
Correctness of the divide step. Any pair closer than cannot be entirely within or entirely within (those are already covered by respectively), so it must straddle the split — meaning both points are within of the split line (if either were farther, the pair’s distance would already exceed ), i.e. both are in the collected strip. The strip scan, sorted by and bounded to comparisons per point by the lemma, finds this pair if it exists.
Tested against brute-force and against closestPair (Method 1) on random point sets including many duplicates, collinear configurations, and inputs designed to stress the strip (many points clustered near the median ).
The faster practical alternative
The randomised incremental method above is about the same code length, has no recursion, and is genuinely expected — the usual choice when is large and recursion overhead matters.
The Bentley-Shamos pattern
The same skeleton — split by a median line, recurse, merge across a narrow strip — solves many problems:
| Problem | Merge step |
|---|---|
| Closest pair | strip of width , 7-point lemma |
| All nearest neighbours | strip plus per-point candidate lists |
| Maximal points / Pareto frontier | merge the two staircases |
| Dominance counting | count left points dominating right points during merge |
| Smallest enclosing circle (Shamos) | superseded by Welzl |
| CDQ divide and conquer | left half’s updates applied to right half’s queries |
That last row is the important one for competitive programming: CDQ divide and conquer is exactly this pattern applied to offline queries with several dimensions, and it is the standard tool for 3D partial-order counting problems — see 02-cdq-divide-and-conquer.
Variants / Use Cases
- KD-Tree — the structure for repeated nearest-neighbour queries.
- Sweep line — the other general geometric technique.