最小圆覆盖
平面上有多个点,求最小圆,能覆盖所有点。
随机增量法
先说说增量法。
增量法适用于一种算法,子问题往往是可以一层处理转移到原问题的。
\[T(n) = T(n-1)+g(n) \]对增量法进行随机化,可以避免由于数据构造造成的最复杂情况出现。
做法1 暴力枚举
目标是求最小圆,能包括所有点。
最暴力的做法就是枚举三个点作为圆边界上的点,三点定圆,取最大圆。
为什么最大圆就是呢?
如果存在点不在当前最大圆中,那么要囊括这个点,这个当前最大圆必须扩大自身,然后去包含它。(直观理解一下。。。三点定住了一个圆,圆必须加大半径,才可以*一下。。。)
这样复杂度即是O(n3)
做法2 随机增量
那么我们如果一个点一个点去加入圆中,或者说对圆进行扩张,就能省不少复杂度。
首先我们对点进行打乱。(随机增量)
首先选择第一个点作为初始圆O(半径为0)【也就是只有一个点的情况】。
然后三重循环。
-
枚举每一个点i,作为第一边界点。
若在当前圆内则无事发生,continue。(不进入循环2)
否则舍弃当前圆,将i点定为第一边界点,并和第一个点构成直径的圆作为当前圆O,并进入循环2。
-
枚举i之前的每个点j,作为第二边界点。
若在当前圆内则无事发生,continue。(不进入循环3)
否则j定为第二边界点,将i,j构成直径的圆作为当前圆O进入循环3。
-
枚举j之前的每个点k,作为第三边界点。
若在当前圆内则无事发生。
否则k定为第三边界点,三点定圆,此圆作为当前圆O。
以上就是算法步骤。
原理解释如下
考虑第一重循环,假设我们在i-1之前的点通过以上步骤完成了最小圆覆盖得到O,但i点却不在O内,那么我们就必须重新构造圆,而且,i一定在新O的边界上(做法1中的“圆扩张理论”),因此将i作为第一边界点。
既然圆已经更新了,那要寻找第二更新点
考虑第二重循环,如果点j不在新圆中,说明j一定在扩张后的圆中,并且为边界点,作为第二边界点。
第三重循环,找到不在新圆中的一个点,也一定为第三边界点,三点定一个圆。
由上,三重循环定圆。
直接上代码。
#include <bits/stdc++.h>
using namespace std;
#define eps 1e-12
struct point
{
double x, y;
}a[100010], o;
double r;
double dis(point a, point b)
{
return sqrt((a.x - b.x) * (a.x - b.x) + (a.y - b.y) * (a.y - b.y));
}
point get_o(point p1, point p2, point p3)
{
double a1, b1, c1, a2, b2, c2;
a1 = 2 * (p1.x - p2.x);
b1 = 2 * (p1.y - p2.y);
c1 = (p1.x * p1.x - p2.x * p2.x + p1.y * p1.y - p2.y * p2.y);
a2 = 2 * (p1.x - p3.x);
b2 = 2 * (p1.y - p3.y);
c2 = (p1.x * p1.x - p3.x * p3.x + p1.y * p1.y - p3.y * p3.y);
point ans;
ans.x = (b2 * c1 - b1 * c2) / (a1 * b2 - b1 * a2);
ans.y = (c1 * a2 - c2 * a1) / (b1 * a2 - a1 * b2);
return ans;
}
int main()
{
int n;
cin >> n;
for (int i = 1; i <= n; ++i)
cin >> a[i].x >> a[i].y;
random_shuffle(a + 1, a + n + 1);
o = a[1], r = 0;
for (int i = 2; i <= n; ++i)
{
if (dis(o, a[i]) - r < eps)
continue;
o.x = (a[1].x + a[i].x) / 2;
o.y = (a[1].y + a[i].y) / 2;
r = dis(a[1], a[i]) / 2;
for (int j = 1; j < i; ++j)
{
if (dis(o, a[j]) - r < eps)
continue;
o.x = (a[i].x + a[j].x) / 2;
o.y = (a[i].y + a[j].y) / 2;
r = dis(a[i], a[j]) / 2;
for (int k = 1; k < j; ++k)
{
if (dis(o, a[k]) - r < eps)
continue;
o = get_o(a[i], a[j], a[k]);
r = dis(o, a[i]);
}
}
}
printf("%.10f\n", r);
printf("%.10f %.10f", o.x, o.y);
return 0;
}
我在自己学习过程中其实产生了疑问。
Q:每次只是取点构圆,会不会出现取了后面的点三点构圆而使前面的点反而不在圆内了?
A:不会。从理论上讲,每次取的点都是不在当前圆内,都一定是边界点,所以三点构圆最大化。
而且假如存在四个点ABCD,当前边界点为D和C。取ACD构圆不包含B,取BCD构圆不包含A,那么取ABC三点构圆的时候就已经包含D了,和边界点选取不符合。如图。
因此,可以放心大胆用这种扩张方法。
Q:三点共圆求法
A:emmm,三个点列方程求解一下,咕咕咕咕。
Q:复杂度保证
A:第一重循环枚举i,第二重循环枚举j,由于可能在圆内,复杂度为
\[\sum_1^iO(i)g(i) \]其中g(i)为第三重循环的复杂度
每个点被取为边界点的可能性为\(\frac3i\)
\[g(i)=\frac3i \] \[T(n)=O(n) \]