洛谷P1429 平面最近点对(加强版)

61 阅读6分钟

一道非常经典的模板题P1429 平面最近点对(加强版)

当然还有加强加强版,虽然两者使用的正解方法是一样的P7883 平面最近点对(加强加强版)

题面:

P1429 平面最近点对(加强版)

题目背景

P7883 平面最近点对(加强加强版)

题目描述

给定平面上 nn 个点,找出其中的一对点的距离,使得在这 nn 个点的所有点对中,该距离为所有点对中最小的。

输入格式

第一行:nn ,保证 2n2000002\le n\le 200000

接下来 nn 行:每行两个整数:x yx\ y ,表示一个点的行坐标和列坐标,中间用一个空格隔开。

输出格式

仅一行,一个实数,表示最短距离,精确到小数点后面 44 位。

输入输出样例 #1

输入 #1

3
1 1
1 2
2 2

输出 #1

1.0000

说明/提示

数据保证 0x,y1090\le x,y\le 10^9

SolutionSolution

显然 O(n2)O(n^2) 枚举是自寻死路,所以我们要想个更聪明的方法。

这里给出的做法是分治,分而治之 (因为其他的做法我不会)

我们先考虑将所有点按 xx 的大小从小到大排序,而此时我们已经处理好了左右两个部分的点集中的最近点对,现在就要做到把这两个点集合并起来,并求出合并后的最近点对。

最终的答案有两种情况,一种是都来自于其中一个点集,一种是两个点集各有一个点组成点对。

对于第一种,我们只要取两个点集中更小的距离就可以了,真正的难点在于如何对跨点集的两个点进行求解。

现在思考我们通过前面对两个点集的处理已经得到了什么,是不是得到了当前两个点集分别的最近点对距离 dd ,那么如果我们要更新这个答案,就只能在比 dd 小的距离中找。

那么我们所求的两个点要跨越两个点集(如果有的话),它们之间的距离又小于 dd ,我们用一条垂直于 xx 轴的直线恰好将两个点集分到两边,设这条直线的方程为 x=mx=m ,则目标点的坐标只可能在 (md,m+d)(m-d,m+d) 之间,因为如果有一个点在这个范围之外,要向另一个点集中的点连线,显然它们的距离不小于 dd

所以我们要在这个带状区域中寻找最近点对。考虑一个位于这个带状区域的点 P1P_1 ,如果存在另一个同样在这个带状区域中的点 P2P_2 ,使得 P1P2<d|P_1P_2|<d ,则 yP1yP2|y_{P_1}-y_{P_2}| 。所以我们先将这个带状区域中的所有点按 yy 坐标排序,然后枚举每一个点 P1P_1 ,在它后面寻找每一个点 P2P_2 使得 yP1yP2|y_{P_1}-y_{P_2}| 成立,然后逐个计算它们之间的距离更新答案。

是不是听起来有点像暴力枚举?如果像这样往后面找点,不是 O(n2)O(n^2) 的复杂度吗?

别急,实际上我们对于每个要枚举的点,往后要找的点至多只有常数 77 个,这是整个算法实现的灵魂与关键。

下面给出大概证明:

由于我们已经按 yy 的值来排好序了,所以我们只需要找每个点上面的点,而且又要求 Δy<d|\Delta y|<d ,所以我们要找的预选点都在一个 2d×d2d\times d 的矩形内(以这个当前枚举到的点 P1P_1 为中心,向两边各延展 dd ,上面延展 dd)。

然后将这个矩形分成 88d2×d2\frac {d}{2} \times \frac {d}{2} 的小正方形,可以发现在任何一个小正方形内部,最远的两个点之间的距离在对角线的两边,为 22d<d\frac {\sqrt {2}}{2}d<d ,根据鸽笼原理,每个小正方形中至多只有一个点。来分类讨论一下。

设一个小正方形中有两个点。若这两个点来自同一个点集,则它们之间的距离 d<dd'<d ,与 dd 的最小性矛盾;若这两个点来自不同点集,则这就是我们要找的答案。

而为什么是至多 77 个?因为当前枚举到的点 PP 已经占据了其中至少一个格子,剩下就只有 81=78-1=7 个格子可以放点。

综上,我们可以通过递归分治来解决这个问题,递归边界为当前区间的点小于等于三个,这时候只要暴力枚举这些点之间的最小距离即可。

但是我们每次都要对每个点按 yy 坐标排序,如果每次都 sortsort 一次,则最终时间复杂度为 O(nlog2n)O(nlog^2n) ,这一步能不能优化一下呢?

考虑到我们每次都是由两个已经排好序的小的点集组成一个大的点集,然后将这个大的点集按 yy 坐标排序,这个过程和我们学过的归并排序是不是很类似?子区间有序,现在要求让组合成的大区间也有序,这个过程就可以按照归并排序的原理来进行维护。

具体的,当我们递归到边界时,此时直接 sortsort 排序,然后就返回了一个有序的子区间,然后根据归并排序的原理,将两个子区间合并后进行我们上面的跨点集找点的算法过程。

CodingCoding

#include <iostream>
#include <cstring>
#include <iomanip>
#include <cmath>
#include <vector>
#include <algorithm>
using namespace std;

#define ll long long
#define ull unsigned long long
#define debug(x) cout << #x << "=" << x << "\n";

int n;
const int maxn = 2e5 + 10;
struct point
{
    double x, y;
} p[maxn];

double calc_dis(double x1, double y1, double x2, double y2)
{
    return sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2));
}

void merge(int l, int mid, int r)
{
    static point temp[maxn];
    int i = l, j = mid, k = 0;
    while (i < mid && j < r)
    {
        if (p[i].y <= p[j].y)
            temp[k++] = p[i++];
        else
            temp[k++] = p[j++];
    }

    while (i < mid)
        temp[k++] = p[i++];
    while (j < r)
        temp[k++] = p[j++];

    for (int t = 0; t < k; t++)
        p[l + t] = temp[t];
}

double solve(int l, int r)
{
    if (r - l <= 3)
    {
        double res = 1e18;
        for (int i = l; i < r; i++)
        {
            for (int j = i + 1; j < r; j++)
            {
                res = min(res, calc_dis(p[i].x, p[i].y, p[j].x, p[j].y));
            }
        }

        sort(p + l, p + r, [](auto &a, auto &b)
             { return a.y < b.y; });

        return res;
    }

    int mid = ((l + r) >> 1);
    double mid_x = p[mid].x;
    double d = min(solve(l, mid), solve(mid, r));

    merge(l, mid, r);

    vector<point> strip;
    for (int i = l; i < r; i++)
    {
        if (fabs(p[i].x - mid_x) < d)
            strip.push_back(p[i]);
    }

    for (int i = 0; i < (int)strip.size(); i++)
    {
        for (int j = i + 1; j < (int)strip.size() && strip[j].y - strip[i].y < d; j++)
            d = min(d, calc_dis(strip[i].x, strip[i].y, strip[j].x, strip[j].y));
    }

    return d;
}

int main()
{
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    cin >> n;
    for (int i = 0; i < n; i++)
        cin >> p[i].x >> p[i].y;

    sort(p, p + n, [](auto &a, auto &b)
         { return a.x < b.x; });

    cout << fixed << setprecision(4) << solve(0, n);

    return 0;
}

整体时间复杂度:O(nlogn)O(nlogn)