数学总结 7(浅谈计算几何)

一、基础概念

点积

\(A(x_1,y_1), B(x_2,y_2)\),定义点积运算:

\[\vec{OA} \cdot \vec{OB} = x_1x_2 + y_1y_2 \]

\[\vec{OA} \cdot \vec{OB} = |OA| \times |OB| \times \cos \theta \]

根据定义 2:

  • \(\theta = 0\),有 \(\vec{OA} \cdot \vec{OB} = |OA| \times |OB|\)
  • \(\theta = 90\),有 \(\vec{OA} \cdot \vec{OB} = 0\)
  • \(\theta \in (0,90)\),有 \(\vec{OA} \cdot \vec{OB} \gt 0\)
  • \(\theta \in (90,180)\),有 \(\vec{OA} \cdot \vec{OB} \lt 0\)

一般可以用来判定垂直判定锐角或钝角

叉积

\[\vec{OA} \times \vec{OB} = x_1y_2 - x_2y_1 \]

\[\vec{OA} \times \vec{OB} = |OA| \times |OB| \times \sin \theta \]

其中 \(\theta\)有向角

由于是有向角,交换律需要变号

\[\vec{OA} \times \vec{OB} = - \vec{OB} \times \vec{OA} \]

叉积可以用来判定方向
具体地,若 \(\vec{OB}\)\(\vec{OA}\)逆时针方向,叉积为;否则为

叉积还可以算面积
叉积算的其实就是 \(\vec{OA}, \vec{OB}\) 围成的平行四边形面积,注意这也是有向面积

简单题

判断点在直线哪一侧

直接叉积看结果正负性。

  • \(\gt 0\) 则在逆时针方向。
  • \(=0\) 在直线上
  • \(\lt 0\) 在顺时针方向。

判断点是否在线段上

先判断它是否在直线上,用叉积。
然后判断 \(x \in [\min(x_1, x_2), \max(x_1, x_2)], y \in [\min(y_1, y_2), \max(y_1, y_2)]\)

求直线的解析式

这很简单吧,已知过 \((x_1, y_1), (x_2, y_2)\),列个方程就行了。
注意特判 \(k = \infty\) 的情况。

求两条直线的交点

先求解析式,再暴力联立直接算。
同样需要注意特判 \(k = \infty\) 的情况。

例 1:极角排序

极角取值范围一般定为 \((-\pi, \pi]\),指的是 \(\vec{OA}\)\(x\)正半轴成的角度,\(x\) 负半轴上定义为 \(-\pi\),同理 \(y\) 正半轴定义为 \(\frac{\pi}{2}\) 以此类推。
代码可以直接用 atan2(y, x) 算出 \((x,y)\) 的极角,long double 的话用 atan2l(y, x)注意先 \(y\)\(x\)
直接用 atan2 排会有精度误差,而且三角函数跑得慢。
考虑用叉积判方向,但是直接判可能会有环,例如三个向量两两之间成 \(120\) 度……
可以先“分界”之后再判定。具体地,将 \(x\) 轴以上的拉出来 sort,以下的拉出来 sort,再拼在一起即可。

当然,有另一种更直观的写法:按照坐标轴、四象限分成八个区域,先按区域排,区域内部按斜率排,代码在后文。

#include <bits/stdc++.h>
#define PII pair<int, int>
#define x first
#define y second
using namespace std;
const int N = 2e5 + 5;

int t, n, m;
PII a[N], b[N];

bool cmp(PII a, PII b) {    // 叉积
    long long opt = a.x * 1ll * b.y - b.x * 1ll * a.y;
    if (opt == 0) return abs(a.x) < abs(b.x);
    return (opt > 0);
}

int main() {
    scanf("%d", &t);
    for (int i = 1, x, y; i <= t; i++) {
        scanf("%d%d", &x, &y);
        if (y > 0 || (y == 0 && x <= 0)) a[++n] = {x, y};
        else b[++m] = {x, y};
    }
    sort(a + 1, a + 1 + n, cmp);
    sort(b + 1, b + 1 + m, cmp);
    for (int i = 1; i <= m; i++) printf("%d %d\n", b[i].x, b[i].y);
    for (int i = 1; i <= n; i++) printf("%d %d\n", a[i].x, a[i].y);
    return 0;
}

例 2:洛谷 P3476 [POI 2008] TRO-Triangles

要求所有三角形的面积之和……
看上去很吓人,先干一个海伦公式上去 \(O(n^3)\)

