最近点对问题
问题描述:
在平面上有点 p1(x1,y1),p2(x2,y2),……,pn(xn,yn)包含于集合S。找出S中距离(一般是欧氏距离)最近的点对。最近点对可能不止一对。
问题分析:
该问题比较简单直观的做法是穷举法,直接两层循环计算所有的点对之间的距离,并用map存储点对的距离,同时找到最小距离。最后再遍历一次map找到距离为最小值的所有点对。时间复杂度为T(n) = O(n^2)+O(n^2) = O(n^2),空间复杂度为O(n^2)。
该问题也可以用分治法解决,核心思想就是把集合划分为相等的两部分S1和S2,找到S1和S2中各自最小距离d1和d2,再考虑S1和S2各有一个点的情况。如下图所示:
令d = min(d1,d2)。如果最小距离点对是在S1和S2各有一个。则横轴必定在[-d,d]的矩形范围内,因此只需要找到S1和S2这个范围内的点,将矩形范围两边的点两两计算距离再比较一次就行。
其实还可以进一步减小比较的范围,对于矩形左侧范围内的任意一点,如果右侧存在满足条件的另一个点,则一定在宽为d长为2d的范围内。由d的定义可知该区域内最多只有五个点存在。若是将矩形范围内所有点按y值顺序排序,则只需要比较y值大于等于当前点的至多五个点(己方也可能有,判断的时候判断是否是异侧的点)。该阶段时间复杂度由O(n^2)减少到O(nlgn)。
算法描述:
1.输入集合S中的n个点的坐标{(x1,y1)...(xn,yn)};
2.判断n值
2.1 如果n==1,直接最大距离;
2.2 如果n==2,直接计算两个点的距离并返回两个点;
2.3 如果n>3,将集合划分为两个部分S1,S2(如果是根据x值排序好的,取中间序号为分界线就可);
2.3.1 将S1输入步骤1,计算S1中的最小距离d1以及对应点对;
2.3.2 将S2输入步骤1,计算S2中的最小距离d2以及对应点对;
2.3.3 计算d = min(d1,d2),依次考察集合S中的点p(x,y),如果(x<=xm+d 并且x>=xm-d),则将点p放入新的集合中;
2.3.4将集合中的点按y值排序,对于任一点(x,y),最多只需要比较序号之后五位即可。
3 返回min(d,d3);
java版本代码实现(含注释):
1 // 点对以及两个点的距离 2 public class Union { 3 double x1; 4 double y1; 5 double x2; 6 double y2; 7 double dis; 8 public Union(double x1, double y1, double x2, double y2, double dis) { 9 // 构造的时候按点的顺序构造 10 if (x1 > x2 || (x1 == x2 && y1 > y2)) { 11 this.x1 = x2; 12 this.y1 = y2; 13 this.x2 = x1; 14 this.y2 = y1; 15 this.dis = dis; 16 } 17 else { 18 this.x1 = x1; 19 this.y1 = y1; 20 this.x2 = x2; 21 this.y2 = y2; 22 this.dis = dis; 23 } 24 } 25 } 26 // 计算两个点的距离 27 public double distance(double x1, double y1, double x2, double y2) { 28 return Math.sqrt((x1-x2)*(x1-x2)+(y1-y2)*(y1-y2)); 29 } 30 // 计算points[l]-points[r]的集合中最小距离点对,输入的points已经按x升序排序 31 public List<Union> findMinimum (double[][] points, int l, int r) { 32 List<Union> res = new ArrayList<>(); 33 // 如果n == 1,返回最大距离 34 if (l == r) { 35 Union union = new Union(0,0,0,0,Double.MAX_VALUE); 36 res.add(union); 37 } 38 // 如果n == 2,返回点对和距离 39 else if (r == l+1) { 40 double dis = distance(points[l][0], points[l][1], points[r][0], points[r][1]); 41 Union union = new Union(points[l][0], points[l][1], points[r][0], points[r][1], dis); 42 res.add(union); 43 } 44 // 如果n>=3 45 else { 46 // 这里points已经按x升序排序,所以可以直接取中间的点分隔为S1,S2 47 int mid = (l+r)>>1; 48 // 获取S1和S2的最小距离点对 49 List<Union> list1 = findMinimum(points, l, mid); 50 List<Union> list2 = findMinimum(points, mid+1, r); 51 // 因为n>=3,所以list1和list2都必定不为空,get(0)一定有元素 52 double d = Math.min(list1.get(0).dis, list2.get(0).dis); 53 List<Union> list3 = new ArrayList<>(); 54 // 找到位于中间矩形区域内的点,加入到新集合中 55 List<double[]> list = new ArrayList<>(); 56 for (int i = l; i <= r; i++) { 57 if (Math.abs(points[i][0] - points[mid][0]) <= d) 58 list.add(new double[]{points[i][0],points[i][1],i}); 59 } 60 // 对新集合按y值排序 61 Collections.sort(list, (p1, p2) -> { 62 if (p1[1] > p2[1]) return 1; 63 else return -1; 64 }); 65 // 对集合的每个点,只要最多找序号之后五个点比较距离即可(同时要判断是否是异侧的点) 66 for (int i = 0; i < list.size()-1; i++) { 67 int k = i+6 > list.size() ? list.size() : i+6; 68 for (int j = i+1; j < k; j++) { 69 // 如果是异侧 70 if ((list.get(i)[2] <= mid && list.get(j)[2] > mid) || (list.get(i)[2] > mid && list.get(j)[2] <= mid)) { 71 double dis = distance(list.get(i)[0],list.get(i)[1],list.get(j)[0],list.get(j)[1]); 72 if (dis <= d) { 73 if (list3.size() == 0) 74 list3.add(new Union(list.get(i)[0],list.get(i)[1],list.get(j)[0],list.get(j)[1],dis)); 75 else { 76 if (dis < list3.get(0).dis) { 77 list3.clear(); 78 list3.add(new Union(list.get(i)[0],list.get(i)[1],list.get(j)[0],list.get(j)[1],dis)); 79 } else if (dis == list3.get(0).dis) { 80 list3.add(new Union(list.get(i)[0],list.get(i)[1],list.get(j)[0],list.get(j)[1],dis)); 81 } 82 } 83 } 84 } 85 } 86 } 87 // 判断list1,list2,list3的dis大小关系 88 if (list3.size() > 0 && list3.get(0).dis < d) 89 return list3; 90 else { 91 if (list1.get(0).dis == d) { 92 for (Union union : list1) 93 res.add(union); 94 } 95 if (list2.get(0).dis == d) { 96 for (Union union : list2) 97 res.add(union); 98 } 99 for (Union union : list3) 100 res.add(union); 101 } 102 } 103 return res; 104 }