最小圆覆盖

最小圆覆盖

平面上有多个点,求最小圆,能覆盖所有点。

随机增量法

先说说增量法。

增量法适用于一种算法,子问题往往是可以一层处理转移到原问题的。

\[T(n) = T(n-1)+g(n) \]

对增量法进行随机化,可以避免由于数据构造造成的最复杂情况出现。

做法1 暴力枚举

目标是求最小圆,能包括所有点。

最暴力的做法就是枚举三个点作为圆边界上的点,三点定圆,取最大圆。

为什么最大圆就是呢?

如果存在点不在当前最大圆中,那么要囊括这个点,这个当前最大圆必须扩大自身,然后去包含它。(直观理解一下。。。三点定住了一个圆,圆必须加大半径,才可以*一下。。。)

这样复杂度即是O(n3)

做法2 随机增量

那么我们如果一个点一个点去加入圆中,或者说对圆进行扩张,就能省不少复杂度。

首先我们对点进行打乱。(随机增量)

首先选择第一个点作为初始圆O(半径为0)【也就是只有一个点的情况】。

然后三重循环。

  1. 枚举每一个点i,作为第一边界点。

    若在当前圆内则无事发生,continue。(不进入循环2)

    否则舍弃当前圆,将i点定为第一边界点,并和第一个点构成直径的圆作为当前圆O,并进入循环2。

  2. 枚举i之前的每个点j,作为第二边界点。

    若在当前圆内则无事发生,continue。(不进入循环3)

    否则j定为第二边界点,将i,j构成直径的圆作为当前圆O进入循环3。

  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) \]

上一篇:【蓝桥杯单片机学习记录6】串行通信——小蜜蜂老师B站讲解


下一篇:Windows环境下RabbitMQ的启动和停止命令