[算法][几何计算]覆盖最多点固定半径圆问题 最多覆盖圆 圆的扫描线 详细解法 [POJ1981]
引题: 覆盖最多点固定半径圆问题
背景:
在二维平面中给定n个点,求半径为r的圆最多可以覆盖多少个点(1 <= n <= 300, 精度eps = 0.0001)
输入:
第一行两个数,整数n代表点的个数,浮点数r代表圆的半径。以下第i + 1行(1 <= i <= n) 输入第i个点的两个浮点数x、y,表示第i个点的坐标
输出:
一个整型,表示点的个数
样例:
Input:
4 1.0 1 1 2 2 2.7 2.7 3 2.5
Output:
3
相比于原题,追加了可以定义半径r,以及求出覆盖最多点圆中的一个合理圆心。
思路一:暴力枚举 时间复杂程度O(n3)
思路:
枚举两个点,求出这两个点在以r为半径的圆的圆弧上的两个圆心(这两个圆心以这两个点的连线为对称轴对称)(实际上每次循环只需要求出其中一个就行了,因为二重循环中总会枚举出这两个圆心,比如说在i = 1, j = 3和i = 3, j = 1的时候就各枚举出了这两个圆)
如何求出这个圆心?列数学方程就可以了,圆心在两个点的中垂线上,并且距离到两个点的距离都为r,勾股定理求出中点到圆心的距离,然后用atan2求出两个点的极角,减去π/2就是π减去中点与圆心形成向量的极角,然后就可以计算出圆心的坐标了。假设这两个点的极角为α = atan2(y2-y1, x2-x1),中点坐标为(x0,y0),圆心坐标为(xc,yc),圆心到中点的距离为d,则可以得到:
xc=x0 + dsinα
yc=y0 - dcosα
然后再求这个圆覆盖点的个数,总时间复杂程度为O(n3)
思路二:扫描线 时间复杂程度O(n2logn)
优点:时间复杂程度更低
缺点:理解起来比较复杂(其实有高中数学基础的话应该好理解)、写起来比较复杂。如果语言没有排序算法需要自己写一个排序(时间复杂程度取决于这个排序)
思路:
在以任意一个点P为圆心、r为半径的圆所覆盖的区域中取任意一点为圆心、r为半径的圆,这个圆一定过点P。
那么只需要让所有的点都为圆心、建半径为r的圆,求出被覆盖最多的区域,则取这个区域中的任意一点为圆心、半径r的圆为覆盖点最多的圆。
那么可以先枚举任意一点P,来探究这个点为圆心、半径为r的圆与其他点Q为圆心的圆覆盖的情况:
- 首先点P的整个圆都先被记录到被覆盖的区域中,并初始化覆盖点最多的圆的圆心为P、半径为r(如何记录进入和退出该区域在下面会描述)
- 其次就是枚举出任意一个点Q如果相交则有两个点(如果不相交,即两点距离大于2r,则跳过),连接点P与两个交点的话很明显这两条线可以划分为进入这个区域的线和离开这个区域的线。所以我们记录这两条线以及判断它是进入区域的线还是离开区域的线即可。
- 这里的一步的难点就在于如何判断这两条线是进入区域和离开区域。首先我们可以根据两个点的坐标和半径就出两个交点的坐标以及点P和两个交点形成向量的极角。
- 如下图中,我们可以得到向量PQ的极角α=atan2(yQ-yP,xQ-xP)(范围为[-π,π],图中α应该为336°-360°=-24°),通过余弦定理得出 两个圆心的连线与P点和交点的连线的夹角,设d为两个圆心的距离,则夹角 β = arccos(d / (2 * r))(范围为[0,π])
- 那么我们就可以求出P与两个交点形成向量的极角 γ=α±β,交点的坐标为(x+r*cosγ, y+r*sinγ)。我们可以假设极角小的线(γ=α-β)为进入区域的线(图中PF),极角大的线为退出区域的线(图中PE)
- 我们可以求出点P与所有圆交点的极角和极线,根据上方定义极角小的线为进入区域、极角大的线为退出区域,那么我们可以让所有的线根据极角进行从小到大排序。时间复杂程度为O(nlogn)
- 因为还要包括自己整个圆的区域,我们可以让整个圆的起始线的极角为极角的最小值,退出线为的极角为极角的最大值。根据atan2和arccos的范围可以知道,极角的范围为[-2π,2π],那么可以让起始线的极角等于-2π,退出线为2π
- 进一步细节:1. 要初始化每个圆所求的答案的圆心为自己的圆心,为什么?2. 由于包含自身肯定是分布在排序的起始和结尾(因为是最小和最大值),我是不是可以不用记录这两个情况直接让最终结果+1?
- 关于1,如果出现一定区域内一个圆只能覆盖一个点的情况,会因为你没有初始化圆心而找不到这个圆,从而可能会导致各种问题,如死循环
- 关于2,其实也可以不记录自己的区域、不额外记录这两个极角,只需要让最终答案+1即可;当然你也可以直接初始化答案为1,再相加相减。对于只求覆盖点的个数题来讲,这样做完全没有任何问题
- 另外如果有相切的情况,即两个交点的极角相等,那么我们应该让进入线排在退出线的前面
- 根据排序后的极线来进行判断覆盖点的个数,来对排序后的极线依次扫描:扫描进入线就让覆盖点个数+1,扫描到退出线就让覆盖点个数-1,取这个过程中的最大值就为对于可以覆盖到点P的圆中覆盖点最多的圆。
- 如果要求圆心,则这个圆心可以是这个极线所对应的交点(因为这个交点一定在这个被覆盖最多次数的这个区域中)
- 对所有的点依次求解,过程中覆盖点的个数的最大值即为答案。枚举所有有点次数为n,则所有的时间复杂程度为O(n2logn)
优化:
- 如果当前交点的个数/2小于目前记录覆盖数最大的值,那么可以直接跳过扫描线的过程。原因也很简单,因为这个区域中最大覆盖点的值就等于交点个数/2,这个区域的最大值都小于目前记录的最大值,则不可能会超过目前记录的最大值。
例子:
样例中为例,下图中,我们求在以D为圆心半径为1的圆中,与其他所有圆覆盖的情况。(当然除了D以外,还要分别枚举圆E、F、C的情况)
由于D与C的极角是负的,其他为正的,所以所有线中D与C的两个交点形成的向量的极角是最小的(DR与DS,分别为-π和-π/2),所以在左下方三象限这个区域中最大的覆盖数为2。
其次就是D与F和D与E的交点形成向量的极角,很明显顺序为DK、DM、DL、DN,其中DK、DM为进入线,DL和DN为退出线。依次扫描可以知道一二四象限这个区域的覆盖数为3。
取整个过程中覆盖数最大的值,也就是3。
上图中程序实际跑出的数据(不包括自身,其中1为进入线,-1为退出线):
cpp code:
1 #include <iostream> 2 #include <algorithm> 3 #include <cstdio> 4 #include <vector> 5 #include <utility> 6 #include <cmath> 7 8 #define cout std::cout 9 #define cin std::cin 10 #define endl std::endl 11 #define string std::string 12 13 const double eps = 1e-8; 14 const double pi = acos(-1.0); 15 16 int cmpDouble (double x) { 17 if (fabs(x) <= eps) return 0; 18 return x < 0 ? -1 : 1; 19 } 20 21 struct Point { 22 double x, y; 23 Point () : x(0.0), y(0.0) {} 24 Point (double x, double y) : x(x), y(y){} 25 26 Point operator + (const Point& p) const { 27 return {x + p.x, y + p.y}; 28 } 29 Point operator - (const Point& p) const { 30 return {x - p.x, y - p.y}; 31 } 32 Point operator * (const double& n) const { 33 return {x * n, y * n}; 34 } 35 Point operator / (const double& n) const { 36 return {x / n, y / n}; 37 } 38 double operator * (const Point& p) const { // dot product 39 return x * p.x + y * p.y; 40 } 41 double operator ^ (const Point& p) const { // cross product 42 return x * p.y - p.x * y; 43 } 44 bool operator == (const Point& p) const { 45 return (cmpDouble(x - p.x) == 0 && cmpDouble(y - p.y) == 0); 46 } 47 double distance (const Point& p) const { // sqrt( (x2 - x1)^2 + (y2 - y1)^2 ) 48 return sqrt( (x - p.x) * (x - p.x) + (y - p.y) * (y - p.y) ); 49 } 50 double angle () const { 51 return atan2(y, x); 52 } 53 Point transfer (const double& angle, const double& dist) const { 54 return {x + dist * cos(angle), y + dist * sin(angle)}; 55 } 56 }; 57 typedef Point Vector; 58 struct Circle { 59 Point center; 60 double radius; 61 Circle () : center(Point()), radius(0.0) {} 62 Circle (Point c, double r) : center(c), radius(r) {} 63 64 bool operator == (const Circle& c) const { 65 return center == c.center && cmpDouble(radius - c.radius) == 0; 66 } 67 bool apart (const Circle& c) const { 68 return cmpDouble(center.distance(c.center) - (radius + c.radius)) > 0; 69 } 70 }; 71 struct Scanline { 72 Point intersection; 73 double polarAngle; 74 int value; 75 Scanline () : intersection(Point()), polarAngle(0.0), value(0) {} 76 bool operator < (const Scanline& l) const { // The smaller angle or greater value comes first 77 if (cmpDouble(polarAngle - l.polarAngle) != 0) return polarAngle < l.polarAngle; 78 return value > l.value; 79 } 80 }; 81 class resolvent { 82 private: 83 static std::pair < Scanline, Scanline > getScanline (const Circle& a, const Circle& b) { 84 Scanline inLine, outLine; 85 inLine.value = 1, outLine.value = -1; 86 if (a == b) { // Initialization of the same circle 87 inLine.polarAngle = -2 * pi; 88 inLine.intersection = a.center; 89 outLine.polarAngle = 2 * pi; 90 outLine.intersection = a.center; 91 } else { // two intersections 92 Vector AB = b.center - a.center; 93 double dist = a.center.distance((b.center)); 94 double r = a.radius; 95 double angleAB = AB.angle(); 96 double angleAI = acos( dist / (2 * r) ); 97 98 inLine.polarAngle = angleAB - angleAI; 99 inLine.intersection = a.center.transfer(inLine.polarAngle, r); 100 101 outLine.polarAngle = angleAB + angleAI; 102 outLine.intersection = a.center.transfer(outLine.polarAngle, r); 103 } 104 return std::make_pair(inLine, outLine); 105 } 106 public: 107 static int solve (std::vector <Point>& p, const double r) { 108 int ans = 0; 109 for (auto i : p) { 110 int temNumber = 0; 111 Circle circleI = Circle(i, r); 112 std::vector <Scanline> lines; 113 114 for (auto j : p) { 115 Circle circleJ = Circle(j, r); 116 if ( circleI.apart(circleJ) ) continue; // Two circles apart 117 std::pair <Scanline, Scanline> pairLine = getScanline(circleI, circleJ); 118 lines.push_back(pairLine.first); 119 lines.push_back(pairLine.second); 120 } 121 if (lines.size() / 2 < ans) continue; // prune 122 std::sort(lines.begin(), lines.end()); 123 for (auto k : lines) { 124 temNumber += k.value; 125 if (temNumber > ans) { 126 ans = temNumber; 127 } 128 } 129 } 130 return ans; 131 } 132 }; 133 134 int main() { 135 int n; 136 double r; 137 Point p; 138 std::vector< Point > points; 139 cin >> n >> r; 140 for (int i = 0; i < n; i++) { 141 cin >> p.x >> p.y; 142 points.push_back(p); 143 } 144 cout << resolvent::solve(points, r) << endl; 145 return 0; 146 }
思路三:精度生成圆 (来自ZJF大佬 博客链接) 时间复杂程度:O(n * r2/eps2)
优点:更小地依赖点的个数
缺点:比较依赖于圆的半径、精度eps
思路:
以每个点为中心建立一个长为2r的正方形,从正方形的顶点开始每隔eps距离生成一个圆心,判断以该点为圆心半径为r的圆覆盖的点个数,选取其中最大覆盖点的个数的圆