计算几何--最小圆覆盖与最小球覆盖
参考书籍《算法竞赛入门到进阶》
最小圆覆盖问题:给定n个点的平面坐标,求一个半径最小的圆,把n个点全部包围,部分点在圆上。(两种算法:几何算法和模拟退火算法)
几何算法:(1)加第1个点P1。C1的圆心就是P1,半径为0。
(2)加第二个点P2。新的C2的圆心是线段P1P2的中心,半径为两点距离的一半。这一步操作是两点定圆。
(3)加第三个点P3。若P3在圆内或圆上,忽略;若不在,则以P3为圆心,重复(1)和(2),若还是不行则用三点定圆。
(4)加第四个点P4。若P4在圆内或圆上,忽略;若不在,则以P4为圆心,从前三个点中选择一个点重复(1)和(2)即两点定圆,若还是不行则选三个点进行三点定圆(一定有)。
(5)继续加入新的点。
复杂度分析:3层for循环,貌似是O(n3),但是当点的分布是随机的时候,可以通过概论计算得到实际复杂度接近O(n),代码中使用random_shuffle()函数实现。
题目链接:https://www.luogu.org/problem/P1742
代码如下:
1 #include <bits/stdc++.h>
2 using namespace std;
3 #define eps 1e-8
4 const int maxn = 100000+5;
5 int sgn(double x)
6 {
7 if (fabs(x)<eps) return 0;
8 else return x<0? -1:1;
9 }
10 struct Point
11 {
12 double x,y;
13 };
14 double Distance(Point A, Point B){return hypot(A.x-B.x,A.y-B.y);}
15 //求三角形abc的外接圆圆心
16 Point circle_center(const Point a, const Point b, const Point c)
17 {
18 Point center;
19 double a1 = b.x-a.x,b1=b.y-a.y,c1=(a1*a1+b1*b1)/2;
20 double a2 = c.x-a.x,b2=c.y-a.y,c2=(a2*a2+b2*b2)/2;
21 double d=a1*b2-a2*b1;
22 center.x=a.x+(c1*b2-c2*b1)/d;
23 center.y=a.y+(a1*c2-a2*c1)/d;
24 return center;
25 }
26 //求最小圆覆盖,返回圆心c和半径r:
27 void min_cover_circle(Point *p, int n,Point &c, double &r)
28 {
29 random_shuffle(p,p+n); //打乱所有点
30 c = p[0];r = 0; //第一个点
31 for (int i = 1; i < n; ++i) //剩下所有点
32 {
33 if (sgn(Distance(p[i],c)-r)>0) //pi在圆外部
34 {
35 c=p[i];r=0; //将圆心设为pi半径为0
36 for (int j = 0; j < i; ++j) //重新检查前面的点
37 {
38 if (sgn(Distance(p[j],c)-r)>0)//两点定圆
39 {
40 c.x=(p[i].x+p[j].x)/2;
41 c.y=(p[i].y+p[j].y)/2;
42 r=Distance(p[j],c);
43 for (int k = 0; k < j; ++k)
44 {
45 if (sgn(Distance(p[k],c)-r)>0)
46 {
47 c=circle_center(p[i],p[j],p[k]);
48 r=Distance(p[i],c);
49 }
50 }
51 }
52 }
53 }
54 }
55 }
56 int main(int argc, char const *argv[])
57 {
58 int n;
59 Point p[maxn];
60 Point c;double r;
61 while(~scanf("%d",&n)&&n)
62 {
63 for (int i = 0; i < n; ++i)
64 {
65 scanf("%lf%lf",&p[i].x,&p[i].y);
66 }
67 min_cover_circle(p,n,c,r);
68 printf("%.10lf\n%.10lf %.10lf\n",r,c.x,c.y);
69 }
70 return 0;
71 }
模拟退火算法:由于复杂度高于几何算法故不作详解,该算法在数据量小的情况下可用。
代码如下
1 void min_cover_circle(Point *p, int n,Point &c, double &r)
2 {
3 double T = 100.0; //初始温度
4 double delta = 0.98; //降温系数
5 c = p[0];
6 int pos;
7 while(T>eps) //eps终止温度
8 {
9 pos = 0; r = 0;
10 for (int i = 0; i <= n-1; ++i) //初始:p[0]是圆心,半径是0
11 {
12 if (Distance(c,p[i])>r)
13 {
14 r = Distance(c,p[i]); //距圆心最远的点看到
15 pos = i;
16 }
17 c.x += (p[pos].x-c.x)/r * T; //逼近最后的解
18 c.y += (p[pos].y-c.y)/r * T;
19 T *= delta;
20 }
21 }
22 }
替换上述同名函数。
最小球覆盖:给定n个点的三维坐标,求一个半径最小的球,把n个点全部包围进来。(同样是两种算法:几何算法和模拟退火算法)
几何算法:(1)加第1个点P1。C1的球心就是P1,半径为0。
(2)加第二个点P2。新的C2的球心是线段P1P2的中心,半径为两点距离的一半。
(3)加第三个点P3。三角形P1P2P3的外接球。球心为三角形的外心,半径为球心到某个点的距离。
(4)加第四个点P4。若四点共面,则为(3),若不共面,四面体P1P2P3P4一定可以唯一确定一个外接球。
(5)加第四个点P5。最小球必为其中四个点的外接球。
但是在该算法中,求三角形外心和四面体的外界球,方程很复杂,代码量很大,有没有简单点的方法呢?
答案是肯定的,使用模拟退火算法,根据上述推导过程我们知道最小覆盖球的球心一定与他距离最远的点有且最多有4个等距离的点。那么我们可以先随便假设一个点为球心,找到与他距离最远的点,并移动球心靠近该点,不断重复此过程,就能找到最小球覆盖的球心了。
题目链接:http://poj.org/problem?id=2069
代码如下:
1 #include <bits/stdc++.h>
2 using namespace std;
3 const double eps = 1e-8;
4 const int inf = 0x3f3f3f3f;
5 const double start_T = 1000;
6 struct point3d
7 {
8 double x,y,z;
9 }data[150];
10 int n;
11 double dis(point3d a, point3d b)
12 {
13 return sqrt((a.x-b.x)*(a.x-b.x)+(a.y-b.y)*(a.y-b.y)+(a.z-b.z)*(a.z-b.z));
14 }
15 double solve()
16 {
17 double step=start_T,ans=inf,mt;
18 point3d z;
19 z.x=z.y=z.z=0;
20 int s=0;
21 while(step>eps)
22 {
23 for (int i = 0; i < n; ++i)
24 {
25 if (dis(z,data[s])<dis(z,data[i])) s=i;
26 }
27 mt=dis(z,data[s]);
28 ans=min(ans,mt);
29 z.x+=(data[s].x-z.x)/start_T*step;
30 z.y+=(data[s].y-z.y)/start_T*step;
31 z.z+=(data[s].z-z.z)/start_T*step;
32 step*=0.97;
33 }
34 return ans;
35 }
36 int main(int argc, char const *argv[])
37 {
38 double ans;
39 cin>>n;
40 for (int i = 0; i < n; ++i)
41 scanf("%lf%lf%lf",&data[i].x,&data[i].y,&data[i].z);
42 ans=solve();
43 printf("%.8f\n",ans);
44 return 0;
45 }