计算几何学习笔记 [基础 V]


随机增量法

最小圆覆盖

题目:P1742 最小圆覆盖

在平面上给定 \(n\) 个点,求一个最小的圆,能覆盖所有点。要求输出半径和圆心坐标。

这里就需要“随机增量法”。类似数学归纳法。

假设我们已经解决了规模为 \(n-1\) 的问题,那么当问题规模变成 \(n\) 的时候,解决新加入的问题即可。

那么放到这个问题是什么呢?

先任取两个点,求出覆盖它们的最小圆。之后每次增加一个点,若点在圆里,那么不用管它;否则最小圆必过这个新点。为什么呢?

引理:新加入的点若不在圆内,那么最小圆必过新点。

证明就不必要了,可以感性理解。

但是这个办法不优

新加入的点不在圆内的概率在 \(\dfrac{3}{i}\),所以期望时间复杂度:

\[\sum_{i = 1}^n O(n^2) \times \dfrac{3}{i} = O(n^2) \]

那么怎么办?我们可以考虑再用增量法优化一次!

算法流程如下:

  • 假设已经有一个圆 \(C\) 覆盖前 \(i-1\) 个点。枚举第 \(i\) 个点,那么:
  • \(P_i\) 不在圆 \(C\) 内,那么最小圆一定过 \(P_i\)。那么:
    • 更新圆 \(C\)。令 \(P_i\) 为圆心。半径为 \(0\)
    • 再枚举第 \(j\) 个点。若 \(P_j\) 不在圆内,那么:
      • 更新圆 \(C\)。圆心设为 \(P_i\)\(P_j\) 的中点,半径设为 \(\dfrac{1}{2}dis(P_i,P_j)\)
      • 枚举第 \(k\) 个点:
        • \(P_k\) 不在圆内,那么 \((P_i,P_j,P_k)\) 三点可以确定一个圆了。更新圆 \(C\) 即可。

伪代码是这样的:

void work()
{
    Circle C;
    for(i = 1 to n)
    {
        if(p[i] 不在圆 C 内)
        {
            C = {p[i], 0};
            for(j = 1 to i - 1)
            {
                if(p[j] 不在圆 C 内)
                {
                    C = {p[i] 和 p[j] 的中点, 0.5 * dis(p[i], p[j])};
                    for(k = 1 to j - 1)
                    {
                        if(k 不在圆 C 内)
                        {
                            C = 过 {p[i], p[j], p[k]} 的圆;
                        }
                    }
                }
            }
        }
    }
}

时间复杂度呢?每层循环都只有不超过 \(\dfrac{3}{i}\) 的概率进入下一层。

\[\begin{aligned} T_1(n) & = \sum_{i = 1}^n T_2(i) \times \dfrac{3}{i} \\ T_2(n) & = \sum_{i = 1}^n T_3(i) \times \dfrac{3}{i} \\ T_3(n) & = O(n) \end{aligned} \]

所以最后 \(T_1(n) = T_2(n) = T_3(n) = O(n)\)

时间复杂度只有 \(O(n)\) 的优秀算法!

那么我们考虑一些算法细节:如何根据三点求一个圆?

首先,我们可以列个方程组:

\[\begin{cases} (x_1 - x)^2 + (y_1 - y)^2 = r^2 \\ (x_2 - x)^2 + (y_2 - y)^2 = r^2 \\ (x_3 - x)^2 + (y_3 - y)^2 = r^2 \\ \end{cases} \]

之后用 \((1) - (3)\)\((2) - (3)\)

\[\begin{cases} 2(x_1 - x_2)x + 2(y_1 - y_2)y = (x_1^2 - x_2^2) + (y_1^2 - y_2^2) \\ 2(x_1 - x_3)x + 2(y_1 - y_3)y = (x_1^2 - x_3^2) + (y_1^2 - y_3^2) \end{cases} \]

做个变量代换,写起来方便:

\[\begin{aligned} a_1 & = 2(x_1 - x_2) \\ b_1 & = 2(y_1 - y_2) \\ c_1 & = (x_1^2 - x_2^2) + (y_1^2 - y_2^2) \\ a_2 & = 2(x_1 - x_3) \\ b_2 & = 2(y_1 - y_3) \\ c_2 & = (x_1^2 - x_2^3) + (y_1^2 - y_3^2) \\ \end{aligned} \]

就有了:

\[\begin{cases} a_1 x + b_1 y = c_1 \\ a_2 x + b_2 y = c_2 \end{cases} \]

喜闻乐见的二元一次方程,解一下:

\[\begin{cases} x & = \dfrac{b_1 \times c_2 - b_2 \times c_1}{b_1 \times a_2 - b_2 \times a_1} \\ y & = \dfrac{a_2 \times c_1 - a_1 \times c_2}{a_2 \times b_1 - a_1 \times b_2} \end{cases} \]

解完了!

用计算几何解法也是可以的,因为我们其实很会“已知平面三点,作过三点的圆”的尺规作图做法,可以拿计算几何知识直接模拟。但是,在这里,解析几何也是可以的。

然后为什么是随机增量呢?

先随机打乱所有点,期望复杂度就是对的了。

代码:

#include 
#define DEBUG puts("QAQ")
#define openFile(a) freopen(a".in","r",stdin),freopen(a".out","w",stdout)
#define NOSYNC ios::sync_with_stdio(false); cin.tie(0); cout.tie(0)
#define FOR(i,j,k)  for(int (i) = (j); (i) <= (k); ++ (i))
#define RFOR(i,j,k) for(int (i) = (j); (i) >= (k); -- (i))
#define For(i,j,k)  for(int (i) = (j); (i) < (k); ++ (i))
#define RFor(i,j,k) for(int (i) = (j); (i) > (k); -- (i))
#define SC(...) scanf(__VA_ARGS__)
#define PR(...) printf(__VA_ARGS__)
#define N 100005
using namespace std;

typedef double db;
const double eps = 1e-12;
const double PI = acos(-1.0);
int dcmp(db x)
{
    if(fabs(x) < eps) return 0;
    return x > 0 ? 1 : -1;    
}

struct Vector {
    db x, y;
    Vector(db x = 0, db y = 0) : x(x), y(y) {}
    Vector operator + (const Vector &A) const {
        return Vector(x + A.x, y + A.y);
    }
    Vector operator - (const Vector &A) const {
        return Vector(x - A.x, y - A.y);
    }
    Vector operator * (const db &A) const {
        return Vector(x * A, y * A);
    }
    Vector operator / (const db &A) const {
        return Vector(x / A, y / A);
    }
    bool operator == (const Vector &A) const {
        return dcmp(x - A.x) == 0 && dcmp(y - A.y) == 0;
    }
};

typedef Vector Point;

db dis(Point A, Point B)
{
    return sqrt((A.x - B.x) * (A.x - B.x) + (A.y - B.y) * (A.y - B.y));
}

struct Circle {
    Point O;
    db r;
};

int n; Point p[N];

Point calc(Point A, Point B, Point C)
{
    db a1 = (A.x - B.x) * 2;
    db b1 = (A.y - B.y) * 2;
    db c1 = (A.x * A.x - B.x * B.x) + (A.y * A.y - B.y * B.y);
    db a2 = (A.x - C.x) * 2;
    db b2 = (A.y - C.y) * 2;
    db c2 = (A.x * A.x - C.x * C.x) + (A.y * A.y - C.y * C.y);
    db x = (b1 * c2 - b2 * c1) / (b1 * a2 - b2 * a1);
    db y = (a2 * c1 - a1 * c2) / (a2 * b1 - a1 * b2);
    return Point(x, y);
}

Circle C;

void work()
{
    C.O = p[1]; C.r = 0;
    FOR(i,2,n)
    {
        if(dcmp(dis(C.O, p[i]) - C.r) > 0)
        {
            C.O = p[i], C.r = 0;
            FOR(j,1,i - 1)
            {
                if(dcmp(dis(C.O, p[j]) - C.r) > 0)
                {
                    C.O = (p[i] + p[j]) / 2, C.r = dis(p[i], p[j]) / 2;
                    FOR(k,1,j - 1)
                    {
                        if(dcmp(dis(C.O, p[k]) - C.r) > 0)
                        {
                            C.O = calc(p[i], p[j], p[k]);
                            C.r = dis(C.O, p[i]);
                        }
                    }
                }
            }
        }
    }
}

int main()
{
	SC("%d", &n);
	FOR(i,1,n) SC("%lf%lf", &p[i].x, &p[i].y);
	random_shuffle(p + 1, p + n + 1);
	work();
	printf("%.10lf\n%.10lf %.10lf\n", C.r, C.O.x, C.O.y);
	return 0;
}