\(p=\frac{a+b+c}{2}\),则 \(S = \sqrt{p(p-a)(p-b)(p-c)}\)

然后考虑先确定三角形中的一个点。
不妨把三角形面积的贡献挂在这样的一个点上:\((-\pi, \pi]\) 的角度范围内可以覆盖另外两个点
然后我们只需要枚举每个点,将符合这个条件的点拎出来做极角排序……然后 \(S = \frac{1}{2} |x_1y_2 - x_2y_1|\)
绝对值很烦,但只要按极角从大到小枚举点这个式子就是正的,计算一下 \(\sum x, \sum y\) 就可以分配律求了。
时间复杂度 \(O(n^2 \log n)\),瓶颈在排序。

例 3:求多边形面积

给定 \(n\) 个点组成的凸多边形,求面积。

显然可以在凸多边形内部任选一点 \(O\),然后将凸多边形分为 \(n\) 个三角形求面积和。
三角形面积叉积一下就可以了。
然后考虑如何选这个点 \(O\),事实上只要是在这个平面内的点都可以选。
因为计算出来的面积是有向的,在凸多边形外部的会被反向抵消掉,手模发现是对的。

求简单多边形面积:边不交,但不一定是凸多边形。

类似地,也可以用上面的方法做,即多边形外的面积被抵消。

二、凸包

给定 \(n\) 个点,找一个面积最小的多边形覆盖所有点,多边形称为凸包

还有斜率逼近法、Jarvis 算法和 Graham 算法。
这里只记一下比较好写的 Andrew 算法了(虽然 Graham 也不错?)。

  • 先把点按双关键字排序。
  • 然后按顺序加入点,用单调栈维护凸包的“凸性”
  • 具体地,如果加入这个点之后“转向”了,那么就弹出栈顶继续判定,重复直到栈中只有一个点或符合条件,就把这个点扔进去。
  • 但这样只能求出一个下凸壳……假设现在栈顶的点是 \(P\)
  • 如何求出完整的凸包?反着枚举点的顺序,从 \(P\) 开始继续求一个上凸壳即可。

例 4:洛谷 P2742 [USACO5.1] 圈奶牛Fencing the Cows /【模板】二维凸包

如题,是板子。

#include <bits/stdc++.h>
#define PII pair<double, double>
#define x first
#define y second
using namespace std;
const int N = 2e5 + 5;

int n, tot;
PII a[N];

inline double cha(PII a, PII b) { return a.x * b.y - b.x * a.y; }
inline bool chk(PII a, PII b, PII c) {
    b.x -= a.x, b.y -= a.y, c.x -= a.x, c.y -= a.y;
    return cha(c, b) > 0;
}
PII stk[N]; int top;
void solve() {
    top = 2, stk[1] = a[1], stk[2] = a[2];
    for (int i = 3; i <= n; i++) {
        while (top >= 2 && chk(stk[top - 1], stk[top], a[i])) top--;
        stk[++top] = a[i];
    }
    int tt = top;
    stk[++top] = a[n - 1];
    for (int i = n - 2; i >= 1; i--) {
        while (top > tt && chk(stk[top - 1], stk[top], a[i])) top--;
        stk[++top] = a[i];
    }
    // for (int i = 1; i <= top; i++) cout << stk[i].x << ' ' << stk[i].y << endl;
}

inline double dis(PII a, PII b) { return sqrt((b.x - a.x) * (b.x - a.x) + (b.y - a.y) * (b.y - a.y)); }

int main() {
    scanf("%d", &n);
    for (int i = 1; i <= n; i++) scanf("%lf%lf", &a[i].x, &a[i].y);
    if (n == 1) return puts("0.00"), 0;
    sort(a + 1, a + 1 + n);
    solve();
    double ans = 0;
    for (int i = 1; i < top; i++) ans += dis(stk[i], stk[i + 1]);
    printf("%.2lf\n", ans);
    return 0;
}

例 5:洛谷 P3829 [SHOI2012] 信用卡凸包

直接把算法写在题目里了……
不难发现圆弧部分其实就是一整个圆的周长,所以只需要计算去掉圆半径之后的凸包周长即可。
那么只要求出四个点,这是好做的,长宽乘个三角函数再偏移到中心点即可。
于是求一下凸包周长再加上圆周长就是答案。

例 6:判断点是否在凸包内

给定一个凸包,多次询问判断 \((x, y)\) 是否在凸包内,\(n, q \leq 10^5\)

