主要介绍分治做法
我们按照分治标准“分割-处理-上传”三步来做
首先将所有点按x值排序,分治函数solve(l , r)返回下标属于区间[l , r]的所有点的内部最近点对
显然当l == r 时不合法,因此我们上传一个+∞
假设对于solve(l , r)已经递归处理完了solve(l , mid) 和 solve(mid + 1 , r) (mid = (l + r) >> 1),那接下来就要处理下面这种情况
对于点对(A , B),A在[l , mid],且B在[mid + 1 , r]内
这种情况并不需要全部枚举左区间与右区间,我们有以下更优的做法
设dis = min(solve(l , mid) , solve(mid + 1 , r))
那么区间[l , r]的实际最优解ans肯定满足ans ≤ dis
令mid_x为下标mid的点的x值
那显然A,B的x均属于[mid_x - dis , mid_x + dis]
我们把这些点找出来再一一枚举即可
小优化:在[l , r]的子问题求解完后,我们可以把[l , r]的点按y排序,这样在最后处理时,对于当前枚举的i和j,假设i属于左边,j属于右边,若满足节点j的y - 节点i的y ≥ ans 那j以后的点对于i而言均不用再考虑了
由于根据以下性质
每处理一个左边的点时,右边最多只会有 6 个点被考虑到
于是对于每个点,最多处理6次,时间复杂度为O(6n)
结合分治的logn,实际时间复杂度为O(n log n)
跟上文完全一样,这里贴上代码
#include<iostream>
#include<cstdio>
#include<cmath>
#include<algorithm>
using namespace std;
const int N = 200010;
const double INF = 100000010;
struct nod
{
double x , y;
} a[N] ,tmp[N];
int n , T;
bool cmp(nod a , nod b)
{
return a.x < b.x;
}
double get_dist(nod a , nod b)
{
double dx = a.x - b.x , dy = a.y - b.y;
return sqrt(dx * dx + dy * dy);
}
double solve(int l , int r)
{
if(l == r) return INF;
int mid = (l + r) >> 1;
double mid_x = a[mid].x;
double ans = min(solve(l , mid) , solve(mid + 1 , r));
int i = l , j = mid + 1 , cnt = l - 1;
while(i <= mid && j <= r)
{
if(a[i].y < a[j].y) tmp[++cnt] = a[i++];
else tmp[++cnt] = a[j++];
}
while(i <= mid) tmp[++cnt] = a[i++];
while(j <= r) tmp[++cnt] = a[j++];
for(int i = l ; i <= r ; i++)
a[i] = tmp[i];
cnt = 0;
for(int i = l ; i <= r ; i++)
if(a[i].x >= mid_x - ans && a[i].x <= mid_x + ans)
tmp[++cnt] = a[i];
for(int i = 1 ; i <= cnt ; i++)
for(int j = i - 1 ; j && tmp[i].y - tmp[j].y <= ans ; j--)
ans = min(ans , get_dist(tmp[i] , tmp[j]));
return ans;
}
int main()
{
scanf("%d" , &n);
for(int i = 1 ; i <= n ; i++)
scanf("%lf%lf" , &a[i].x , &a[i].y);
sort(a + 1 , a + n + 1 , cmp);
printf("%.4lf\n" , solve(1 , n));
return 0;
}
附上上面性质的证明连接:点这里