题目
给定二维坐标系中的 n n n个点,求其中最近的点对。
- 测试输入
5
1 2
-2 3
2 -1
0 3
-3 0 - 测试输出
1.414
分析
如果将点的距离两两计算出来需要 O ( n 2 ) O(n^2) O(n2)的时间复杂度,显然不是最优方案。先考虑一维数组情况,如果数组已经排好序,则只需要再遍历一遍,找到相邻值的最小距离即可,时间复杂度只是 O ( n l o g ( n ) ) O(nlog(n)) O(nlog(n)),但是这种方法不能推广到二维情况。因为按照一个维度排序后,相邻点的最小距离并不是所有距离中的最小值。
这里可以采用分治法进行改进,首先将点集按照横坐标排序,根据其中心点将点集一分为二,构成最小距离的两个点要么同时在左边的子点集,要么同时在右边的子点集,或者一个在左边一个在右边。前两种情况可以递归解决,这里主要考察第三种情况。
假设中心点坐标为 ( x mid , y mid ) (x_\textrm{mid},y_\textrm{mid}) (xmid,ymid),左边子点集最小距离为 δ left \delta_\textrm{left} δleft,而右边最小距离为 δ right \delta_\textrm{right} δright,那么点集的可能最小距离为 δ = min ( δ left , δ right ) \delta=\textrm{min}(\delta_\textrm{left},\delta_\textrm{right}) δ=min(δleft,δright)。对于第三种情况,最小距离的点只能出现在 x = x mid − δ x=x_\textrm{mid}-\delta x=xmid−δ和 x = x mid + δ x=x_\textrm{mid}+\delta x=xmid+δ的纵向带状区域中。对于左边带状区域中的一个点 ( x left , y right ) (x_\textrm{left},y_\textrm{right}) (xleft,yright),和它的距离可能小于 δ \delta δ的右边带状区域点只能出现在 y = y left − δ y=y_\textrm{left}-\delta y=yleft−δ和 y = y left + δ y=y_\textrm{left}+\delta y=yleft+δ横向带状区域中,这样就把待比较的右边点限制在两个正方形区域内了。这样对于每个左边点,只需要比较对应的两个正方形区域内的右边点就可以了,而不需要将纵向带状区域中所有点都比较一遍,从而大大降低了比较次数。具体做法是将右边带状区域中的点按照纵坐标排序,利用二分查找得到纵坐标最接近 y left y_\textrm{left} yleft的点,分别向上和向下找 δ \delta δ距离,从中检测是否还有与点 ( x left , y right ) (x_\textrm{left},y_\textrm{right}) (xleft,yright)距离小于 δ \delta δ的点。总体来看,利用这样分治的思想,算法的时间复杂度是 O ( n l o g ( n ) ) O(nlog(n)) O(nlog(n))。
进一步分析可知,两个正方形区域中最多只能有6个点,分别在各个顶点上。这是因为如果再有第7个点,那么必然在正方形内部区域中,使得与顶点的距离小于 δ \delta δ,产生矛盾。所以另一种简单做法是对每个左边点比较它上下各3个点即可。
代码
import java.util.*;
public class MinDistPair {
static double dist(double[] a, double[] b) {
double difx = a[0] - b[0];
double dify = a[1] - b[1];
return Math.sqrt(difx * difx + dify * dify);
}
static double distMin3(double[] a, double[] b, double[] c) {
double distab = dist(a, b);
double distac = dist(a, c);
double distbc = dist(b, c);
return Math.min(distab, Math.min(distac, distbc));
}
static double[][] getLeft(double[][] points, double bound, int mid, int lo) {
int left = mid;
double leftMin = points[mid][0] - bound;// 横坐标的左边界
while (left - 1 >= lo && points[left - 1][0] > leftMin) --left;
int len = mid - left;// 不包含mid点
double[][] leftPoints = new double[len][2];
System.arraycopy(points, left, leftPoints, 0, len);
return leftPoints;
}
static double[][] getRight(double[][] points, double bound, int mid, int hi) {
int right = mid;
double rightMax = points[mid][0] + bound;// 横坐标的右边界
while (right + 1 <= hi && points[right + 1][0] < rightMax) ++right;
int len = right - mid + 1;// 包含mid点
double[][] rightPoints = new double[len][2];
System.arraycopy(points, mid, rightPoints, 0, len);
return rightPoints;
}
static double minPair(double[][] points, int lo, int hi) {
if (lo == hi - 1)// 递归到只有两个点时直接计算距离
return dist(points[lo], points[hi]);
if (lo == hi - 2)// 递归到只有三个点时不能再分,也直接计算距离
return distMin3(points[lo], points[lo + 1], points[hi]);
int mid = lo + (hi - lo) / 2;
double curMin = Math.min(minPair(points, lo, mid), minPair(points, mid, hi));
double[][] left = getLeft(points, curMin, mid, lo);// 找和mid点横坐标距离小于curMin的左边点
double[][] right = getRight(points, curMin, mid, hi);// 找和mid点横坐标距离小于curMin的右边点
Arrays.sort(right, new Comparator<double[]>() {
@Override
public int compare(double[] o1, double[] o2) {
return (int) (o1[1] - o2[1]);// 按照纵坐标升序排列
}
});
for (double[] lp : left) {// 每个左边点在右边点中比较
int rmid = binarySearch(right, lp[1]);// 找到纵坐标距离最近的右边点
int i = rmid - 1;
double downMin = lp[1] - curMin;// 纵坐标的下边界
while (i >= 0 && i < right.length && right[i][1] > downMin) {
double tmpDist = dist(right[i], lp);
if (tmpDist < curMin) curMin = tmpDist;
--i;
}
int j = rmid;
double upMax = lp[1] + curMin;// 纵坐标的上边界
while (j >= 0 && j < right.length && right[j][1] < upMax) {
double tmpDist = dist(right[j], lp);
if (tmpDist < curMin) curMin = tmpDist;
++j;
}
}
return curMin;
}
static int binarySearch(double[][] points, double key) {
int i = 0;
int j = points.length - 1;
int mid;
while (i <= j) {
mid = i + (j - i) / 2;
if (key == points[mid][1]) {
return mid;
} else if (key < points[mid][1]) {
j = mid - 1;
} else {
i = mid + 1;
}
}
return i;
}
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int n = sc.nextInt();
double[][] points = new double[n][2];
for (int i = 0; i < n; i++) {
points[i][0] = sc.nextDouble();
points[i][1] = sc.nextDouble();
}
Arrays.sort(points, new Comparator<double[]>() {
@Override
public int compare(double[] o1, double[] o2) {
return (int) (o1[0] - o2[0]);// 按照横坐标升序排列
}
});
System.out.println(minPair(points, 0, n - 1));
}
}