暴力遍历凸包每条边,看这个点是否都在它的逆时针方向(叉积),\(O(nq)\)
这个想法很笨……因为我们多判断了很多无用边,没有利用凸包的单调性
可以在凸包上二分,先将 \(1\) 号点和其它每一个点连线,相邻两条线和凸包的边会构成若干三角形,二分点在哪个三角形内,查询它是否在对边的逆时针方向即可,总复杂度 \(O(q \log n)\)
\(1\) 号点左右的边可能得特判一下边界以外,但这是细节。
代码在后文【战争】这道题。

例 7:洛谷 P4049 [JSOI2007] 合金

多么搞笑的题目啊。……
Corner Case 一大堆,卡了两个小时。
比较 tricky,但首先由于 \(a+b+c=1\),只需要知道 \((a,b)\) 就能确定唯一的 \(c\),不妨把一个材料或合金看作 \((a,b)\) 这一个二维平面上的点。
然后考虑“合成”这件事,如果要选出一些 \(A\) 集合中的点合成 \(B\) 集合中的一个点 \(P\),则这些点围成的凸包要包含 \(P\) 点。
显然,凸包内的点可以任意取,所以只要保证 \(P\) 在内即可。

为什么“显然”?
考虑从 \(p\) 点向凸包上所有点连边,形成若干个向量
这些向量通过分配 \([0,1]\) 权值数乘、相加只能构成凸包内的点集。
类似于线性基。

或者……一个看起来更对的证明是:

考虑 \(x\) 坐标最大和最小限制了 \(x\) 的上下界,\(y\) 坐标同理。
上下界可以刻画成两条直线,夹着这个凸包。
所以对这个凸包做旋转卡壳(在后文有讲解)一直在两条直线间的就是可以拼出的部分。
显然,这些部分就是凸包本身。


然后就是从 \(A\) 中取最少的点包含 \(B\) 中所有点。
最开始的思路是对于 \(A\)\((x,y)\) 点对,只要 \(B\) 中所有点都在直线 \((x,y)\) 的同一侧即可。
但是会出现虽然围成了凸包,依然无法包含所有点的情况。
所以考虑钦定一个方向:例如所有点都在直线的逆时针方向,叉积为正。
并且还要保证如果叉积为零,点必须在线段上才能覆盖
然后 \((x,y)\) 就可以选择,否则不能选择。

接下来是选取最少的 \((x,y)\) 并构成一个凸包,建立图论模型就是形成环
类似于 Floyd 求无向图最小环,这题求有向图最小环即可。

动态凸包

大致是拿平衡树找到插入的位置,然后向左向右暴力扔出在凸包内的点。
由于每个点只会进一次出一次,复杂度是 \(O(n \log n)\)

三、旋转卡壳

给定平面上 \(n\) 个点,求最远的两个点之间的距离以及这两个点。

首先贪心,显然地我们只需要在凸包上决策选哪两个点即可。
然后事情交给旋转卡壳去做。
顾名思义,就是用两根直线恰好卡在凸包的两侧,和凸包的边不交(可以重合),然后旋转凸包调整直径。这个过程中两条直线之间距离最大值就是凸包的直径。

对踵点:显然这两条直线会卡在凸包的两个点上,此时称这两个点为一组对踵点对

在凸包旋转的过程中,当对踵点对连线与直线垂直,两直线间的距离就是这两个点的距离,否则小于这两个点的距离(显然)
所以直线之间的距离一定能取到对踵点对距离:

38898c0d3a877215e802b6cebdf864ad.png

另外观察凸包“旋转”的过程,什么时候对踵点会发生改变?
当其中一条直线和凸包的某条边重合,就是另一组对踵点对的开始。
如下图,\(\color{blue}{蓝色}\)直线顺时针旋转得到新的\(\color{red}{红色}\)直线,产生了\(\color{green}{绿色}\)新的对踵点对。

QQ20250604-193236.png

而且在旋转过程中,两个对踵点都是单调顺时针转或都单调逆时针转,取决于旋转方向(这个也很显然)。
这启发我们先任选一组对踵点对,然后朝着固定方向旋转直到其中一条直线与凸包的边重合,然后更新对踵点对距离作为凸包直径
这个过程很麻烦……旋转直线?听起来很不可做,但是我们不妨枚举重合的边,由于另一个对踵点满足单调性,可以用双指针维护。

