计算几何学习笔记

· · 算法·理论

AI 使用声明:我在学习这些算法和理解这些证明的时候大量借用了 AI 进行辅助,因此 AI 显然对这篇文章有着较大的贡献。但保证文章内容全部是我手打的。

前言

说三件事。

第一件事,笔者在学习计算几何做法的时候费了非常多的功夫,期间也阅读了很多难以在可接受的时间内理解的笔记。因此,为初学者提供可能有助于理解的内容是写作此笔记的一大目的。

第二件事,虽然该笔记在很多地方添加了比较详细的证明,但是计算几何的很多直觉实际上来自于画图。对一些细节不理解的读者可以试着在草稿纸上画一下。

第三件事,长文警告。本文两万多字(含代码)。

本文覆盖的算法:

计算几何基本功

弧度制

定义 1 \pi rad = 180\degree。

推论 圆心角为 1rad 的扇形,弧长和半径相等。

之后描述角度的时候,如果不使用 \degree 标注,一律视为使用弧度制。如 \frac \pi 2 表示直角。

平面向量

向量的定义

定义 2 向量是既有大小又有方向的量。

向量的表示示例:\bm a,\bm b,……

如何表示平面向量:

两种记法的换算:

向量 \bm a=(x,y) 的大小为 \sqrt{x^2+y^2}。

当 x \not = 0 时,方向角 \theta 满足 \tan \theta = \frac{y}{x}。然后你判断一下 (x,y) 在哪个象限就可以求出 \theta。x=0 是平凡的。

特别地,\bm a=(0,0) 称为零向量,记作 \bm 0。它的大小为 0,方向不存在。

向量的加减法运算

定义 3 定义向量 \bm a=(x_1,y_1) 和 \bm b=(x_2,y_2) 的和 \bm a+\bm b 等于 (x_1+x_2,y_1+y_2)。

定义 4 定义向量 \bm a=(x_1,y_1) 和 \bm b=(x_2,y_2) 的差 \bm a-\bm b 等于 (x_1-x_2,y_1-y_2)。

如果用模长和方向角表示向量,加减法计算遵守平行四边形定则和三角形定则。

具体地:

(这里的描述中,\bm {AB} 表示从 A 点到 B 点的有向线段对应的向量。以后可能也会使用类似的表述。)

前者的证明:

令 A(a_x,a_y),B(b_x,b_y),C(c_x,c_y),则 \bm {AB}= (b_x - a_x,b_y - a_y),\bm {BC}= (c_x-b_x,c_y-b_y),\bm {AC} = (c_x - a_x,c_y - a_y)。

带入原式发现左边等于右边。

后者的证明:

注意到 \bm {AD} 和 \bm {BC} 同向且相等。

向量的数乘

定义 5 定义向量 \bm a=(x,y) 和实数 k 的乘积 k\bm a 等于 (kx,ky)。

如果用模长和方向角表示向量,对于 k>0,直接将模长乘上 k 即可。方向不变。对于 k=0,是 \bm 0。对于 k<0,方向会反。

向量的共线

定义 6 定义向量 \bm a,\bm b 共线,当且仅当存在不全为 0 的 \lambda_1,\lambda_2 \in \mathbb R,使得 \lambda_1 \bm a = \lambda_2 \bm b。记为 \bm a \parallel \bm b。

推论 1 对于任意向量 \bm a,\bm 0 \parallel \bm a。

推论 2 对于任意一对非零共线向量 \bm a 和 \bm b,\left| \theta_{\bm a} - \theta_{\bm b}\right| \in \{0,\pi\}。

也即方向相同或相反。

平面向量基本定理

定义 7 形如 \lambda_1\bm e_1+\lambda_2\bm e_2 的式子称为 \bm e_1,\bm e_2 的线性组合。

定理 1(平面向量基本定理)若 \bm e_1,\bm e_2 是同一平面内两个不共线的向量,则对任意向量 \bm a,存在唯一实数对 (\lambda_1,\lambda_2),使得 \bm a=\lambda_1\bm e_1+\lambda_2\bm e_2。

本定理的结论比较直观,不做证明。

这就是坐标记法的来历:坐标本质是向量在 \bm e_1 = (1,0) 和 \bm e_2 = (0,1) 下的分解系数。

向量的点积

定义 8 定义向量 \bm a=(x_1,y_1) 和 \bm b=(x_2,y_2) 的点积 \bm a \cdot \bm b 等于 x_1x_2 + y_1y_2。

性质 1 \bm a \cdot \bm b = \bm b \cdot \bm a。

性质 2 (\bm a + \bm b) \cdot \bm c = \bm a \cdot \bm c + \bm b \cdot \bm c。

性质 3 (k \bm a) \cdot \bm b = k(\bm a \cdot \bm b)。

如果用模长和方向角表示向量,如何计算点积?

定义 9 定义非零向量 \bm a 和 \bm b 的夹角 \langle\bm a,\bm b\rangle 为 \min\{|\theta_{\bm a}-\theta_{\bm b}|,\ 2\pi-|\theta_{\bm a}-\theta_{\bm b}|\}。

两个向量不平行时,也就是对应的两条直线的夹角中小的那个。

