2014.07.06 17:56
简介:
给定二维平面上n个点,求出距离最近的两个点的距离。
描述:
个人觉得这本书的第十章是最精华的,因为每种算法思想用一个典型问题来讲解。平面最近点对问题,自然是分治法的例子了。
暴力的O(n^2)枚举法自然不用多说,两层循环能得出结果,但效率上只能承担至多几千个点的计算。
分治法的基本思路是:如果我将n个点分为两批,那么距离最短的点要么同属一批(这样可以用子问题得到答案),要么各落在一批中(这样就必须合并子问题才能得到答案)。
分解为子问题的过程自然很简单,选出一个分界点mid,然后将n个点分为两部分,对每一部分递归求最近距离点对。
合并过程复杂一些,并且发生在递归调用结束之后。也就是:
递归左半部分的子问题 -> 递归右半部分的子问题 -> 合并结果得出最终答案
我们定义δ(delta)为左右两部分计算得出的最近距离的较小值。如果最终结果比δ还要小的话,那么它应该会出现在分界线mid处(否则的话,最终结果肯定早就在子问题中得到了,也就不可能比δ还小),而且是由离mid很近的一左一右两个点得到。因此以mid位置的点p[mid]为中心,δ为半径的正方形区域(不是圆形,因为不好计算)都是可能得到最小值的区域。
那么,我们从下标mid开始向两边扫描,找出所有落在这个可疑区域的点,于是可以得到左右两个点集P左和P右。然后将两个集合中的点一一比对就能得到结果了。
如果这样算法就完了,那么实际的程序运行甚至会比暴力算法还慢,因为数据如果一直是无序的,那就无法在该结束循环的时候提前结束循环。
排序1:在算法的一开始,就应该选取一个坐标轴(比如X)进行排序,这样我们在“那么,我们从下标mid开始向两边扫描”这句话的时候,就有了break的条件了。如果X坐标是有序的, 那么当某个p[i]的X坐标与p[mid]相差超过了δ,就可以停止扫描了。
排序2:“于是可以得到左右两个点集P左和P右”,尽管我们可以直接对两个候选集合中的点一一计算距离,得到最短距离。但这么做的话,效率仍然是暴力级别的。这些点全都落在了2δ为边长的正方形区域内,而且这个δ给人的印象很小很小,于是我们可能会认为P左与P右加起来点的数量很小,即使枚举也没关系的。在实际的递归调用里,一个看似小的浪费就会被放大到可观察的程度,然后你的代码就超时了。在排序1中X坐标已经有序了,那么可能变化的比较离谱的,只有Y坐标了。于是我们对候选集合中点按Y坐标进行排序。有序的数据,总能给break语句制造机会。这个排序让算法完整了。
为了确认算法的效率,我把代码修改了一句话之后提交到了ZOJ的题目“Quoit Design”运行,结果的确能AC掉。
实现:
1 // Divide-and-conquer solution for Closest Pair of Points problem. 2 // This piece of code is also accepted on Zhejiang University Online Judge 3 // Problem ID 2107. 4 // http://acm.zju.edu.cn/onlinejudge/showProblem.do?problemId=1107 5 #include <algorithm> 6 #include <cmath> 7 #include <cstdio> 8 #include <vector> 9 using namespace std; 10 11 struct Point { 12 double x; 13 double y; 14 Point(double _x = 0.0, double _y = 0.0): x(_x), y(_y) {} 15 }; 16 17 bool comparatorX(const Point &a, const Point &b) 18 { 19 if (a.x == b.x) { 20 return a.y < b.y; 21 } else { 22 return a.x < b.x; 23 } 24 } 25 26 bool comparatorY(const Point &a, const Point &b) 27 { 28 if (a.y == b.y) { 29 return a.x < b.x; 30 } else { 31 return a.y < b.y; 32 } 33 } 34 35 double dist(const Point &a, const Point &b) 36 { 37 return sqrt((a.x - b.x) * (a.x - b.x) + (a.y - b.y) * (a.y - b.y)); 38 } 39 40 double closestPairOfPointsRecursive(const vector<Point> &points, int left, 41 int right) 42 { 43 if (right - left + 1 == 2) { 44 return dist(points[left], points[left + 1]); 45 } else if (right - left + 1 == 3) { 46 return min(dist(points[left], points[left + 1]), 47 min(dist(points[left + 1], points[left + 2]), 48 dist(points[left], points[left + 2]))); 49 } 50 51 int mid = left + (right - left) / 2; 52 double delta = min(closestPairOfPointsRecursive(points, left, mid), 53 closestPairOfPointsRecursive(points, mid + 1, right)); 54 vector<Point> p; 55 56 int i, j; 57 i = mid; 58 while (i >= left && points[i].x > points[mid].x - delta) { 59 p.push_back(points[i]); 60 --i; 61 } 62 63 i = mid + 1; 64 while (i <= right && points[i].x < points[mid].x + delta) { 65 p.push_back(points[i]); 66 ++i; 67 } 68 69 int np; 70 double result = delta; 71 72 sort(p.begin(), p.end(), comparatorY); 73 np = (int)p.size(); 74 for (i = 0; i < np; ++i) { 75 for (j = i + 1; j < np; ++j) { 76 if (p[j].y - p[i].y > result) { 77 break; 78 } 79 result = min(result, dist(p[i], p[j])); 80 } 81 } 82 p.clear(); 83 84 return result; 85 } 86 87 double closestPairOfPoints(vector<Point> &points) 88 { 89 int n = points.size(); 90 sort(points.begin(), points.end(), comparatorX); 91 return closestPairOfPointsRecursive(points, 0, n - 1); 92 } 93 94 int main() 95 { 96 vector<Point> points; 97 int n; 98 int i; 99 100 while (scanf("%d", &n) == 1 && n > 0) { 101 points.resize(n); 102 for (i = 0; i < n; ++i) { 103 scanf("%lf%lf", &points[i].x, &points[i].y); 104 } 105 // printf("%.2f\n", closestPairOfPoints(points) / 2.0); 106 printf("%.2f\n", closestPairOfPoints(points)); 107 } 108 109 return 0; 110 }