现在的问题是移动另一个对踵点。转化为如何比较两个点中哪一个到这条边的距离更大
看到距离,作垂线。由于这条边的长度是确定的,所以围成三角形面积确定(可以用叉积算出),已知底边和面积求高,小学数学秒了。

例 8:洛谷 P1452 【模板】旋转卡壳 | [USACO03FALL] Beauty Contest G

int n;
struct Point { int x, y, id; } p[N];
inline long long cha(Point a, Point b) { return a.x * 1ll * b.y - b.x * 1ll * a.y; }    // 叉积
inline long long S(Point a, Point b, Point c) { b.x -= a.x, b.y -= a.y, c.x -= a.x, c.y -= a.y; return cha(b, c); } // 三角形面积(有向)
inline long long dist(Point a, Point b) { return (a.x - b.x) * 1ll * (a.x - b.x) + (a.y - b.y) * 1ll * (a.y - b.y); }   //距离平方
Point stk[N]; int top;

void Convex();  // 凸包,top 个点存在 stk 里

long long ans; int px, py;
inline void update(Point x, Point y) {
    register long long res = dist(x, y);
    if (ans < res) ans = res, px = x.id, py = y.id;
}
void rotating() {
    if (top <= 2) { if (top == 2) update(stk[1], stk[2]); return; }
    int now = 2;    //当前对面的“对踵点”
    for (int i = 1; i <= top; i++) {
        while (llabs(S(stk[i], stk[i + 1], stk[now % top + 1])) > llabs(S(stk[i], stk[i + 1], stk[now]))) now = now % top + 1;
        update(stk[i], stk[now]), update(stk[i + 1], stk[now]);
    }
}

四、半平面交

半平面:给定一条有向直线 \(l\)\(l\) 左侧的所有点构成点集就称作一个半平面。
半平面交:给定 \(n\) 条有向直线,它们半平面的交集就是半平面交。
拿一张 OI-Wiki 的图:

QQ20250611-154928.png

如图就是一个半平面交:

7ieux7g3.png

求解半平面交一般使用 S&I 算法,算法核心是单调栈

  1. 先对所有有向直线做极角排序
  2. 由于规定了半平面是左侧平面,所以对于极角相同的直线只保留靠左的
  3. 按顺序往栈中加入直线,每次检查栈顶两条直线和待加入直线的关系。
    • 设待加入直线为 \(l\),栈顶两条直线为 \(u,v\),栈顶两条直线交点\(O\)
    • \(O\)\(l\) 的右侧或在 \(l\) 上,则弹出栈顶,继续判定。
  4. 绕一圈回来再检查栈顶、栈底是否满足条件。
    • 栈顶两条直线的交点不在栈底直线的左边,弹出栈顶
    • 栈底两条直线的交点不在栈顶直线的左边,删除栈底
    • 最后栈内直线围成的就是半平面交。

极角相同直线保留最左边,可以用叉积完成。

例 9:洛谷 P4196 [CQOI2006] 凸多边形 /【模板】半平面交

极角排序部分为了看起来更加赏心悦目,写了第二种方法(分八块按斜率排序)。
另外这题数据范围较小,所以不卡精度。
没看题解自己胡的代码,所以比较丑……

#include <bits/stdc++.h>
using namespace std;
const int N = 505;

struct Point { double x, y; int id; } ;
struct Line {
    Point a, b;
    double k, c;
    int id;
    inline void get() {
        if (a.x == b.x) k = 1e9, c = 0;
        else {
            k = (a.y - b.y) / (a.x - b.x);
            c = a.y - k * a.x;
        }
    }
} ;  // (a, b);  y = kx + c

int n, m, t1, t2;
Point p[N];
Line seg[N], seg2[N], l[N];

inline double cha(Point a, Point b) { return a.x * b.y - b.x * a.y; }
inline int pos(Point a) {
    if (a.x > 0 && a.y == 0) return 1;
    if (a.x > 0 && a.y > 0) return 2;
    if (a.x == 0 && a.y > 0) return 3;
    if (a.x < 0 && a.y > 0) return 4;
    if (a.x < 0 && a.y == 0) return 5;
    if (a.x < 0 && a.y < 0) return 6;
    if (a.x == 0 && a.y < 0) return 7;
    return 8;
}
inline bool cmp(Point a, Point b) {
    int pa = pos(a), pb = pos(b);
    if (pa ^ pb) return pa < pb;
    return a.y * b.x < b.y * a.x;
}
inline Point met(Line a, Line b) {
    if (a.k > b.k) swap(a, b);
    if (b.k == 1e9) return (Point){b.a.x, a.k * b.a.x + a.c, 0};
    double x = (b.c - a.c) / (a.k - b.k);
    double y = a.k * x + a.c;
    return (Point){x, y, 0};
}
bool check(Point a, Point b, Point c) { // c 是否在直线 ab 的逆时针方向
    b.x -= a.x, b.y -= a.y, c.x -= a.x, c.y -= a.y;
    return cha(b, c) > 0;
}