定理 2 设 \bm a,\bm b 为非零向量,夹角为 \theta=\langle\bm a,\bm b\rangle,则

证明 原式等价于:

旋转坐标系,归约到证明当 \bm a = (1,0) 且 \bm b = (\cos \theta,\sin \theta) 且 \theta \in [0,\pi] 时,\bm a \cdot \bm b = \cos \theta。

直接带入定义发现这是对的。

向量的叉积

定义 10 定义向量 \bm a=(x_1,y_1) 和 \bm b=(x_2,y_2) 的叉积 \bm a \times \bm b 等于 x_1y_2 - y_1x_2。

性质 1 \bm a \times \bm b + \bm b \times \bm a = 0。

性质 2 (\bm a + \bm b) \times \bm c = \bm a \times \bm c + \bm b \times \bm c。

性质 3 (k \bm a) \times \bm b = k(\bm a \times \bm b)。

如果用模长和方向角表示向量,如何计算叉积?

定义 11 设 \bm a,\bm b 为非零向量,从 \bm a 到 \bm b 的有向角 \theta 满足 \theta \equiv \theta_{\bm b} - \theta_{\bm a} \pmod {2\pi},取值为 (-\pi,\pi]。

就是从 \bm a 的方向到 \bm b 的方向要旋转的逆时针度数。

定理 3 设 \bm a,\bm b 为非零向量,从 \bm a 到 \bm b 的有向角为 \theta,则:

证明 原式等价于:

旋转坐标系,归约到证明当 \bm a = (1,0) 且 \bm b = (\cos \theta,\sin \theta) 且 \theta \in (-\pi,\pi] 时,\bm a \times \bm b = \sin \theta。

直接带入定义发现这是对的。

注意到 S_{ABC} = \frac{1}{2} AB \cdot AC\sin A,于是有:

推论 1 三角形 ABC 的面积为 S= \frac{1}{2}|\bm {AB} \times \bm {AC}|。

推论 2 若从 \bm a 到 \bm b 的有向角为 \theta,则 \theta >0 当且仅当 \bm a \times \bm b >0。

这个推论可以用于极角排序(将若干个非零向量按照方向角从小到大排序)。如果 \bm a 和 \bm b 都在上平面或都在下平面,就可以利用这个性质进行极角排序。

::::info[极角排序板子]

struct point{
    int x,y;
}A[50];
point operator - (point a,point b){
    return {a.x-b.x,a.y-b.y};
}
int operator *(point a,point b){
    return a.x*b.y-a.y*b.x;
}
struct line{
    point a,b;
}B[500];
int N;
bool cmp(line a,line b){
    point A=a.b-a.a,B=b.b-b.a;
    //根据数据实际情况,A 和 B 都不会是零向量。
    int flag1=0,flag2=0;
    if (A.y>0||(A.y==0&&A.x>0))flag1=1;
    if (B.y>0||(B.y==0&&B.x>0))flag2=1;
    if (flag1!=flag2)return flag1>flag2;
    else return (A*B)>0;
}
这份代码已经初现端倪了,计算几何实际上就是一个 corner case 特别多的专题。

点和直线的性质

方向为 \bm a 的有向直线的左侧半平面,指其逆时针旋转 180\degree 扫过的半平面。右侧类似。

我们可以用一个有向直线来表示一个半平面。表示的就是该有向直线的左侧半平面。

定理 4 考虑点 A,B,P 所在的平面,以下命题成立:

证明 考虑到叉积的符号实际上就对应着从 \bm {AB} 到 \bm {AP} 有向角的范围。

定理 5 直线 AB 和 CD 相交于 P 点,满足:

\bm {AP} = \bm{AB} \frac{\bm{AC} \times \bm{CD}}{\bm{AB} \times \bm{CD}}

证明

\bm{AB} \frac{\bm{AC} \times \bm{CD}}{\bm{AB} \times \bm{CD}} &= \bm{AB} \frac{(\bm{AP}+\bm{PC}) \times \bm{CD}}{\bm{AB} \times \bm{CD}}\\ &=\bm{AB} \frac{\bm{AP} \times \bm{CD}}{\bm{AB} \times \bm{CD}} \end{aligned}

当 \bm {AP} 和 \bm {AB} 同向时:

\bm{AB} \frac{\bm{AC} \times \bm{CD}}{\bm{AB} \times \bm{CD}} &=\bm{AB} \frac{\bm{AP} \times \bm{CD}}{\bm{AB} \times \bm{CD}}\\ &=\bm{AB} \frac{|\bm {AP}|}{|\bm {AB}|}\\ &=\bm {AP} \end{aligned}

当 \bm {AP} 和 \bm {AB} 反向时:

\bm{AB} \frac{\bm{AC} \times \bm{CD}}{\bm{AB} \times \bm{CD}} &=\bm{AB} \frac{\bm{AP} \times \bm{CD}}{\bm{AB} \times \bm{CD}}\\ &=-\bm{AB} \frac{|\bm {AP}|}{|\bm {AB}|}\\ &=\bm {AP} \end{aligned}

定理 6 过 P 做直线 AB 的垂线,垂足为 H,则:

|\bm {PH}| = \frac{|\bm{AB} \times \bm{AP}|}{|\bm{AB}|}

即点到直线的距离。

证明 考虑右侧的部分。它等于 AP \sin \angle PAB。

因为 \angle PAB 和 \angle PAH 要么相等要么互补,所以也等于 AP \sin \angle PAH。

然后观察到 \triangle APH 上 \sin \angle PAH = \frac{PH}{AP},做完。

点和线段的性质

定理 7 点 P 在线段 AB 上,当且仅当以下全部成立:

证明 前者使得 P 在直线 AB 上。后者使得 A 和 B 分居 P 的两侧(或与 P 重合)。合起来就是 P 在线段 AB 上的充要条件。

定理 8 P 到线段 AB 的距离为:

\frac{|\bm{AB} \times \bm{AP}|}{|\bm{AB}|} \min(|\bm{PA}|,|\bm{PB}|)

证明 前者使用定理 6 的结论。后者直观。

判断两线段是否相交

定理 9 线段 AB 和 CD 相交,当且仅当以下至少一项成立:

证明 必要性:考虑交点 P 的位置。

充分性:

多边形

定义 12 给定平面上 n 个点 P_0(x_0,y_0),P_1(x_1,y_1),\dots,P_{n-1}(x_{n-1},y_{n-1}) 依次相连,称为多边形。

推论 n 边形有 n 个点和 n 条边。

定义 13 若一个多边形的所有 n 条边除相邻边公共端点外两两不相交,称为简单多边形。

定义 14 设简单多边形 P 顶点逆时针排列,令 P_n = P_0,则称它为一个凸多边形,当且仅当对于任意 i \in [0,n),所有 P_i 和 P_{i+1} 以外的顶点都位于有向直线 \bm {P_i P_{i+1}} 的左侧。

引理 简单多边形外角和为 2\pi。

非常地不显然,也并不直观,但反正初中大家都对这个结论非常了解了那就不再解释了。

定理 10 设简单多边形 P 顶点逆时针排列,令 P_n = P_0,P_{n+1} = P_1,P 是凸多边形当且仅当,对于任意整数 i \in [0,n):

\bm{P_i P_{i+1}} \times \bm{P_{i+1}P_{i+2}} > 0

证明 必要性由定理 4 显然。

充分性,考虑用反证法,存在某个点落在某条有向边的右侧。不妨设这个有向边是 P_0P_1。

你沿着多边形的边逆时针转动,那么你没走到一个端点你就会左转 \theta \in (0,\pi),然后接着走。

如果你跑到了 \bm{P_0 P_1} 的右侧,说明你总共旋转了超过 \pi!

然后你要想办法试图回到 P_0,但是因为 \bm{P_{n-1}P_0} \times \bm{P_0P_1}>0,所以 P_{n-1} 必然在 \bm{P_0 P_1} 的左侧。

所以你要先从左侧转到右侧再转回左侧,这时你必然累计旋转了超过 2\pi,因此你不是简单多边形,矛盾。因此充分性得证。

定义 15 设 A 为平面上一点,P 为顶点逆时针排列的凸多边形。

以后若提到多边形,默认讨论凸多边形。

定理 11 在凸多边形内侧或边界上任取两点 A,B,线段 AB 上任意的一点 C 都在该凸多边形的内侧或边界上。

证明 考虑一条有向边 \bm {P_iP_{i+1}}。A 在其左侧(或边界上),B 在其左侧(或边界上),所以整条线段都在其左侧(或边界上)。证毕。

定理 12(鞋带公式的凸多边形情况) 设凸多边形 P 顶点逆时针排列,令 P_n = P_0,则多边形的面积 S 满足

S=\frac{1}{2}\sum_{i=0}^{n-1}\bm{OP_i}\times \bm{OP_{i+1}}

证明 指定凸多边形内的一个点 A。

&= \bm{OA} \times \bm{OA} + \bm{OA} \times \bm{AP_{i+1}} + \bm{AP_i} \times \bm{OA} + \bm{AP_i} \times \bm{AP_{i+1}}\\ &=\bm{OA} \times \bm{P_iP_{i+1}} + 2S_{\triangle AP_iP_{i+1}} \end{aligned}

然后全部加起来之后就发现所有的第一项全都消掉了(\sum_{i=0}^{n-1} \bm {P_iP_{i+1}} = 0)。所以总和就是面积 S。

定理 13 设凸多边形 P 顶点逆时针排列,令 P_n = P_0,则多边形的重心 G 满足

\bm{OG}=\frac{1}{6S}\sum_{i=0}^{n-1}(\bm{OP_i}+\bm{OP_{i+1}})(\bm{OP_i}\times \bm{OP_{i+1}})

仅做了解,不进行证明。

考这玩意不给公式的出题人保护好自己的母亲。

皮克定理

定理 14(皮克定理) 顶点均为整点的简单多边形内部格点数为 I,边界格点数为 B,则面积为:

S=I+\frac{B}{2}-1

仅做了解,不进行证明。

曼哈顿距离和切比雪夫距离

定理 15 令 u=x+y,v=x-y,则:

|x_1 - x_2| + |y_1 - y_2| = \max(|u_1 - u_2|,|v_1 - v_2|)

