平面最近点对(学习笔记)
先来推荐几篇博客.其实大家只要认真看了这些博客,就没必要看我的了QWQ.
平面最近点对问题:在同一平面内的所有点中,找出距离最近的两个点.
既然有模板题,还是双倍经验,那就直接来讲吧.首先暴力应该都想得到,就是\(n^2\)枚举所有点,算出所有点两两之间的距离,同时比较大小,记录答案.
考虑如何优化?核心思想是分治.至于为什么要用分治,介绍算法框架的时候自然而然就明白了.
设solve(V)为点集V中的最近点对的距离,那么,要求点集V的最近点对,我们先对其按x坐标从小到大排序,然后考虑选择一条垂直分割线,把V划分为点数尽可能相等的两部分V1和V2(说直白了就是选择点集V最中间两个点的中线),则solve(V)=min{solve(V1),solve(V2), dis(u,v)|u∈V1,v∈V2}.
如何理解这个式子?关于点集V中的最近点对,无非只有三种情况,两个点都在V1中,两个点都在V2中,或者一个点在V1中,另一个点在V2中.前两种情况就是分治的子问题,我们递归下去求解就好了.那么第三种情况如何高效求解呢?
令d=min{solve(V1),solve(V2)},那么我们可以只需要考虑已经选择好的垂直分割线往两端各延伸d的这一个区域,因为只有这个区域内的点对才可能有比d更小的距离(这里不懂的话,画个图很快就明白了)
solve(V){
选择一条垂直分割线,它将V分割为V1,V2;
d=min(solve(V1),solve(V2));
取出垂直分割线往两端延伸d的区域内的所有点;
求出区域内最近距离d’;
return min(d,d’);
}
到了这里,问题只剩最后一步,如何求出区域内最近距离d’,上面那一段文字只是优化了求解的时间复杂度,仍未提到具体地如何做?对于区域内的点,我们对其按y坐标排序;对于每个点,找到y坐标与它相差不超过d的点,计算距离d’,更新答案.
对y排序时,sort排序,总的时间复杂度\(nlog_nlog_n\):
int n,b[200005];
struct point{
double x,y;
}a[200005];
bool cmpx(point x,point y){return x.x<y.x;}
bool cmpy(int x,int y){return a[x].y<a[y].y;}
double dis(point x,point y){
return sqrt((x.x-y.x)*(x.x-y.x)+(x.y-y.y)*(x.y-y.y));
}
//l,r是点的下标,表示本次处理由第l~r个点构成的点集
double solve(int l,int r){
if(l==r)return 1e18;
if(l+1==r)return dis(a[l],a[r]);
//亲测:这一句可以不写,上面那个边界条件必须要写
int mid=(l+r)>>1;
//找到分割点,即点集中最中间的点
double d=min(solve(l,mid),solve(mid+1,r));
//分治
int k=0;
for(int i=l;i<=r;i++)
if(fabs(a[i].x-a[mid].x)<=d)b[++k]=i;
sort(b+1,b+k+1,cmpy);
for(int i=1;i<=k;i++)
for(int j=i+1;j<=k;j++)
if(a[b[j]].y-a[b[i]].y<=d)
d=min(d,dis(a[b[i]],a[b[j]]));
return d;
}
int main(){
n=read();
for(int i=1;i<=n;i++)
scanf("%lf%lf",&a[i].x,&a[i].y);
sort(a+1,a+n+1,cmpx);
printf("%.4lf\n",solve(1,n));
return 0;
}
优化:对y排序时,归并排序,总的时间复杂度\(nlog_n\)(亲测:快了100+ms)
int n;
struct point{
double x,y;
}a[200005],b[200005];
bool cmpx(point x,point y){return x.x<y.x;}
bool cmpy(point x,point y){return x.y<y.y;}
double dis(point x,point y){
return sqrt((x.x-y.x)*(x.x-y.x)+(x.y-y.y)*(x.y-y.y));
}
double solve(int l,int r){
if(l==r)return 1e18;
int mid=(l+r)>>1;
double midline=(a[mid].x+a[mid+1].x)/2;
//比较两份代码,就会发现,上面的分割线直接就是a[mid].x
//而这里的分割线是(a[mid].x+a[mid+1].x)/2
//显然此处更为严谨,这里以a[mid].x作为分割线会WA
double d=min(solve(l,mid),solve(mid+1,r));
int i=l,j=mid+1,k=i;
while(i<=mid&&j<=r){
if(cmpy(a[i],a[j]))b[k]=a[i],i++,k++;
else b[k]=a[j],j++,k++;
}
if(i<=mid)while(i<=mid)b[k]=a[i],i++,k++;
else while(j<=r)b[k]=a[j],j++,k++;
for(int i=l;i<=r;i++)a[i]=b[i];
int L=1,R=0;
for(int i=l;i<=r;i++)
if(fabs(a[i].x-midline)<=d){
while(L<=R&&(a[i].y-b[L].y)>=d)L++;
//优化时间复杂度,把一定不符合要求的点先去掉再进循环.
//这里没有的话就会TLE
for(int j=L;j<=R;j++)
d=min(d,dis(a[i],b[j]));
b[++R]=a[i];
}
return d;
}