Line stk[N]; int top = 0;
Point ans[N]; int cnt;

int main() {
    
    scanf("%d", &t1);
    for (int i = 1; i <= t1; i++) {
        scanf("%d", &t2);
        for (int j = 1; j <= t2; j++) scanf("%lf%lf", &p[j].x, &p[j].y);
        for (int j = 1; j <= t2; j++) seg[++n] = {p[j], p[j % t2 + 1]}, seg[n].get(), seg[n].id = n;
    }
    for (int i = 1; i <= n; i++) p[i] = {seg[i].b.x - seg[i].a.x, seg[i].b.y - seg[i].a.y, i};
    sort(p + 1, p + 1 + n, cmp);    // 极角排序
    for (int i = 1; i <= n; i++) seg2[i] = seg[p[i].id];
    // for (int i = 1; i <= n; i++) cout << '\t' << seg2[i].a.x << ' ' << seg2[i].a.y << '\t' << seg2[i].b.x << ' ' << seg2[i].b.y << endl;
    for (int i = 1, j; i <= n; i = j + 1) {
        j = i;
        while (j < n && cha(p[j], p[j + 1]) == 0) j++;  // 极角相同
        Line Min = seg2[i];
        for (int k = i + 1; k <= j; k++) {
            if (check(Min.a, Min.b, seg2[k].a)) Min = seg2[k];
        }
        l[++m] = Min;
    }
    
    // for (int i = 1; i <= m; i++) cout << l[i].a.x << ' ' << l[i].a.y << '\t' << l[i].b.x << ' ' << l[i].b.y << endl;

    for (int i = 1; i <= m; i++) {
        while (top >= 2) {
            Point t = met(stk[top - 1], stk[top]);
            if (!check(l[i].a, l[i].b, t)) top--;   // 如果不在左边就弹出
            else break;
        }
        stk[++top] = l[i];
    }
    while (top >= 3) {
        Point t = met(stk[top - 1], stk[top]);
        if (!check(stk[1].a, stk[1].b, t)) top--;
        else break;
    }
    int now = 1;
    while (top - now + 1 >= 3) {
        Point t = met(stk[now], stk[now + 1]);
        if (!check(stk[top].a, stk[top].b, t)) now++;
        else break;
    }
    for (int i = now; i < top; i++) ans[++cnt] = met(stk[i], stk[i + 1]);
    ans[++cnt] = met(stk[now], stk[top]);
    
    // for (int i = now; i <= top; i++) cout << stk[i].a.x << ' ' << stk[i].a.y << '\t' << stk[i].b.x << ' ' << stk[i].b.y << endl;
    // for (int i = 1; i <= cnt; i++) cout << ans[i].x << ' ' << ans[i].y << endl;

    double S = 0;
    for (int i = 1; i <= cnt; i++) S += cha(ans[i], ans[i % cnt + 1]);
    printf("%.3lf\n", fabs(S) / 2);
    return 0;
}

五、闵可夫斯基和

两个图形 \(A,B\) 的闵可夫斯基和定义为:

\[C = \{ a+b | a \in A, b \in B \} \]

这里只介绍凸包的闵可夫斯基和。

怎么求?实际上就是把 \(B\) 图形绕着 \(A\) 的边转一圈,中途 \(B\) 覆盖过的点集就是闵可夫斯基和
这很魔怔,瞪眼法可得:把 \(A,B\) 的边按极角排序,再拼在一起,就得到了闵可夫斯基和的结果。
但是拼接后可能会存在三点共线情况,但这并不会影响什么,可以直接特判,也可以再求一遍凸包解决。

例 10:洛谷 P4557 [JSOI2018] 战争

首先,一个部落的“领地”可以被刻画成点集构成的凸包
不妨设两个凸包分别为 \(A,B\),将凸包内的点集刻画为向量的形式,即一个点 \((x, y)\) 看作 \((0, 0) \to (x, y)\) 的向量:
设两个点(向量)\(a, b\) 分别来自 \(A, B\),则对于一次询问需要查询:是否存在 \(a, b\) 满足 \(a = b + (dx, dy)\)
即:

\[a-b = (dx, dy) \]

存在 \(a \in A, b \in B\),使得 \(a-b = (dx, dy)\)
存在这一条件并不好支持查询,换一个角度,求出 \(C = {a-b | a \in A, b \in B}\) 这个全集判定 \((dx, dy)\) 是否在这之内?

\(A,B\) 都是凸包,求 \(A-B\)。凸包减法?什么玄学东西?
显然可以把 \(B\) 中坐标全部取反,然后变成 \(A + (-B)\)
\(-B\) 显然也是凸包,那么就变成了凸包加法,联想到闵可夫斯基和

因此我们的思路是:先求出 \(A,-B\),再求出 \(A-B\) 的闵可夫斯基和凸包 \(C\),对于每次询问判定 \((dx, dy)\) 是否在 \(C\) 之内。
这是之前提过的问题,不再赘述。

#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 5;
int q;

struct Point { int x, y; } ;
inline long long cha(Point a, Point b) { return a.x * 1ll * b.y - b.x * 1ll * a.y; }
inline int check(Point a, Point b, Point c) {  // c 是否在有向直线 a->b 的逆时针方向(左侧,含)
    b.x -= a.x, b.y -= a.y, c.x -= a.x, c.y -= a.y;
    long long res = cha(b, c);
    if (res == 0) return 0;
    if (res > 0) return 1;
    return -1;
}
inline bool online (Point a, Point b, Point c) {    // c 在线段 ab 上
    int x = min(a.x, b.x), xx = max(a.x, b.x);
    int y = min(a.y, b.y), yy = max(a.y, b.y);
    return (check(a, b, c) == 0 && x <= c.x && c.x <= xx && y <= c.y && c.y <= yy) ;
}
inline bool cmp(Point a, Point b) { return (a.x == b.x) ? a.y < b.y : a.x < b.x; }  // 二维排序


struct Polygon {
    int n, top;
    Point a[N], stk[N];
    inline void input(bool opt) {
        for (int i = 1; i <= n; i++) scanf("%d%d", &a[i].x, &a[i].y);
        if (opt) for (int i = 1; i <= n; i++) a[i].x = -a[i].x, a[i].y = -a[i].y;
    }
    inline void convex() {
        sort(a + 1, a + 1 + n, cmp);
        stk[++top] = a[1], stk[++top] = a[2];
        for (int i = 3; i <= n; i++) {
            while (top >= 2 && check(stk[top - 1], stk[top], a[i]) <= 0) top--;
            stk[++top] = a[i];
        }
        int tt = top;
        stk[++top] = a[n - 1];
        for (int i = n - 2; i >= 1; i--) {
            while (top > tt && check(stk[top - 1], stk[top], a[i]) <= 0) top--;
            stk[++top] = a[i];
        }
        for (int i = 1; i <= top; i++) a[i] = stk[i];
        n = --top;
    }
    // 以下是 C 凸包函数
    inline void add(Point t) { a[++n] = t; }
    inline void uniq() {    // 去掉三点共线
        top = n, n = 1;
        for (int i = 1; i <= top; i++) stk[i] = a[i];
        for (int i = 2; i <= top; i++) a[i].x = a[i].y = 0;

        for (int i = 2; i <= top; i++)
            if ((stk[i].x * 1ll * a[n].y) ^ (stk[i].y * 1ll * a[n].x)) a[++n] = stk[i];
            else a[n].x += stk[i].x, a[n].y += stk[i].y;
    }
} A, B, C;

inline void trans() {   // 求出闵可夫斯基和凸包 C
    int t1 = 1, t2 = 1;
    while (t1 <= A.n && t2 <= B.n) {
        Point deltaA = (Point){A.a[t1 + 1].x - A.a[t1].x, A.a[t1 + 1].y - A.a[t1].y};
        Point deltaB = (Point){B.a[t2 + 1].x - B.a[t2].x, B.a[t2 + 1].y - B.a[t2].y};
        if (t1 <= A.n && check((Point){0, 0}, deltaA, deltaB) >= 0) C.add( deltaA ), t1++;
        else C.add( deltaB ), t2++;
    }
    while (t1 <= A.n) C.add( (Point){A.a[t1 + 1].x - A.a[t1].x, A.a[t1 + 1].y - A.a[t1].y} ), t1++;
    while (t2 <= B.n) C.add( (Point){B.a[t2 + 1].x - B.a[t2].x, B.a[t2 + 1].y - B.a[t2].y} ), t2++;
    C.uniq();
    C.a[0] = (Point){A.a[1].x + B.a[1].x, A.a[1].y + B.a[1].y};
    for (int i = 1; i <= C.n; i++) C.a[i].x += C.a[i - 1].x, C.a[i].y += C.a[i - 1].y;
    // for (int i = 0; i <= C.n; i++) cout << C.a[i].x <<  ' ' << C.a[i].y << endl;
    C.n--;
}