仅做了解,不进行证明。

凸包

定义 16 给定平面上 n 个点,如果某个凸多边形满足这 n 个点都在它的内部或边界上,并且在所有满足这一条件的凸多边形中面积最小,就称这个凸多边形为这 n 个点的凸包。

(注意:凸包可能退化成线段或点。)

观察一个凸包。

观察 凸包的每个顶点都来自给定的 n 个点。

理解 考虑如果多边形一个角上没有给定的点,把这个角切掉一点点。切了之后,多边形就会变小,而且由于给定的点数量有限,一定存在切法不会把点切出去,所以它不是凸包。

让我们从给定的 n 个点中,选出一个横坐标最小的点 A 和一个横坐标最大的点 B。

考虑沿着多边形的边逆时针走,你会先在直线 AB 的下方从 A 走到 B,然后再在直线 AB 的上方从 B 走回 A。

(注意定理 11,线段 AB 被包括在凸包内。所以这一观察是直观的。)

我们声称在下方从 A 走到 B 的路径为凸包的下凸壳,在上方从 B 走回 A 的路径为凸包的上凸壳。

这两个拼起来就是整个凸包。

结论 1 在下凸壳上从左往右走,斜率单调递增;在上凸壳上从左往右走,斜率单调递减。

因为,在下凸壳上走的时候一直左转,左转斜率会变大。在上凸壳上走的时候一直右转,右转斜率会变小。

结论 2 在上 / 下凸壳上从左往右走,横坐标单调不减。

以上凸壳为例,下凸壳类似。

每一步都要右转。如果 P_i 的横坐标小于 P_{i-1},那么你发现当前的前进方向已经是向左而不是向右了。根据结论 1,斜率单减,不可能从负的再变回正的,所以你不可能再向右了。

因为 P_i 的横坐标小于 P_{i-1} 所以 P_i 不是 B。而且之后你的横坐标不能再增大,想从 P_i 到达 B 已经不可能了,矛盾。

我们尝试分别求出上凸壳和下凸壳。以上凸壳为例。

将所有点按横坐标排序。

作为上凸壳,根据先前的结论,它一定选取了这 n 个点中的一部分,然后按照横坐标从小到大的顺序依次相连。

考虑这样一个问题:

在什么情况下,一个点 P_i 会出现在上凸壳的端点上?

答案是:

P_i 在上凸壳的端点上,当且仅当不存在 <i 的整数 j 和 >i 的整数 k,满足直线 P_jP_k 在 P_i 的上方(或者也可以说成,P_i 在 \bm {P_jP_k} 的右侧。另外,三点共线也不允许存在)。

对于必要性,如果存在这样的 (j,k),则 P_i 被其他点构成的上凸壳盖住了,就算去掉也不会影响上凸壳,与 P_i 在上凸壳的端点上矛盾。

对于充分性,注意到如果删除 P_i,其他点都无法覆盖 P_i 这个点,所以 P_i 是重要的,因此它在上凸壳的端点上。

接下来我将描述算法流程,随后证明它求出的是正确的上凸壳。

开一个栈,首先加入 P_0 = A 和 P_1。

对于 P_2 \sim P_{n-1},加入 P_i 前,先取出栈顶的两个点,设分别是 C 和 D。其中 D 是栈顶。

重复此操作,直到 D 在 \bm{CP_i} 的左侧。然后再将 P_i 入栈。

