最近点对算法分析Closest Pair of Points

Given n points in the plane, find a pair with smallest Euclidean distance between them.
题目很简单,就是在二维平面上寻找到距离最近的点对。

最直接的算法就是暴力寻找法。一个一个找呗,复杂度显然是O(n^2)。
    public static int minDis(Point[] Ps,int start,int end){
        //暴力找出最小距离
        if(end-start+1<2){
            return 0;
        }
        int min=calculateDistance(Ps[start],Ps[end]);
        int temp=min;       
        for(int i=start;i<end;i++){
            for(int j=i+1;j<=end;j++){
                temp=calculateDistance(Ps[i],Ps[j]);
                if(temp<=min){
                    min=temp;
                }
            }
        }       
        return min;
    }

还是那个问题,我们能做得更好吗?显然是可以的。下面我们将用分治法将它降到O(nlog(n));

运用分治法:要找到整个平面的最近点对,我们可以先找左右两边最近的点对:

这里写图片描述

那么这条中线如何确定呢?显然可以先按照x轴的坐标进行排序,这里排序的复杂度是O(nlog(n)),还是可以接受的。
因为我刚刚写了求无序数组的中位数的方法,复杂度可以降到线性,这里可以直接拿来用:

    private static void swap(Point[] Ps,int i,int j){
        Point temp=Ps[i];
        Ps[i]=Ps[j];
        Ps[j]=temp;
    }

    private static int findMiddle(Point[] Ps,int start,int end){
        //线性时间找出从start到end的元素x坐标的中位数
        if(Ps==null){
            return 0;
        }
        if(Ps.length==1){
            return Ps[0].x;
        }       
        int key=Ps[end].x;//基准      
        int k=start;
        for(int i=start;i<end;i++){
            if(Ps[i].x<=key){
                swap(Ps,k++,i);
            }
        }
        swap(Ps,k,end);

        //只找一边 T(n)=T(n/2)+O(n);
        int median=start+(end-start+1)/2;//中点
        if(k>median){
            return findMiddle(Ps,start,k-1);
        }else if(k<median){
            return findMiddle(Ps,k+1,end);
        }else{          
            return median;//返回中位数的下标,此时数组中元素的次序已被改变
        }
    }
    //以上完成后中位数在中间

此时返回左边的最小距离dl和右边的最小距离dr;设d为dl和dr的较小值。

找到中间数垂线后就结束了吗?显然还会存在两个点在两边的最近点。

这里写图片描述

那是不是要左右两边分别遍历一遍呢?那又太费时间了。可以看到,如果这个点的x坐标和中间数的差值大于d,那么就不用考虑这个点了。也就是说我们仅仅需要考虑如下中间的带状区域里的点:

这里写图片描述

然后对带状区域里面的点进行暴力寻找似乎就完成任务了,但是我们多想一步,就和超出带状区域的点我们舍弃一样,我们能否再横着画一条带状区域呢?这样就需要对带状区域里的点根据y坐标进行排序。  
我们可以推知如果存在这样的点对,那它一定在如下的d*(2d)的矩形框内。

这里写图片描述

而在这两个d*d的正方向框里,最多都只能含有4个点,如果多于4个将肯定会出现距离小于d的点对!因此对于带状区域里的每个点,最多只需要检查7个附近点就可以了,这一步的复杂度将是常量级的!

参考了其他大神的代码后,我发现为了避免对y排序,可以以中线为界向两边扫描,这样更具有技巧性。

最后贴上找最近点对的Java代码:
    public static int calculateDistance(Point p1,Point p2){
        return (p1.x-p2.x)*(p1.x-p2.x)+(p1.y-p2.y)*(p1.y-p2.y);
        //先返回距离的平方
    }

    public static int findmin(Point[] Ps,int start,int end){
        //找从start到end的元素中的最小距离对
        if(Ps.length==1){
            return 0;
        }
        if(Ps.length==2){
            return calculateDistance(Ps[0], Ps[1]);
        }

        if(Ps.length==3 || end-start+1<=3){
            //直接找最小距离对
            int temp1=calculateDistance(Ps[start],Ps[start+1]);
            int temp2=calculateDistance(Ps[start],Ps[start+2]);
            int temp3=calculateDistance(Ps[start+1],Ps[start+2]);
            temp1=temp1<temp2?temp1:temp2;
            temp1=temp1<temp3?temp1:temp3;          
            return temp1;
        }

        //根据x坐标找出中位数垂线
        int median=findMiddle(Ps,start,end);
        int dl=findmin(Ps,start,median);//左边
        int dr=findmin(Ps,median,end);//右边

        int d=dl<dr?dl:dr;

        int temp=d;
        for (int i = median; i>=start && Ps[median].x-Ps[i].x<d ; i--) {//在左边的带状区域里面
            for(int j=median+1;j<=end && Ps[j].x-Ps[median].x<d ;j++){//在右边的带状区域里面
                if(Math.abs(Ps[j].y-Ps[i].y)<d){//纵坐标之差小于d
                    temp=calculateDistance(Ps[i],Ps[j]);                                        
                    if(temp<=d)
                        d=temp;                                         
                }
            }           
        }       
        return d;
    }

猜你喜欢

转载自blog.csdn.net/weixin_37540865/article/details/78201399