计算几何学习笔记
zhoumurui
·
2026-09-20 12:15:23
·
算法·理论
AI 使用声明:我在学习这些算法和理解这些证明的时候大量借用了 AI 进行辅助,因此 AI 显然对这篇文章有着较大的贡献。但保证文章内容全部是我手打的。
前言
说三件事。
第一件事,笔者在学习计算几何做法的时候费了非常多的功夫,期间也阅读了很多难以在可接受的时间内理解的笔记。因此,为初学者提供可能有助于理解的内容是写作此笔记的一大目的。
第二件事,虽然该笔记在很多地方添加了比较详细的证明,但是计算几何的很多直觉实际上来自于画图。对一些细节不理解的读者可以试着在草稿纸上画一下。
如果一种图画不出来,可能你想要描述的情况并不存在。
你可能漏掉了一些情况。大分讨是计算几何的一环。
第三件事,长文警告。本文两万多字(含代码)。
本文覆盖的算法:
计算几何基本功
弧度制
定义 1 \pi rad = 180\degree 。
推论 圆心角为 1rad 的扇形,弧长和半径相等。
之后描述角度的时候,如果不使用 \degree 标注,一律视为使用弧度制。如 \frac \pi 2 表示直角。
平面向量
向量的定义
定义 2 向量是既有大小又有方向的量。
向量的表示示例:\bm a ,\bm b ,……
如何表示平面向量:
平面向量 \bm a 的方向可以这样表示:一个方向等同于 x 轴正方向的向量,想要与 \bm a 同向,需要逆时针旋转的最小正角度。
例如:方向等同于 y 轴正方向的向量,方向记为 \frac{\pi}{2} ;方向等同于 y 轴负方向的向量,方向记为 \frac{3\pi}{2} 。
大小直接记就可以了。于是向量用模长 \left| \bm a\right| \ge 0 和方向角 \theta \in [0,2\pi) 表示了。
注意到平面上一个点指向另一个点的有向线段就是一个向量。向量也可以用有向线段表示。
从 O(0,0) 指向 P(x,y) 的有向线段,大小和方向与从 A(x_0,y_0) 指向 B(x_0+x,y_0+y) 的有向线段相同。
因此一个向量如果和从 O(0,0) 指向 P(x,y) 的有向线段大小和方向都相同,就可以记为 (x,y) 。
于是向量用横坐标 x \in \mathbb R 和纵坐标 y \in \mathbb R 表示了。
两种记法的换算:
向量 \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,B,C ,\bm {AC} = \bm {AB} + \bm {BC} 。
平行四边形定则:对平面上的平行四边形 ABCD ,\bm{AB}+\bm{AD} = \bm{AC} 。
前者的证明:
令 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 的距离为:
过 P 做直线 AB 的垂线,垂足为 H ,若 H 在线段 AB 上,则答案为:
\frac{|\bm{AB} \times \bm{AP}|}{|\bm{AB}|}
\min(|\bm{PA}|,|\bm{PB}|)
证明 前者使用定理 6 的结论。后者直观。
判断两线段是否相交
定理 9 线段 AB 和 CD 相交,当且仅当以下至少一项成立:
证明 必要性:考虑交点 P 的位置。
若 P 与 C,D 其中之一重合,满足第二项。
若 P 与 A,B 其中之一重合,满足第三项。
其他情况,直观地,满足第一项。
充分性:
第二项和第三项的充分性显然。
对于第一项,可以得出直线 AB 和线段 CD 相交,且线段 AB 与直线 CD 相交。由于可以看出两条直线并不平行(如果平行显然不会满足分居两侧的条件),这两个交点显然是同一个,从而线段 AB 和线段 CD 都通过这个点。
多边形
定义 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 为顶点逆时针排列的凸多边形。
若 A 在 P 的某条边上,则称 A 在 P 的边界上。
否则,若对于多边形的任意一条边 P_iP_{i+1} ,A 都在有向边 \bm {P_iP_{i+1}} 的左侧,则称 A 在 P 内侧。
否则,称 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} 的右侧(或在直线 CP_i 上),弹出栈顶(即 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;
}
```