正确性证明有两部分。 第一,正确的上凸壳中的点,都没有被弹出栈。这一点直观且显然。 第二,没被弹出栈的点,都在正确的上凸壳上。 考虑归纳,对于任意的正整数 $k$,处理完前 $k$ 个点后,栈内是前 $k$ 个点的上凸壳。 $k=1$ 时显然。 考虑加入一个新的点的情况。这时,原本栈内的点 $P_i$ 若不再满足刻画,必然是新加入的点 $P_{k+1}$ 导致的。 假如 $P_i$ 在 $P_jP_{k+1}$ 的下方,但它在凸壳上,而且没有在加入 $P_{k+1}$ 前被清掉。 $P_j$ 取横坐标小于 $P_i$ 的,最后一个凸壳上的点,限制最严格。理由是,这时,其他可选的点都在 $P_jP_{k+1}$ 下方。 这一点意味着,逃过一劫的位置,一定“没被处理到”。因为处理到了,就会用最严格的限制处理它。 令栈内的点为 $S_1,\dots,S_{len}$,$S_{len} = P_k$。刚才的例子就是说,存在一个 $S_i$ 在 $S_{i-1}P_{k+1}$ 的下方,而且没被删。 由此可见,$S_{i+1}$ 一定没被删,否则根据算法过程,就把 $S_i$ 删掉了。 但是,考虑 $S_i \to S_{i+1}$。已知 $S_{i+1}$ 在 $S_{i-1}S_i$ 的下方,$S_i$ 又在 $S_{i-1}P_{k+1}$ 的下方,于是得到 $S_{i+1}$ 也在 $S_{i-1}P_{k+1}$ 下方,所以它也是一个漏网之鱼。 以此类推,可得 $S_{len}$ 也是漏网之鱼,但这是不可能的。证毕。 根据第一数学归纳法,第二点得证。 从而,刚才给出的算法就是正确的求上凸壳的做法。 求下凸壳,类似地做即可。 代码见下,可以通过凸包模板题。 ```cpp #include<bits/stdc++.h> #define int long long using namespace std; struct Point{ int x,y; }a[1000005],stk[1000005],b[1000005]; Point operator -(Point a,Point b){ return {a.x-b.x,a.y-b.y}; } int operator *(Point a,Point b){ return a.x*b.y-a.y*b.x; } int n; bool cmp(Point a,Point b){ if (a.x==b.x)return a.y<b.y; return a.x<b.x; } bool operator ==(Point a,Point b){ return a.x==b.x&&a.y==b.y; } void solve(){ cin>>n; for (int i=1;i<=n;i++){ cin>>a[i].x>>a[i].y; } sort(a+1,a+1+n,cmp); n=unique(a+1,a+1+n)-a-1; int cnt=0; for (int i=1;i<=n;i++){ while (cnt>=2&&(a[i]-stk[cnt-1])*(stk[cnt]-stk[cnt-1])<=0)--cnt; stk[++cnt]=a[i]; } for (int i=1;i<=cnt;i++){ b[i]=stk[i]; } int k=cnt; cnt=0; for (int i=1;i<=n;i++){ while (cnt>=2&&(a[i]-stk[cnt-1])*(stk[cnt]-stk[cnt-1])>=0)--cnt; stk[++cnt]=a[i]; } for (int i=cnt-1;i>=2;i--){ b[++k]=stk[i]; } for (int i=1;i<=k;i++){ cout<<b[i].x<<" "<<b[i].y<<"\n"; } } signed main(){ int t=1; while (t--)solve(); } ``` ### 旋转卡壳 以求平面最远点对为例。 **引理 1** 对于三角形 $ABC$,若 $\angle A$ 是直角或钝角,则有 $BC > \max(AB,AC)$。 **证明** 直角就是勾股定理推论。对于钝角,过 $B$ 做 $AC$ 的垂线交 $AC$ 与点 $D$,有 $BC>DC>AC$。同理过 $C$ 做 $AB$ 的垂线交 $AB$ 于点 $E$,有 $BC>BE>AB$。 **引理 2** $D$ 在线段 $BC$ 上,则 $AD \le max(AB,AC)$。 **证明** 若 $D$ 与 $B$ 或 $C$ 重合,符合条件。若不重合,$\angle ADB + \angle ADC = \pi$,于是至多有一个是锐角。对非锐角调用引理 1。 **结论** 至少有一对凸包的顶点,取到平面最远点对的距离。 **证明** 令一对最远点对是 $(A,B)$,且 $A$ 和 $B$ 不全在凸包上。令直线 $AB$ 和凸包交于点 $C$ 和点 $D$,其中 $C$ 在凸包边 $EF$ 上,$D$ 在凸包边 $GH$ 上。因为 $A,B$ 都不会在凸包外,所以必然有 $AB \le CD$。 $D$ 在线段 $GH$ 上,$CD \le \max(CG,CH)$。 $C$ 在线段 $EF$ 上,$CG \le \max(GE,GF)$,$CH \le \max(HE,HF)$。 因此 $AB \le \max(GE,GF,HE,HF)$,从而我们把边长信息转到了凸包顶点上,得证。 **推论** 平面最远点对 $=$ 凸包直径。 于是你想到枚举凸包顶点的点对 $(x,y)$,但是这还是平方复杂度。 考虑凸包上的一条边 $P_0P_1$。 使一条直线 $l$ 从直线 $P_0P_1$ 出发,向 $\bm {P_0P_1}$ 的左侧平移。这时,先被直线 $l$ 划过的点,与直线 $P_0P_1$ 的距离小;后被直线 $l$ 划过的点,与直线 $P_0P_1$ 的距离大。 最后 $l$ 会离开凸包。考察它离开凸包前一瞬间划过的部分,那个部分就是距离 $P_0P_1$ 最远的部分。 如果凸包上存在和 $P_0P_1$ 平行的另一条边,那个部分就是这条边。否则,是一个凸包的端点。 也就是说,存在至多 $2$ 个凸包端点,距离直线 $P_0P_1$ 最远。 **结论** 从 $P_1$ 出发逆时针在多边形上行走并到达 $P_0$,经过的凸包端点与 $P_0P_1$ 的距离的变化,是先单调不减,后单调不增。 **证明** 旋转凸包使得 $P_0P_1$ 与 $y$ 轴平行,且 $P_0P_1$ 同为凸包 $x$ 坐标最小端点。由于整个凸包都在直线 $P_0P_1$ 的一侧,这一定可以做到。 然后考虑凸包一章中我们证明的,经过下凸壳时横坐标单增和经过上凸壳时横坐标单减,得证。 **结论** 凸多边形 $P_0,\dots,P_{n-1}$ 顶点逆时针排列,令 $P_n = P_0$,$S_i$ 为距离直线 $P_iP_{i+1}$ 最远的端点组成的集合(有 $|S_i| \le 2$)。凸包的直径等于 $\max_{i=0}^{n-1} \max_{P \in S_i}\max(P_iP,P_{i+1}P)$。 ::::info[证明] **证明** 只需要证明每一对最远点对都能被上面的式子取到即可。 用反证法。 假设一对最远点对为 $(P_x,P_y)$。右式没有取到它。 1. $P_x$ 和 $P_y$ 相邻的情况。 不妨令 $y=x+1$。这时,考虑到 $P_xP_{x+1}$ 是凸包直径,以它为半径,$P_x$ 和 $P_{x+1}$ 为圆心分别画圆,取两圆的一个交点为 $A$ 满足 $A$ 在 $\bm{P_xP_{x+1}}$ 的左侧,就把凸包框在了曲边三角形 $AP_xP_{x+1}$ 内。 这时考虑 $P_xP_{x-1}$。 根据假设,曲边三角形中能取到一个点 $P_z$ 使得 $P_{x+1}$ 到 $P_xP_{x-1}$ 的距离小于 $P_z$ 到 $P_xP_{x-1}$ 的距离。 因此考虑过 $P_{x+1}$ 做 $P_xP_{x-1}$ 的平行线 $l$,则 $P_z$ 和 $P_x$ 位于 $l$ 的异侧。这要求 $l$ 把曲边三角形切开。 $P_{x-1}$ 在曲边三角形内,因为曲边三角形的结构,从 $\bm{P_xP_{x+1}}$ 到 $\bm{P_xP_{x-1}}$ 的方向角在 $(0,\frac{\pi}{2})$ 范围内。考虑将 $\bm{P_xP_{x-1}}$ 平移到 $\bm{P_{x+1}B}$,有 $\bm{P_xP_{x+1}}$ 到 $\bm{P_{x+1}B}$ 的方向角在 $(0,\frac{\pi}{2})$ 范围内。 然而,对于曲边三角形内任何一个点 $C$,有从 $\bm{P_xP_{x+1}}$ 到 $\bm{P_{x+1}C}$ 的方向角在 $(\frac{\pi}{2},\pi]$ 范围内。因此 $l$ 根本就不可能切开曲边三角形,矛盾! 2. $P_x$ 和 $P_y$ 不相邻的情况。 考虑直线 $P_{x-1}P_x$,$P_y$ 不是距离该直线的最远点。因此,若做直线 $l_1$ 过 $P_y$ 且平行于直线 $P_{x-1}P_x$,那么 $P_{y-1}$ 和 $P_x$ 在直线 $l_1$ 的异侧。 如何解释这个结论? 下面的论证中称内侧为有 $P_x$ 的一侧,外侧为另一侧。 考虑直线 $l_1$ 和以 $P_x$ 为圆心 $P_xP_y$ 为半径的圆 $O_1$,这两个图形在 $P_y$ 相交。 因为 $(P_x,P_y)$ 是最远点对,所以 $P_{x-1}$ 和 $P_{x+1}$ 都在圆 $O_1$ 内侧。又因为 $P_y$ 不是距离 $P_{x}P_{x-1}$ 最远的点,而且根据上面证明的距离单调性,所以 $P_{y-1}$ 和 $P_{y+1}$ 中必然恰好有一个与 $P_x$ 位于 $l_1$ 对侧。 同时,$P_{y+1} $ 和 $P_{y-1}$ 必然位于 $\bm{P_xP_y}$ 对侧(考虑凸包沿 $P_xP_y$ 切开也是凸多边形,由凸多边形的性质)。 这时,我们发现 $P_{y+1}$ 位于 $\bm{P_xP_y}$ 左侧,且位于圆 $O_1$ 内;$P_{y-1}$ 位于 $\bm{P_xP_y}$ 右侧,且位于圆 $O_1$ 内。 又因为 $P_{x-1}$ 位于 $\bm{P_xP_y}$ 左侧,所以将直线 $P_xP_{x-1}$ 平移到 $l_1$ 的时候,若 $P_{x-1}$ 平移到 $P_y$,则 $P_{x}$ 会平移到 $\bm{P_xP_y}$ 右侧的一个位置。 所以 $l_1$ 外侧与圆 $O_1$ 的交集,就位于 $\bm{P_xP_y}$ 右侧!从而位于左侧的 $P_{y+1}$ 没有了机会,$P_{y-1}$ 是位于直线 $l_1$ 外侧的那个点。 考虑直线 $P_xP_{x+1}$,类似地,若做直线 $l_2$ 过 $P_y$ 且平行于直线 $P_xP_{x+1}$,那么 $P_{y+1}$ 和 $P_x$ 在直线 $l_2$ 的异侧。 由此,考虑 $l_1$ 和 $l_2$ 的内侧夹角。那个夹角与 $\angle P_{x-1}P_xP_{x+1}$ 相等,但是 $\angle P_{y-1}P_yP_{y+1}$ 大于那个夹角。所以有: $$ \angle P_{y-1}P_yP_{y+1} > \angle P_{x-1}P_xP_{x+1} $$ 然而,由对称性,可以反转 $x,y$ 再做一遍,得到: $$ \angle P_{x-1}P_xP_{x+1} > \angle P_{y-1}P_yP_{y+1} $$ 矛盾。 :::: ::::info[说明] 如果你指任取一个最远点代替 $S_i$,结论是不正确的。考虑一个平行四边形,有小概率取不到对角线。 :::: 这个时候,你声称你已经会求平面最远点对了。只要先求凸包,然后枚举凸包上的边,再二分最远点即可。 不过,还有更简洁的想法! **结论** 令 $A_i$ 为距离直线 $P_iP_{i+1}$ 最远的点中,$\max(P_iA,P_{i+1}A)$ 最大的一个,若有并列最大则任取一个。那么,有 $A_i = A_{i+1}$,或 $P_{i+1},A_i,A_{i+1}$ 在凸包上逆时针排列。 **证明** 过 $A_i$ 做直线 $l_1$ 平行于 $P_iP_{i+1}$。 过 $A_{i+1}$ 做直线 $l_2$ 平行于 $P_{i+1}P_{i+2}$。 则整个凸包位于被 $P_iP_{i+1}$、$P_{i+1}P_{i+2}$、$l_1$、$l_2$ 这四条直线框起来的平行四边形内,而且 $l_1$ 和 $l_2$ 上都有凸包的端点。 用反证法,如果 $P_{i+1},A_i,A_{i+1}$ 顺时针排列。这说明从 $P_{i+1}$ 开始逆时针行动,先经过 $l_2$ 上的 $A_{i+1}$,后经过 $l_1$ 上的 $A_i$。 然而考虑这个平行四边形。令 $l_1$ 和 $l_2$ 的交点为 $Z$,则 $ZP_{i+1}$ 是对角线。在 $l_1$ 上任取一点 $X$,$l_2$ 上任取一点 $Y$,都有 $Y$ 在 $\bm{P_{i+1}Z}$ 的左侧而 $X$ 在 $\bm{P_{i+1}Z}$ 的右侧。 因此,$X$ 在 $\bm {P_{i+1}Y}$ 的右侧。 于是我们刚才生成的凸多边形 $P_{i+1}A_{i+1}A_iP_i$ 其实并不是凸的,矛盾! 这样我们就可以动用一个双指针了。当 $P_i$ 逆时针移动的时候,$A_i$ 也逆时针移动。 代码见下,可以通过旋转卡壳模板题。 ```cpp #include<bits/stdc++.h> #define int long long using namespace std; struct Point{ int x,y,id; }a[1000005],stk[1000005],b[1000005]; Point operator -(Point a,Point b){ return {a.x-b.x,a.y-b.y}; } int operator *(Point a,Point b){ return a.x*b.y-a.y*b.x; } int n; bool cmp(Point a,Point b){ if (a.x==b.x)return a.y<b.y; return a.x<b.x; } bool operator ==(Point a,Point b){ return a.x==b.x&&a.y==b.y; } int length(Point x){ return x.x*x.x+x.y*x.y; } int dist(Point x,Point y,Point z){ return (y-x)*(z-x); } void solve(){ cin>>n; for (int i=1;i<=n;i++){ cin>>a[i].x>>a[i].y;a[i].id=i; } sort(a+1,a+1+n,cmp); n=unique(a+1,a+1+n)-a-1; int cnt=0; for (int i=1;i<=n;i++){ while (cnt>=2&&(a[i]-stk[cnt-1])*(stk[cnt]-stk[cnt-1])<=0)--cnt; stk[++cnt]=a[i]; } for (int i=1;i<=cnt;i++){ b[i]=stk[i]; } int k=cnt; cnt=0; for (int i=1;i<=n;i++){ while (cnt>=2&&(a[i]-stk[cnt-1])*(stk[cnt]-stk[cnt-1])>=0)--cnt; stk[++cnt]=a[i]; } for (int i=cnt-1;i>=2;i--){ b[++k]=stk[i]; } if (k==1){ cout<<"0 1\n"; return; } else if (k==2){ cout<<b[1].id-1<<" "<<b[2].id-1<<"\n"; return; } reverse(b+1,b+1+k); int j=2,ans=length(b[2]-b[1]),ansi=1,ansj=2; for (int i=1;i<=k;i++){ int nxt=(i==k?1:i+1); int temp=(j==k?1:j+1); while (dist(b[i],b[nxt],b[j])<dist(b[i],b[nxt],b[temp]) ||(dist(b[i],b[nxt],b[j])==dist(b[i],b[nxt],b[temp])) &&(max(length(b[j]-b[i]),length(b[j]-b[nxt]))< max(length(b[temp]-b[i]),length(b[temp]-b[nxt])))){ j=temp;temp=(j==k?1:j+1); } if (length(b[j]-b[i])>ans){ ans=length(b[j]-b[i]); ansi=i,ansj=j; } if (length(b[j]-b[nxt])>ans){ ans=length(b[j]-b[nxt]); ansi=nxt,ansj=j; } } cout<<b[ansi].id-1<<" "<<b[ansj].id-1<<"\n"; } signed main(){ int t=1;cin>>t; while (t--)solve(); } ``` ### 半平面交 我必须想办法用一种自然的方式写明白半平面交! --- 力竭了,没能做到。 因此,下面只展示半平面交的算法流程。 --- 若干个半平面的交可以有下面的几种形态: - 一个凸多边形。 - 空集。 - 由多个半平面组成的一个不闭合的凸壳。 - 一对反向平行有向直线中间的部分。 - 单个半平面。 为什么只有这五种情况,可以考虑归纳得到,也就是在上面图形的基础上添加一个新的半平面。由于我们后续会做讨论,这里就不重复讨论了。 发现这些图形都由所有依然有效力的半平面构成的边组成。这些半平面**按照方向角排序**,依次为 $A_1,\dots,A_k$。 确保它们两两不同向。平行同向的两条有向直线,靠左的那一条直线的效果会完全覆盖靠右的那一条直线,因此可以把后者删掉。这样就两两不同向了。 考虑在一个已有的半平面交中,添加一个方向角比已有所有有向直线都大的有向直线。 - 一个凸多边形: - 可能根本没有影响。 - 可能被切掉一小块。**$A_1$ 和 $A_k$ 的交点被切掉**。$A$ 中的一个前缀和一个后缀可能被切得失去效力。 - 可能直接变成空集。 - 一个空集: - 还是空集。 - 一个由多个半平面组成的一个不闭合的凸壳: - 可能根本没有影响。 - 可能变成了一个新的不闭合的凸壳。$A$ 中的一个后缀可能被切得失去效力。 - 可能变成了闭合凸多边形。$A$ 中的一个前缀和一个后缀可能被切得失去效力。 - 可能直接变成空集。 - 一对反向平行有向直线中间的部分: - 可能根本没有影响。 - 可能会变成一个不闭合凸壳。 - 可能直接变成空集。 - 单个半平面: - 可能根本没有影响。 - 可能变成一个由多个半平面组成的一个不闭合的凸壳。 - 可能变成一对反向平行有向直线中间的部分: - 可能直接变成空集。 于是你想要用一个算法解决所有情况。 - 维护一个双端队列表示当前还有效力的半平面。 - 准备加入一个新的半平面。 - 如果它把队尾切掉了,弹出队尾。重复。 - 如果它把队头切掉了,弹出队头。重复。 - 将新的半平面加入队尾。 - 全部加入之后,检查队头有没有切掉队尾,队尾有没有切掉队头。依旧重复。 于是你得到了一个非常简洁的算法流程。 代码见下,可以通过半平面交模板题。 ``` #include<bits/stdc++.h> #define int long long using namespace std; struct point{ double x,y; }A[505]; point operator - (point a,point b){ return {a.x-b.x,a.y-b.y}; } double operator *(point a,point b){ return a.x*b.y-a.y*b.x; } struct line{ point a,b; }B[505],C[505]; int N; bool cmp(line a,line b){ point A=a.b-a.a,B=b.b-b.a; //根据数据实际情况,A 和 B 都不会是零向量。 int flag1=0,flag2=0; if (A.y>0||(A.y==0&&A.x>0))flag1=1; if (B.y>0||(B.y==0&&B.x>0))flag2=1; if (flag1!=flag2)return flag1>flag2; else return (A*B)>0; } bool left(line a,point x){ return (x-a.a)*(a.b-a.a)<0; } point inter(line a,line b){ //保证相交 point d1=a.b-a.a,d2=b.b-b.a; double delta=d1*d2; double t=((b.a-a.a)*d2)/delta; return {a.a.x+t*d1.x,a.a.y+t*d1.y}; } bool left(line a,line b,line c){ return left(c,inter(a,b)); } line deq[1000005]; int hh=1001,tt=1000; point x[1000005]; signed main(){ int n; cin>>n; for (int i=1;i<=n;i++){ int m;cin>>m; for (int j=1;j<=m;j++){ cin>>A[j].x>>A[j].y; }for (int j=1;j<=m;j++){ int k=(j==m?1:j+1); B[++N]={A[j],A[k]}; } } sort(B+1,B+1+N,cmp); int M=N;N=0; C[++N]=B[1]; for (int i=1;i<M;i++){ if (cmp(C[N],B[i+1])||cmp(B[i+1],C[N])){ C[++N]=B[i+1]; } else if (!left(B[i+1],C[N].a))C[N]=B[i+1]; } //题目数据范围确保了 N>=2 deq[++tt]=C[1]; deq[++tt]=C[2]; for (int i=3;i<=N;i++){ while (tt-hh+1>=2&&!left(deq[tt-1],deq[tt],C[i]))--tt; while (tt-hh+1>=2&&!left(deq[hh],deq[hh+1],C[i]))++hh; deq[++tt]=C[i]; } while (tt-hh+1>=3&&!left(deq[hh],deq[hh+1],deq[tt]))++hh; while (tt-hh+1>=3&&!left(deq[tt-1],deq[tt],deq[hh]))--tt; M=0; for (int i=hh;i<=tt;i++){ int j=(i==tt?hh:i+1); x[++M]=inter(deq[i],deq[j]); } double S=0; for (int i=1;i<=M;i++){ int j=(i==M?1:i+1); double ax=x[i].x,ay=x[i].y; double bx=x[j].x,by=x[j].y; S+=ax*by-ay*bx; } printf("%.3lf",S/2); return 0; } ```