[分治][暴力剪枝]Vijos P1012 平面最近点对 模板

题目链接
题目大意是,给你n个点,要求出这n个点中,距离最近的两个点的距离;
这题正解应该是分治;
先上一篇大牛的blog
计算几何 平面最近点对 nlogn分治算法 求平面中距离最近的两点
然后说一下我自己的理解和思路;
首先是分治的思想,对于n个点,我们可以把这些点按照x轴排序后,再分成两半,每一半中点的数量都不超过n/2,可能还有一部分点处于分割线上;
然后我们只要求出左边和右边n/2个点钟的最近点对的距离,记为 σ 1 σ 2
其中的较小值为 σ =min{ σ 1 , σ 2 };
但是此时还有一种情况,就是有两个点,一个处于左边,一个处于右边(或者处于分割线上);
现在考虑两个点各在一边的情况:
首先,我们可以肯定:

这两个点的范围一定包含在 [ m i d σ , m i d + σ ]
也就是说,这个范围以外的点,都不需要考虑,然后我们可以把需要考虑的点加入到一个数组中;

for(left to right){
    if(a[i]∈[mid−σ,mid+σ])
        把a[i]加入到数组中存储
}

看下图,然后我们想到,比如对于左边的一个点p,右边和他组成最小距离的点,一定处于右边这个 σ ( 2 σ ) 的矩形框内;

我们可以怎样利用这个性质呢;
可以先把 [ m i d σ , m i d + σ ] 内的点按照y坐标排序;
然后在枚举比较距离的时候,我们可以进行剪枝,在y坐标间隔超出 σ 时,直接break(因为后面的点间隔更大);
这样实现:

double solve(int left,int right){
    if(left+1==right) return dis(a[left],a[right]);
    double xl,xr,xm;
    int mid=(left+right)>>1;
    xl=solve(left,mid);
    xr=solve(mid,right);
    xm=min(xl,xr);
    int k=1;
    for(int i=left;i<=right;i++){
        if(fabs(a[mid].x-a[i].x)<=xm) b[k++]=a[i];
    }
    sort(b+1,b+k,cmp);
    for(int i=1;i<k;i++){
        for(int j=i+1;j<k && (b[j].y-b[i].y<xm);j++){
            xm=min(xm,dis(b[i],b[j]));
        }
    }
    return xm;
}

然后我们想到,其实我们按照y坐标完成排序之后,从下往上遍历左边点的时候,我们发现,其实右边的那个可能答案的矩形,是单调向上移动的, 所以也可能使用一个two-pointer来做这个扫描,具体的代码没有尝试了;

最终ac代码如下:

#include <cstdio>
#include <cmath>
#include <iostream>
#include <algorithm>
using namespace std;
const int maxx=1e5+6;

class point {
public:
    double x,y;
    point(double x=0,double y=0):x(x),y(y){}

    bool operator < (const point &u)const{
        if(x!=u.x) return x<u.x;
        return y<u.y;
    }   
};
point a[maxx],b[maxx];

inline double dis(point a,point b){ return sqrt((a.x-b.x)*(a.x-b.x)+(a.y-b.y)*(a.y-b.y)); }
inline bool cmp(point a,point b){return a.y<b.y;}
double solve(int left,int right){
    if(left+1==right) return dis(a[left],a[right]);
    double xl,xr,xm;
    int mid=(left+right)>>1;
    xl=solve(left,mid);
    xr=solve(mid,right);
    xm=min(xl,xr);
    int k=1;
    for(int i=left;i<=right;i++){
        if(fabs(a[mid].x-a[i].x)<=xm) b[k++]=a[i];
    }
    sort(b+1,b+k,cmp);
    for(int i=1;i<k;i++){
        for(int j=i+1;j<k && (b[j].y-b[i].y<xm);j++){
            xm=min(xm,dis(b[i],b[j]));
        }
    }
    return xm;
}

int main() {
    int n;
    double x,y;
    scanf("%d",&n);
    for(int i=1;i<=n;i++){
        scanf("%lf%lf",&x,&y);
        a[i]=point(x,y);
    }
    sort(a+1,a+1+n);
    double ans=solve(1,n);
    printf("%.3f\n",ans);
    return 0;
}

错点:
1.后面操作的是b数组,一开始写错了,写成a数组了;
2.在开启std::ios::sync_with_stdio(false);后,不能流读写和标准读写混合使用,否则oj读写有问题;

然后,因为这题的数据比较简单,也可以使用暴力+剪枝卡过,代码如下:
只要在前面的x已经大于ans的情况下,就可以不用遍历后面的的x了;剪枝效果很棒;

#include <cmath>
#include <cstdio>
#include <iostream>
#include <algorithm>
using namespace std;
const int maxx=1e5+6;

struct point{
    double x,y; 
};
point a[maxx];

inline bool cmp(point a,point b){
    if(a.x!=b.x) return a.x<b.x;
    else return a.y<b.y;
}
inline double f(point a,point b){
    return (a.x-b.x)*(a.x-b.x)+(a.y-b.y)*(a.y-b.y);
}
inline double g(double x){return x*x;}

int main() {
    std::ios::sync_with_stdio(false);
    int n,x,y;
    cin>>n;
    for(int i=1;i<=n;i++){
        cin>>x>>y;
        a[i].x=x;a[i].y=y;
    }
    sort(a+1,a+1+n,cmp);
    double mi=0x7fffffff;
    for(int i=1;i<=n;i++){
        for(int j=i+1;j<=n;j++){
            if(g(a[i].x-a[j].x)>=mi) break;
            mi=min(mi,f(a[i],a[j]));
        }
    }
    mi=sqrt(mi);
    printf("%.3f\n",mi);
    return 0;
}

然后,大神说还有一种乱搞的办法解这道题;
先对所有点按照x坐标排序之后,对于每一个点,不去遍历其他的所有点,而是只遍历它x轴附近的一些点,随便设定一个阈值,比如100个点,然后可能可以过;
但是这样的搞法容易被数据卡,所以还能优化;
想到,这样的解法如果有问题,肯定是有这样的点,比如点a和点b的x轴相距很近,但是y轴相聚很远,但是在x轴相聚比较远的地方,可能存在一个点c,与点a的y轴距离很近,如果这样的话,就会wa;
但是道高一尺魔高一丈,我们可以把整个平面旋转一个任意的角度,然后再按照上面的方法来一次,这样我们基本就可以确定答案是正确的了;(逃

猜你喜欢

转载自blog.csdn.net/qq_33982232/article/details/81485821
今日推荐