这个问题真的很有意思,给定空间上的n个节点S={(xi,yi)},如何查找这n个点对中最近的点对的距离? 我们都知道两点间距离:((xi-xj)²+(yi-yj)²)1/2
那么如果使用暴力搜索,需要两两检测,需要花费O(N²).
我们可以采用分治法的思想.首先我们假设这些点都已经按照x坐标排序过,那么可以在中间画一条线,把点集分为Pl和Pr,那么最近的一对点要么都在Pl中,要么都在Pr中,要么分别在Pl和pr中,把这三个距离分别用dl,dr,dc表示.
显然我们可以递归第计算dl和dr,关键是计算dc.令δ=min(dl,dr),显然我们可以做出双道带,来缩小考虑的范围
对于在带中的,我们可以通过两层循环蛮力计算,但毕竟还是不好,最坏的情况是说有点都可能在这带状区域里.那么考虑对y坐标排序.如果pi和pj的y坐标差大于δ,那么可以直接break跳出内循环.如对于p3来说,我们只需要考虑p4和p5,之后的break就行了.
还有一点可优化的是,其实对于任意的点pi,最多有7个点需要被考虑.
如图所示,这是点最多的情况.因为如果在上图的情况中随便再添一个点,比如在左边,那么这个点距离左边其他四个点的距离肯定有小于δ的(最均匀矩形中心,距离小于δ).那么只要距离一个点小于δ.那么与之前的假设在Pl中最短的距离为δ矛盾了!所以我们只需考虑最坏情况,某个角上是pi,那么其余还有7个点要考虑,得证.
接下来就容易了,但是还有要注意的是,不能每次递归都去排序x,y坐标.可以保留两个表,一个x坐标排序的表,一个y坐标排序的表.这个算法是O(NlogN)的.
还是来实现吧:
import java.util.ArrayList;
import java.util.Set;
import java.util.TreeSet;
public class Distance {
static final int NUM=10;//使用穷举法的点数
public static void main(String[] args) {
Set<Point> testData = new TreeSet<Point>();
java.util.Random random = new java.util.Random();
for(int i = 0;i < 100000;i++){
int x = random.nextInt(100000);
int y = random.nextInt(100000);
testData.add(new Point(x, y));
}
Point [] points = new Point[testData.size()];
points = (Point[]) testData.toArray(points);
Point[] result=new Point[2];
long startTime=System.currentTimeMillis(); //获取开始时间
result=findpair(points);
long endTime=System.currentTimeMillis(); //获取结束时间
System.out.println("最近点: ("+result[0].getX()+","+result[0].getY()
+")--("+result[1].getX()+","+result[1].getY()+")");
System.out.println("距离为: "+distance(result[0], result[1]));
System.out.println("分治法运行时间:"+(endTime-startTime)+"ms");
long startTime2=System.currentTimeMillis(); //获取开始时间
result=bong(points);
long endTime2=System.currentTimeMillis(); //获取结束时间
System.out.println("穷举法运行时间:"+(endTime2-startTime2)+"ms");
}
//寻找最近点对
public static Point[] findpair(Point[] p){
Point[] result=new Point[2];
if(p.length<NUM)
return bong(p);
//开始画线了 求所有点在x坐标的中位数
int minX = (int) Double.POSITIVE_INFINITY;
int maxX = (int) Double.NEGATIVE_INFINITY;
for(int i = 0; i < p.length; i++){
if(p[i].getX() < minX)
minX = (int) p[i].getX();
if(p[i].getX() > maxX)
maxX = (int) p[i].getX();
}
int midX = (minX + maxX)/2;
//把以midx为界划分出的点分成两组放到两个表
ArrayList<Point> L1=new ArrayList<Point>();
ArrayList<Point> L2=new ArrayList<Point>();
for(int i = 0; i < p.length; i++){
if(p[i].getX() <= midX)
L1.add(p[i]);
if(p[i].getX() > midX)
L2.add(p[i]);
}
//按x坐标排序
Point [] p1 = new Point[L1.size()];
Point [] p2 = new Point[L2.size()];
L1.toArray(p1);
L2.toArray(p2);
mergeSort(p1, "x"); //按X坐标升序排列
mergeSort(p2, "x"); //按X坐标升序排列
//递归求p1,p2中最近的两个点
Point[] result1 = new Point[2];
result1 = findpair(p1);
Point[] result2 = new Point[2];
result2 = findpair(p2);
//求二者中的最小值
if (distance(result1[0], result1[1])<distance(result2[0], result2[1])) {
result=result1;
}else {
result=result2;
}
double distance=Math.min(distance(result1[0], result1[1]),distance(result2[0], result2[1]));
//开始划分带了 在两个子集中找哪些距离划分线小于d的保存
ArrayList<Point> L3 = new ArrayList<Point>();
for(int i = 0; i < p1.length; i++){
if(midX - p1[i].getX() < distance)
L3.add(p1[i]);
}
for(int i = 0; i < p2.length; i++){
if(p2[i].getX() - midX < distance){
L3.add(p2[i]);
}
}
//将得到的按照y升序排列
Point [] p3 = new Point [L3.size()];
L3.toArray(p3);
mergeSort(p3, "y");
//然后开始优化的穷举 即比较之后的7个点
if (p3.length<NUM) {
Point[] temp= bong(p3);
if (distance(temp[0], temp[1])<distance&&distance(temp[0], temp[1])!=0) {
result=temp;
}
}else {
for(int i=0;i<p3.length-7;i++){
double tempd;
for(int j=1;j<8;j++){
if (i+j>=p3.length) {
break;
}else {
tempd=distance(p3[i], p3[i+j]);
if (tempd<distance&tempd!=0) {
result[0]=p3[i];
result[1]=p3[i+j];
}
}
}
}
}
return result;
}
//归并排序
private static void mergeSort(Point[] p, String flag) {
Point[] result = new Point[p.length];
mergeSort(p, result, 0, p.length - 1, flag);
}
private static void mergeSort(Point[] a, Point [] result, int left, int right, String flag){
if(left < right){
int center = (left + right) >> 1;
//分治
mergeSort(a, result, left, center, flag);
mergeSort(a, result, center + 1, right, flag);
//合并
merge(a, result, left, center + 1, right, flag);
}
}
private static void merge(Point [] a, Point [] result, int leftPos, int rightPos, int rightEnd, String flag){
int leftEnd = rightPos - 1;
int numOfElements = rightEnd - leftPos + 1;
int tmpPos = leftPos; //游标变量, 另两个游标变量分别是leftPos 和 rightPos
while(leftPos <= leftEnd && rightPos <= rightEnd){
if(flag.equals("x")){
if(a[leftPos].getX() <= a[rightPos].getX())
result[tmpPos++] = a[leftPos++];
else
result[tmpPos++] = a[rightPos++];
}else if(flag.equals("y")){
if(a[leftPos].getY() <= a[rightPos].getY())
result[tmpPos++] = a[leftPos++];
else
result[tmpPos++] = a[rightPos++];
}else
throw new RuntimeException();
}
while(leftPos <= leftEnd)
result[tmpPos++] = a[leftPos++];
while(rightPos <= rightEnd)
result[tmpPos++] = a[rightPos++];
//将排好序的段落拷贝到原数组中
System.arraycopy(result, rightEnd-numOfElements+1, a, rightEnd-numOfElements+1, numOfElements);
}
//穷举法
private static Point[] bong(Point[] p){
Point[] result=new Point[2];
if (p.length<=1) {
result[0]=new Point(Double.MIN_VALUE, Double.MIN_VALUE);
result[1]=new Point(Double.MAX_VALUE, Double.MAX_VALUE);
return result;
}else{
double min=distance(p[0], p[1]);
int start=0;
int end=1;
for (int i = 0; i < p.length; i++) {
for (int j = i+1; j < p.length; j++) {
if (distance(p[i], p[j])<min&&distance(p[i], p[j])!=0) {
min=distance(p[i], p[j]);
start=i;
end=j;
}
}
}
result[0]=p[start];
result[1]=p[end];
return result;
}
}
//计算距离
private static double distance(Point p1,Point p2){
return Math.sqrt((p1.getX()-p2.getX())*(p1.getX()-p2.getX())+(p1.getY()-p2.getY())*(p1.getY()-p2.getY()));
}
//用一个类来表示点
static class Point implements Cloneable,Comparable<Point>{
double x,y;
public Point(double x, double y) {
super();
this.x = x;
this.y = y;
}
public double getX() {
return x;
}
public double getY() {
return y;
}
public int compareTo(Point o) {
if(x == o.getX() && y == o.getY())
return 0;
else
return 1;
}
public boolean equals(Object p) {
// TODO 自动生成的方法存根
if (this.x==((Point) p).getX()&&this.y==((Point) p).getY()) {
return true;
}else {
return false;
}
}
}
}
我们采用了随机数产生器随机产生点,并统计了分治法与纯粹暴力搜索的运行时间,差距相当明显(这个程序当规模特别大的时候还是会栈溢出,为啥)