int main() {
    scanf("%d%d%d", &A.n, &B.n, &q);
    A.input(0), B.input(1);
    A.convex(), B.convex();
    // for (int i = 1; i <= B.n; i++) cout << B.a[i].x << ' ' << B.a[i].y << endl;
    trans();
    while (q--) {
        int dx, dy; scanf("%d%d", &dx, &dy);
        Point p = (Point){dx, dy};
        if (check(C.a[0], C.a[1], p) < 0) { putchar('0'); putchar('\n'); continue; }
        if (check(C.a[C.n], C.a[0], p) < 0) { putchar('0'); putchar('\n'); continue;  }
        int l = 1, r = C.n;
        while (l < r) {
            int mid = l + r + 1 >> 1;
            if (check(C.a[0], C.a[mid], p) >= 0) l = mid;
            else r = mid - 1;
        }
        // [l, l + 1] 两条边之间
        if (check(C.a[l], C.a[l + 1], p) > 0 || online(C.a[l], C.a[l + 1], p) ) putchar('1');
        else putchar('0');
        putchar('\n');
    }
    return 0;
}

六、最小圆覆盖

给定平面上 \(n\) 个点,求覆盖这些点的最小圆的圆心和半径。

不妨用增量法,逐渐加入点并计算新的最小圆。

\(n=1\) 的时候,最小圆覆盖当然圆心是第一个点 \(p_1\),半径为 \(0\)
\(n=2\) 的时候,最小圆覆盖就是两点定圆。
\(n=3\) 的时候,分为两种情况:

  1. \(p_3\) 在上一个圆的圆周上或圆内。这种情况不会影响最小圆覆盖,忽略这个点。
  2. \(p_3\) 在上一个最小圆覆盖圆之外。这个时候 \(p_3\) 一定在前三个点的最小圆覆盖的圆周上,才能满足“最小”的性质。那么现在原问题转化为子问题:从 \(p_1p_2\) 中找出一或两个点,与点 \(p_3\) 两点定圆或三点定圆
  3. \(n \geq 4\) 的时候,与 \(n=3\) 的时候同理,分为两种情况,第一种直接忽略这个点,第二种则是从前面的点中挑出一或两个\(p_4\) 一起定圆。

最后把所有的点都加入之后,就可以得出最小圆覆盖。
需要一层循环枚举所有的点进行加入,一层循环来枚举两点定圆,还需要一层循环来枚举三点定圆(假设现在会两点定圆和三点定圆)。
所以这个算法的时间复杂度是 \(O(n^3)\)


事实上,把增量法改进为随机增量法就可以优化到 \(O(n)\),具体地,在执行算法之前把点打乱顺序。
时间复杂度证明:

由于三点定圆,所以对于 \(n\) 个点中,每个点参与三点定圆计算的概率不超过 \(\frac{3}{n}\)
由于只有更新半径才会往下一层循环,找另一个点(详见代码)所以对于当前层的第 \(i\) 个点,它往下找的概率不超过 \(\frac{3}{i}\)

  • 对于第三层,每个点参与计算的概率为 \(1\),由于只有参与和不参与两种状态,所以期望等于概率为 \(1\),总复杂度为 \(\sum\limits_{i=1}^{n} 1 = O(n)\)
  • 对于第二层,有 \(O(n) + \sum\limits_{i=1}^{n} \frac{3}{i} \times O(n) = O(n)\)
  • 对于最外层,有 \(O(n) + \sum\limits_{i=1}^{n} \frac{3}{i} \times O(n) = O(n)\)

得证期望总复杂度为 \(O(n)\)


关于如何两点定圆和三点定圆?

两点定(最小)圆是简单的,圆心是 \(p_1, p_2\) 的中点,半径是线段长度的一半。
三点定圆较为复杂,但列个方程就解完了。

