37.计算几何入门
计算几何入门
初高中解析几何关注理论求解,通过代数方法研究几何性质。算法竞赛中的计算几何则面临实际计算问题:
- 数值精度:浮点数运算存在误差,简单几何判断可能因精度问题失败
- 算法实现:将几何概念转化为可编程的算法步骤
- 避免除法:解析几何大量使用除法,而在计算机的浮点运算中,除法损失精度严重,计算几何的精髓在于尽可能用乘法代替除法,避免精度损失。
点积与叉积、点线关系、多边形与简单多边形、简单多边形面积、凸包、半平面交。
点积与叉积
为什么学点积与叉积?
点积和叉积是计算几何的基础工具,几乎所有几何问题都离不开它们:
- 点积:判断向量夹角、投影长度、距离计算
- 叉积:判断方向、计算面积、判断点线关系
解析几何中判断点线关系需要除法(如斜率比较),而计算几何用叉积的符号判断,完全避免除法运算。
点积(Dot Product)
定义:\(\vec{a} \cdot \vec{b} = |a||b|\cos\theta = a_x b_x + a_y b_y\)
公式推导:
设 \(\vec{a} = (a_x, a_y)\),\(\vec{b} = (b_x, b_y)\),夹角为 \(\theta\)。
为什么 \(a_x b_x + a_y b_y = |a||b|\cos\theta\)?
\(\vec{a} \cdot \vec{b} =|a||b|\cos\theta\) 的推导:
- 直接计算:\(\vec{a} \cdot \vec{b} = a_x b_x + a_y b_y\)
- 利用余弦定理:\(|\vec{a} - \vec{b}|^2 = |\vec{a}|^2 + |\vec{b}|^2 - 2|\vec{a}||\vec{b}|\cos\theta\)
- 展开左边:\(|\vec{a} - \vec{b}|^2 = (a_x - b_x)^2 + (a_y - b_y)^2 = a_x^2 + a_y^2 + b_x^2 + b_y^2 - 2(a_x b_x + a_y b_y)\)
- 对比得到:\(a_x b_x + a_y b_y = |a||b|\cos\theta\)
\(\vec{a} \cdot \vec{b}=a_x b_x + a_y b_y\) 的推导:
- 在直角坐标系中,\(x\) 轴和 \(y\) 轴是垂直的,所以 \(\vec{i} \cdot \vec{i} = 1\),\(\vec{j} \cdot \vec{j} = 1\),\(\vec{i} \cdot \vec{j} = 0\)
- 向量 \(\vec{a} = a_x \vec{i} + a_y \vec{j}\),\(\vec{b} = b_x \vec{i} + b_y \vec{j}\)
- 利用点积的分配律:\(\vec{a} \cdot \vec{b} = (a_x \vec{i} + a_y \vec{j}) \cdot (b_x \vec{i} + b_y \vec{j}) = a_x b_x + a_y b_y\)
直观理解:点积就是两个向量对应坐标相乘再相加,结果等于长度乘以夹角的余弦。
1 | struct Point { |
叉积(Cross Product)
定义:\(\vec{a} \times \vec{b}\) 是一个向量,方向垂直于 \(\vec{a}\) 和 \(\vec{b}\) 所在平面,大小等于以这两个向量为邻边的平行四边形的面积。在二维平面中,叉积方向垂直于平面,用正负号表示方向:当 \(\vec{b}\) 在 \(\vec{a}\) 的逆时针方向时,叉积为正;当 \(\vec{b}\) 在 \(\vec{a}\) 的顺时针方向时,叉积为负。
正负判断(右手定则):右手四指从 \(\vec{a}\) 的方向弯向 \(\vec{b}\) 的方向,大拇指指向的方向就是叉积的方向。如果大拇指指向屏幕外(靠近观察者),则叉积为正;如果指向屏幕内(远离观察者),则叉积为负。
\(\vec{a} \times \vec{b} = |a||b|\sin\theta = a_x b_y - a_y b_x\)
\(\vec{a} \times \vec{b} = |a||b|\sin\theta\) 的推导:
- 平行四边形面积 = 底 × 高 = \(|a| \times (|b|\sin\theta) = |a||b|\sin\theta\)
\(\vec{a} \times \vec{b} = a_x b_y - a_y b_x\) 的推导:
- 在直角坐标系中,\(\vec{i} \times \vec{i} = 0\),\(\vec{j} \times \vec{j} = 0\),\(\vec{i} \times \vec{j} = 1\),\(\vec{j} \times \vec{i} = -1\)
- 向量 \(\vec{a} = a_x \vec{i} + a_y \vec{j}\),\(\vec{b} = b_x \vec{i} + b_y \vec{j}\)
- 利用叉积的分配律:\(\vec{a} \times \vec{b} = (a_x \vec{i} + a_y \vec{j}) \times (b_x \vec{i} + b_y \vec{j}) = a_x b_y - a_y b_x\)
1 | struct Point { |
点线关系
浮点精度与 dcmp
计算几何中大量使用浮点数,直接比较浮点数是否相等容易出错。常用技巧是设定一个极小的正数
\(\varepsilon\)(eps),用
dcmp
函数判断浮点数的正负和是否接近零:
1 | const double eps = 1e-8; |
\(|x| < \varepsilon\) 视为 \(x=0\)
\(x > \varepsilon\) 视为正,\(x < -\varepsilon\) 视为负
这样可以有效避免浮点误差带来的判断错误
角度与弧度
角度:常用单位,360°为一圈
弧度:数学和计算几何中常用,\(2\pi\)(大约6.28)为一圈
换算:\(1\text{弧度} = 180/\pi\text{度}\),\(1\text{度} = \pi/180\text{弧度}\)
注意:计算几何中所有三角函数、旋转等都用弧度
三点共线
三点\(A,B,C\)共线,当且仅当\(\overrightarrow{AB}\)和\(\overrightarrow{AC}\)平行。用叉积判断:
\(\overrightarrow{AB} \times \overrightarrow{AC} = 0\) 时三点共线。
叉积为零说明两个向量方向一致或相反。
直观理解:\(|\overrightarrow{AB} \times \overrightarrow{AC}| = |AB| \cdot |AC| \cdot \sin\theta\),三点共线时\(\theta=0\)或\(\pi\),所以结果为0。
1 | inline bool InLine(const Point &a, const Point &b, const Point &c) { |
点到直线距离
点\(P\)到直线\(AB\)的距离,其实就是\(|\overrightarrow{AP}|\)在垂直于\(AB\)方向上的投影长度。
公式:\(\text{距离} = |\overrightarrow{AP}| \cdot \sin\theta\),其中\(\theta\)是\(AP\)与\(AB\)的夹角。
叉积的几何意义:\(|\overrightarrow{AB} \times \overrightarrow{AP}| = |AB| \cdot |AP| \cdot \sin\theta\),所以距离等于\(\frac{|\overrightarrow{AB} \times \overrightarrow{AP}|}{|AB|}\)。
1 | double PointToLine(const Point &p, const Point &a, const Point &b) { |
点绕点逆时针旋转
点\(P\)绕\(O\)逆时针旋转\(\theta\)弧度,利用旋转矩阵:
- \(x' = \cos\theta(x - x_0) -
\sin\theta(y - y_0) + x_0\)
- \(y' = \sin\theta(x - x_0) + \cos\theta(y - y_0) + y_0\)
旋转矩阵本质:
旋转矩阵是一种将点绕原点(或某个点)旋转一定角度后,得到新坐标的线性变换工具。它保证旋转后点到中心的距离不变,只改变与\(x\)轴的夹角。
(本节不展开推导,了解其作用即可。)
二维旋转矩阵的形式:
\[ \begin{bmatrix} x' \\ y' \end{bmatrix} = \begin{bmatrix} \cos\theta & -\sin\theta \\ \sin\theta & \cos\theta \end{bmatrix} \begin{bmatrix} x \\ y \end{bmatrix} \]
也就是说,旋转后的新坐标 \([x', y']\),等于"旋转矩阵"与原坐标 \([x, y]\) 的矩阵乘法结果。
直观理解:
- 旋转矩阵是一个 \(2 \times 2\)
的方阵。 - 只要用矩阵乘法,就能把任意点绕原点旋转任意角度。
1 | Point Rotate(const Point &p, const Point &o, double theta) { |
两直线平行
两直线\(AB\)与\(CD\)平行,当且仅当方向向量\(\overrightarrow{AB}\)与\(\overrightarrow{CD}\)平行。
用叉积判断:\(\overrightarrow{AB} \times \overrightarrow{CD} = 0\)
直观理解:\(|\overrightarrow{AB} \times \overrightarrow{CD}| = |AB| \cdot |CD| \cdot \sin\theta\),平行时\(\theta=0\)或\(\pi\),所以结果为0。
1 | bool Parallel(const Point &a, const Point &b, const Point &c, const Point &d) { |
两直线交点
设直线\(AB\)和\(CD\)不平行,求交点:
利用参数方程和叉积,推导出交点公式
原理解释:
\(t\) 表示 \(P\) 在 \(AB\) 上的位置比例,利用"面积比"来定位交点。
公式推导:
\[ t = \frac{(\vec{A} - \vec{C}) \times (\vec{D} - \vec{C})}{(\vec{B} - \vec{A}) \times (\vec{D} - \vec{C})} \]回想叉积公式,分子分母都对应的 底×高
\[ P = \vec{A} + t(\vec{B} - \vec{A}) \]
注意:分母为零时,说明两直线平行或重合。
直观理解:交点是两条直线"方向量"线性组合的结果。
1 | Point LineCross(const Point &a, const Point &b, const Point &c, const Point &d) { |
线段相交
判断线段\(AB\)与\(CD\)是否相交:
判断两端点分别在对方线段的两侧(叉积符号不同或为零)
直观理解:如果\(C\)、\(D\)在\(AB\)两侧,\(A\)、\(B\)在\(CD\)两侧,则必有交点。
1 | bool SegCross(const Point &a, const Point &b, const Point &c, const Point &d) { |
点到线段距离
点\(P\)到线段\(AB\)的距离:
- 若投影在线段外,取到端点距离
- 若投影在线段内,取到直线距离
1 | double PointToSeg(const Point &p, const Point &a, const Point &b) { |
多边形与简单多边形
多边形:由有限个点按顺序连接而成的封闭图形。
凸多边形:任意两点连线都在多边形内部的多边形。
简单多边形:边与边只在顶点相交,不自交的多边形,可以是"凹"的。
凸多边形是简单多边形,简单多边形不一定是凸多边形
判断点在多边形内
射线法:从点向右发射水平射线,统计与多边形边界的交点数。
- 交点数为奇数:点在多边形内
- 交点数为偶数:点在多边形外
改进的射线法:处理边界情况,避免浮点误差。
1 | bool InPolygon(const Point &p, Point poly[], int n) { |
算法原理详解:
- 射线方向:从测试点P向右(x轴正方向)发射水平射线
- 叉积判断方向:
p1.cross(p2, p)
判断点p相对于边p1→p2的位置k > 0
:p在边的左侧k < 0
:p在边的右侧k = 0
:p在边上
- y坐标比较:
d1 = dcmp(p1.y - p.y)
,d2 = dcmp(p2.y - p.y)
d1 <= 0 && d2 > 0
:边的起点在射线下方或同高,终点在射线上方d2 <= 0 && d1 > 0
:边的终点在射线下方或同高,起点在射线上方
- flag计数规则:
flag++
:射线从下向上穿过边,且p在边的左侧flag--
:射线从下向上穿过边,且p在边的右侧- 最终
flag != 0
表示点在多边形内
为什么这样设计: - 避免了射线与顶点重合的边界情况 - 通过叉积方向判断,确保计数准确性 - 处理了凹多边形的复杂情况
多边形与圆的关系
点在圆内:点到圆心距离小于半径。
线段与圆相交:线段到圆心距离小于半径,且线段两端点不全在圆内。
多边形与圆相交:多边形有边与圆相交,或有顶点在圆内。
1 | bool CircleIntersectPolygon(Point poly[], int n, Point center, double r) { |
简单多边形面积
有向面积:多边形面积有正负,逆时针为正,顺时针为负。
叉积法:利用叉积计算多边形面积,公式为:
\[ S = \frac{1}{2} \sum_{i=0}^{n-1} \vec{P_i} \times \vec{P_{i+1}} \]
其中 \(P_n = P_0\)(首尾相连)。
直观理解:将多边形分解为多个三角形,每个三角形的面积用叉积计算。

简单多边形面积的三角形分解过程中,某些三角形(如右图 P0-P3-P4)会出现"负面积"。这是因为这些三角形的顶点顺序为顺时针,面积公式自动赋予其负号。几何上,有"容斥"的味道:重叠部分被减去,最终得到正确的多边形面积。
多边形面积公式的优美之处就在于,无论凸多边形还是"凹"多边形,所有三角形的正负自动抵消,保证结果正确。
格林公式的离散形式:标准叉积公式 \(S = \frac{1}{2} \sum_{i=0}^{n-1} (x_i y_{i+1} - x_{i+1} y_i)\) 可以通过格林公式变形为更高效的形式。
变形过程: 1. 原始叉积:对于每条边 \(P_i \rightarrow P_{i+1}\),计算 \(x_i \cdot y_{i+1} - x_{i+1} \cdot y_i\) 2. 重新组织:将每个顶点的贡献分离,对于顶点 \(P_i(x_i, y_i)\),它参与的项有: - 作为起点:\(x_i \cdot y_{i+1}\)(贡献为正) - 作为终点:\(-x_{i+1} \cdot y_i\)(贡献为负) 3. 合并同类项:顶点 \(P_i\) 的总贡献 = \(x_i \cdot y_{i+1} - x_{i+1} \cdot y_i = y_i \cdot (x_{i-1} - x_{i+1})\)
优化效果:这样变形后,每个顶点只需要计算一次 \(y_i \cdot (x_{i-1} -
x_{i+1})\),避免了重复的模运算(如
(i+1) % n
),提高了计算效率。
1 | double PolygonArea(Point p[], int n) { |
凸包(Convex Hull)
定义:包含所有点的最小凸多边形。
Graham扫描法:
- 找到最左下角的点作为起始点
- 按极角排序其他点(以起始点为原点,按逆时针方向排序)
- 用栈维护凸包,每次加入新点时检查是否破坏凸性

1 | int Graham(Point p[], int n, Point res[]) { |
凸包的应用
- 最远点对:凸包上的点对可能是最远点对
- 最小包围圆:凸包的最小包围圆就是点集的最小包围圆
- 碰撞检测:两个凸多边形的碰撞检测比一般多边形简单
半平面交(Half Plane Intersection)
定义:多个半平面的交集,结果是一个凸多边形(可能为空)。
半平面:由一条直线分割平面得到的两个区域之一。
半平面表示
半平面由一条有向直线表示,即一个向量。向量的一侧为标记的半平面,通常约定为向量的左侧。
极角排序:对直线的方向向量进行极角排序。每条直线 \(s \rightarrow e\) 的方向向量 \(\vec{v} = (e_x - s_x, e_y - s_y)\) 与 \(x\) 轴正方向的夹角就是极角。极角小的排在前面,相同时距离原点近的排在前面。
1 | struct Line { |
算法步骤
- 排序:将所有半平面按极角排序
- 去重:去除极角相同的半平面
- 双端队列维护:用双端队列维护半平面交的边界
- 更新队列:每次加入新的半平面时,更新队列
1 | bool HalfPlaneIntersection(Line l[], int n, Point cp[], int &m) { |
多边形边平移求新核
1 | // 将点沿a->b方向左侧垂直平移L |
半平面交的应用
- 多边形求交:两个凸多边形的交集
- 碰撞检测:多个障碍物的安全区域
- 多边形收缩:将多边形边向内平移,求新的边界
模板参考
https://github.com/CSGrandeur/icpc_solution/blob/master/templates/ComputationalGeometry.md