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.

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

  1. Shuffle the points.
  2. Maintain the best distance and a hash grid with cell size .
  3. Insert points one at a time, checking only the 9 neighbouring cells.
  4. 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;
}

Comparison

MethodTimeNotes
Brute forcefine to
Sweep + setthe default
Divide and conquerclassic; also gives all near pairs — Bentley-Shamos
Randomised + grid expectedfastest, needs a good hash
KD-tree expectedreusable for other queries — 09-kd-tree-and-spatial-structures
Delaunaythe closest pair is a Delaunay edge — 20-delaunay-and-voronoi

Variants

VariantApproach
Farthest pairit is a hull diameter — rotating calipers
Closest pair in 3Dthe same sweep with a 2D active structure,
Closest pair under / rotate 45° (Manhattan) or sweep directly — 17-manhattan-geometry
Closest pair with insertionsthe randomised grid handles it naturally
All pairs within distance grid buckets of side , then check 9 cells
closest pairsa heap over the candidates found by the sweep
Closest pair between two setsthe 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

  1. Sort the points by once, up front.
  2. Split at the median into left half and right half .
  3. Recurse: let .
  4. The strip. Any closer pair must straddle the split line, so both points lie within distance of it. Collect those points, sorted by .
  5. 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.

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:

ProblemMerge step
Closest pairstrip of width , 7-point lemma
All nearest neighboursstrip plus per-point candidate lists
Maximal points / Pareto frontiermerge the two staircases
Dominance countingcount left points dominating right points during merge
Smallest enclosing circle (Shamos)superseded by Welzl
CDQ divide and conquerleft 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.