设三个点分别为 \((x_1, y_1), (x_2, y_2), (x_3, y_3)\)
圆心为 \((x,y)\) 半径为 \(r\)

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

展开:

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

\((2)\) 式减 \((1)\) 式,得:

\[x_2^2 + y_2^2 - 2x \times (x_2 - x_1) - 2y \times (y_2 - y_1) = 0 \]

整理式子移项,同理 \((3)\) 式减 \((1)\) 式整理可得:

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

将系数换元化简:

\[\begin{cases} ax+by=c \\ dx+ey=f \end{cases} \]

解得

\[\begin{cases} y = \frac{\frac{af}{d}-c}{\frac{ae}{d}-b} \\ x = \frac{c-by}{a} \end{cases} \]

圆的半径求一下距离即可。

例 11:洛谷 P1742 最小圆覆盖

#include <bits/stdc++.h>
using namespace std;
const double eps = 1e-8;
const int N = 5e5 + 5;

struct Point { double x, y; };
inline double dis(Point A, Point B) { A.x -= B.x, A.y -= B.y; return sqrt(A.x * A.x + A.y * A.y); }

Point circle_center(const Point A, const Point B, const Point C) {
    // ax + by = c
    // dx + ey = f
    double a = 2 * (A.x - B.x), b = 2 * (A.y - B.y), c = A.x * A.x + A.y * A.y - B.x * B.x - B.y * B.y;
    double d = 2 * (A.x - C.x), e = 2 * (A.y - C.y), f = A.x * A.x + A.y * A.y - C.x * C.x - C.y * C.y;
    double y = (a * f / d - c) / (a * e / d - b);
    double x = (c - b * y) / a;
    return (Point){x, y};
}

void min_cover_circle(Point * p, int n, Point &c, double &r) {
    random_shuffle(p + 1, p + 1 + n);
    c = p[1]; r = 0;
    for (int i = 2; i <= n; i++) {
        if ( dis(p[i], c) - r > eps ) {
            c = p[i]; r = 0;
            for (int j = 1; j < i; j++)
                if ( dis(p[j], c) - r > eps ) {
                    c.x = (p[i].x + p[j].x) / 2;
                    c.y = (p[i].y + p[j].y) / 2;
                    r = dis(p[j], c);
                    for (int k = 1; k < j; k++)
                        if ( dis(p[k], c) - r > eps ) {
                            c = circle_center(p[i], p[j], p[k]);
                            r = dis(p[i], c);
                        }
                }
        }
    }
}

int n;
Point p[N], c;
double r;

int main() {
    while (~scanf("%d", &n) && n) {
        for (int i = 1; i <= n; i++) scanf("%lf%lf", &p[i].x, &p[i].y);
        min_cover_circle(p, n, c, r);
        printf("%.10lf\n%.10lf %.10lf", r, c.x, c.y);
    }
    return 0;
}

*例 12:洛谷 P4586 [FJOI2015] 最小覆盖双圆问题

给定平面上 \(n\) 个点 \((x_1,y_1),\dots,(x_n,y_n)\),找出 \(2\) 个半径相同的圆 \(R_1\)\(R_2\),覆盖给定的 \(n\) 个点,且半径最小。

设计一个算法,计算出所求最小覆盖双圆 \(R_1\)\(R_2\) 的半径。

很显然的,既然要分成两个圆,那么一定是用一条直线分割成两个点集,然后分别求出两个点集的最小圆覆盖的半径 \(r_1r_2\),答案就是 \(\max(r1,r2)\)
要求出这条直线的位置,其实很容易地可以想到用二分

这里所说的是:用二分求出一条\(X\) 轴垂直的直线,然后再求解。

但是还有一种情况未考虑——如果正解是斜着画一条直线呢?
斜着划直线并不好做,不妨直接把整个坐标轴旋转

每次旋转之后都求一次最小双圆覆盖,然后再取最优值即是答案。问题就在于如何对每个点进行“旋转”?

QQ截图20220917182333.png

对于这个坐标系,只需要旋转 \(180\) 度就可以了,因为把坐标系旋转 \(180\) 度后就又回到了第一次计算的问题。
有一个很重要的优化:在二分的时候,如果当前的 \(\min(r1,r2)\) 无法更新答案,则后面都无法更新答案,直接退出二分。

复杂度 \(O(C n \log n)\),其中 \(C\) 为常数,\(C=180\)

posted @ 2025-06-12 16:28  Conan15  阅读(28)  评论(0)    收藏  举报