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;
}