试着用计算机来解决几何问题吧(●’◡’●)
前置知识
- 几何基础
- 平面直角坐标系
- 极坐标与极坐标系
- 向量(向量积)
二维计算几何基础
图形的表示
点与线的表示
例图:

typedef double ld;typedef pair<ld, ld> pld;typedef pair<ll, ll> pll;
struct point { ld x, y; point operator+(const point& p) const { // 加 return { x + p.x, y + p.y }; } point operator-(const point& p) const { // 减 return { x - p.x, y - p.y }; } point operator*(const ld p) const { // 数乘 return { x * p, y * p }; } point operator/(const ld p) const { // 除 return { x / p, y / p }; }};
struct line { point s, t;};
point A(1, 0), B(3, 1), delta = B - A;line l(A, B);距离与旋转
例图:

ld sqr(ld x) { return x * x; }
struct point { ... point rotate(ld t)const { return { x * cos(t) - y * sin(t), x * sin(t) + y * cos(t) }; } point rot90()const { return { -y,x }; } point unit()const { return *this / sqrt(sqr(x) + sqr(y)); }};
ld dis(const point& a, const point& b) { return sqrt(sqr(a.x - b.x) + sqr(a.y - b.y));}
point C = A + (B - A).rot90();向量旋转解析
设$\vec{a}=(x,y)$,倾角为$\theta$,长度为$l=\sqrt{x^2+y^2}$。则$x=l\cos{\theta},y=l\sin{\theta}$,顺时针旋转后得到$\vec{b}=(l\cos{\theta+\alpha},l\sin{\theta+\alpha})$。
三角恒等变化:
$$ \vec{b}=(\cos{(\theta+\alpha)}l,\sin{(\theta+\alpha)}l) $$
$$ =(l(\cos{\theta}\cos{\alpha-sin{\theta}sin{\alpha}}),l(\sin{\theta}\cos{\alpha}+\sin{\alpha}\cos{\theta})) $$
$$ =(x \cos{\alpha-y \sin{\alpha},y \cos{\alpha}+x \sin{\alpha}}) $$
点积与叉积
例图:

ld dot(const point& a, const point& b) { return a.x * b.x + a.y * b.y;}ld det(const point& a, const point& b) { return a.x * b.y - a.y * b.x;}- 点积:也叫数量积、内积。几何意义是一个向量在另一个向量上的投影再乘第二个向量的模长,是一个实数,有交换律。
- 若同向($\theta = 0°$),为模长之积。
- 若锐角($\theta \lt 90°$),数量积为正
- 若直角($\theta = 90°$),数量积为 0
- 若钝角($\theta \gt 90°$),数量积为负
- 若反向($\theta = 180°$),为模长之积的相反数
- 叉积:也叫外积,几何意义是两向量围成的平面四边形的面积。是一个向量,其方向$\vec{a}\times \vec{b}$:用右手从$\vec{a}$沿着不大于平角的方向向$\vec{b}$ 旋转,拇指方向是外积的方向。没有交换律。
- 若平行,外积为$\vec{0}$
- 共起点后,对于$\vec{a}\times \vec{b}$,若$\vec{a}$在$\vec{b}$的右侧,外积为正,否则外积为负(以纸面为参考,$\vec{a}$往$\vec{b}$是逆时针为正,顺时针为负)
由向量外积可以判断两向量的旋转关系、方便求出点到直线的距离。适用于凸包和旋转卡壳。
观察两个式子:
-
投影:$dot=|A||B|\cosθ$
-
面积:$det=|A||B|\sinθ$
可以发现点积与叉积的正负由角度决定,故根据点积和叉积的正负,可以判断向量夹角的象限。

const ld eps = 1e-8;int sgn(ld x) { if (fabs(x) < eps) return 0; return x < 0 ? -1 : 1;}
bool turn_left(const point& a, const point& b, const point& c) { return sgn(det(b - a, c - a)) > 0;}
bool same_dir(const line& a, const line& b) { return sgn(det(b.t - b.s, a.t - a.s)) == 0 && sgn(dot(b.t - b.s, a.t - a.s)) > 0;}判等判交求交
浮点数等于零
注意浮点数的精度误差,一般用sgn()来实现相等判断
const ld eps = 1e-8;int sgn(ld x) { if (fabs(x) < eps) return 0; return x < 0 ? -1 : 1;}点判等
也是一样要注意精度捏
friend bool operator==(const point& a, const point& b) { return sgn(a.x - b.x) == 0 && sgn(a.y - b.y) == 0;}点在线段上
点在线段上有两个要求:
- 点在直线上:叉积判断共线
- 点在两端点上:点积判断方向

bool point_on_segment(const point& a, const line& l) { // 叉乘为0,点积小于0;l:BC, a:A BA?//AC return sgn(det(a - l.s, l.t - a)) == 0 && sgn(dot(a - l.s, l.t - a)) >= 0;}- 叉乘为 0 则共线(0° 或 180°)
注意:可能需要处理线段退化的情况:线段退化时直接调用会返回true。
线段退化情况在调用之前自行判断。
线段判交
线段有交的两种情况:
- 一条线段的端点在另一条线段上。
- 互相严格跨立。
第一种使用point_on_segment()处理;
第二种用叉积判断角度异号。

// l:AB, c:C, d:D;ABxAC * ABxAD < 0,表明旋转方向不同(两点在线段两侧)bool two_side(const point& c, const point& d, const line& l) { return sgn(det(l.t - l.s, c - l.s)) * sgn(det(l.t - l.s, d - l.s)) < 0;}bool inter_judge(const line& a, const line& b) { if (point_on_segment(a.s, b) || point_on_segment(a.t, b) || point_on_segment(b.s, a) || point_on_segment(b.t, a)) return true; return two_side(a.s, a.t, b) && two_side(b.s, b.t, a);}几种测试情况思考:
![]() | ![]() |
|---|---|
![]() | ![]() |
直线求交
求两条直线的交点
直线和直线的位置关系有三种:
- 平行
- 有一个交点
- 重合(共线)
判断位置关系
思路:
if(平行) { if(过同一个点) { return 两只直线重合; } else { return 两直线平行; }}else { return 两直线相交;}判断平行(包括重合):叉积是否为 0
bool parallel_judge(const line& a, const line& b) { return sgn(det(a.t - a.s, b.t - b.s)) == 0}判断共线:平行且交换端点后也平行
bool collinear_judge(const line& a, const line& b) { return parallel_judge(a, b) && sgn(det(a.t - a.s, b.t - a.s)) == 0;}两直线相交求交点
使用面积计算等高三角形底边的比例求交点。
注意:这里使用了除法,除法会导致精度严重下降(通常 epsilon 就是为了克服除法的误差而引入的)

这里$\triangle ABC$与$\triangle ABD$共底,面积之比(叉积之比)即为 CK 与 KD 的长度之比。则:
对$K(k_x,k_y),C(c_x,c_y),D(d_x,d_y)$,有:
$$ CK=\frac{u}{u+v}CD $$
so:K 是 CD 的一个定比分点。
point line_intersect(const line& a, const line& b) { ld u = det(a.t - a.s, b.s - a.s); ld v = det(a.t - a.s, b.t - a.s); return (b.s * v + b.t * u) / (v + u);}射线求交(留思考
两条射线之间的位置关系:
- 平行(不重合)
- 共线同向(部分重合或完全重合)
- 共线反向且不重合
- 共线反向且部分重合
- 相交
求交点
两条射线如果有相交交点的话(情况 5),这个交点一定是两条射线所在直线的交点,用前文的方法求即可。
判断相交
对于射线来说,若有交点,则交点一定在两条射线所在直线上,射线可以看作其所在直线的一部分,那么只要判断这个交点是否同时在两条射线的延长线方向即可。
距离
点到线段距离
点到线短距离有两种情况:
- 垂线段长度
- 到某个端点的距离
- 注意线段退化问题
ld point_to_line(const point& p, const line& l) { if (sgn(dis(l.s, l.t)) == 0) // 线段退化为点 return dis(p, l.s); return fabs(det(p - l.s, l.t - l.s)) / dis(l.s, l.t);}
ld point_to_segment(const point& p, const line& l) { if (sgn(dot(p - l.s, l.t - l.s)) < 0) return dis(p, l.s); if (sgn(dot(p - l.t, l.s - l.t)) < 0) return dis(p, l.t); return point_to_line(p, l);}凸包
能包含所有给定点的最小凸多边形的叫做凸包
- 凸多边形:每个内角在$[0,\pi)$内的简单多边形;如果允许非严格则是$[0,\pi]$
- 或者,点集所有可能的带权平均点集合为凸包。
求凸包
有两种求法:Graham 算法和Andrew 算法,两种算法的时间复杂度都是$O(n\log n)$。区别在于对点的排序方式不同。
Andrew 算法
对点的排序方法:按照$x$为第一关键字,$y$为第二关键字,对点集进行排序,排序完成后,易知:第 1 个点和第 n 个点一定在凸包点集里。
可以分别求出下凸壳和上凸壳,求上下两半时也不会互相影响。
算法:用栈来维护在凸包上的点
- 逆时针先求下凸壳:第 1 个点和第 2 个先入栈,当加入更多点时,设栈中的倒数第二个点为$A$,最后一个点为$B$,新加入的点为$C$,若$B$在$\vec{AC}$的左边(或在线上),则将$B$弹出,令$C$入栈。
- 接下来依然是逆时针求上凸壳,上述求下凸包时,最后入栈的点一定是$n$号点,在加入$n-1$ 号点后,我们继续上一步的一模一样的操作:判断旧点和新向量的位置关系再做取舍。
- 注意,求上凸壳的时候依然是遍历到 1 号点,此时会有首尾相同,点重复的问题。
凸包的周长:($k$为凸包中点的个数)
$$ C=\sum_{i=0}^{i=k-1}dis(p_{i},p_{(i+1) \mod{k}}) $$
模版代码:
ll Andrew() { // 求凸包并算面积 // stk[]是整型,存的是下标 // p[]存储向量或点 tp = 0; sort(p + 1, p + 1 + n); // 对点进行排序 stk[++tp] = 1; for (int i = 2; i <= n; ++i) { while (tp >= 2 && cross((p[stk[tp]] - p[stk[tp - 1]]), (p[i] - p[stk[tp - 1]])) <= 0) { // =代表严格 used[stk[tp--]] = 0; } used[i] = 1; stk[++tp] = i; }
ll tmp = tp;
for (int i = n - 1; i > 0; --i) { if (!used[i]) { while (tp > tmp && cross((p[stk[tp]] - p[stk[tp - 1]]), (p[i] - p[stk[tp - 1]])) <= 0) { used[stk[tp--]] = 0; } used[i] = 1; stk[++tp] = i; } } for (int i = 1; i <= tp; ++i) { convex[i] = p[stk[i]]; }
ll s = 0;
for (int i = 3; i < tp; i++) { point l1 = convex[i - 1] - convex[1], l2 = convex[i] - convex[1]; s += abs(cross(l1, l2)); }
return s;}Graham 算法
取左下角的点为基准,对其余点进行逆时针排序。
- 由于其他点相对于基准点在$[0,\pi )$的半平面内,因此可以直接使用叉积排序
- 不要使用
atan2:当值域很大时,精度难以区分相近的点 - 注意处理极角序相同的点:按照到基准点的距离从小到大排序。
用一根细线尝试绕过所有点。
- 需要弹出不满足凸性的点
- 使用单调栈实现
ll Graham() { // 求凸包并算面积 // stk[]是整型,存的是下标 // p[]存储向量或点 tp = 0; sort(p + 1, p + 1 + n, [](const point &a, const point &b) { return a.y == b.y ? a.x < b.x : a.y < b.y; }); // 对点进行排序 stk[++tp] = 1; for (int i = 2; i <= n; ++i) { while (tp >= 2 && cross((p[stk[tp]] - p[stk[tp - 1]]), (p[i] - p[stk[tp - 1]])) < 0) { // =代表严格 used[stk[tp--]] = 0; } used[i] = 1; stk[++tp] = i; }
ll tmp = tp;
for (int i = n - 1; i > 0; --i) { if (!used[i]) { while (tp > tmp && cross((p[stk[tp]] - p[stk[tp - 1]]), (p[i] - p[stk[tp - 1]])) < 0) { used[stk[tp--]] = 0; } used[i] = 1; stk[++tp] = i; } } for (int i = 1; i <= tp; ++i) { convex[i] = p[stk[i]]; }
ll s = 0;
for (int i = 3; i < tp; i++) { point l1 = convex[i - 1] - convex[1], l2 = convex[i] - convex[1]; s += abs(cross(l1, l2)); }
return s;}两种算法相比
Graham 不够好:相对较慢,容易写错
- 相对较慢:排序需要计算$O(n\log n)$次叉积。
- 任意写错:细节比较多,任意出现挂边界的情况。
Andrew 算法扫描两遍计算出上下凸壳,通常在效率和实现上相比 Graham 有优势。
凸包典题:
[P2742 USACO5.1] 圈奶牛 Fencing the Cows /【模板】二维凸包 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
[P3829 SHOI2012] 信用卡凸包 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
三分
二分要求单调性,最基本的应用是求一个单调函数的零点。
三分是二分的变种,它的基本用途是求单峰函数的极值点。
三分的原理:以求极大值为例。每次对一个区间$[l,r]$求三等分点$lp$和$rp$:
- 如果$f(lp)\lt f(rp)$,说明极大值一定在$[lp,r]$内取到,因为如果在$[0,lp]$内,那$rp$一定处于单调下降的区间内,它的函数值不可能大于$f(lp)$,于是我们令$l=lp$
- 如果$f(lp)\gt f(rp)$,同理,极大值一定在$[l,rp]$内取到,令$r=rp$
这样进行下去,直到$fabs(l-r)\lt eps$ 为止,如果是求极小值,只需要把处于判断处的大于小于互换。
ld three_section_max(ld l, ld r, function<ld(ld)> f) { while (r - l > eps) { ld m1 = l + (r - l) / 3; ld m2 = r - (r - l) / 3; if (f(m1) < f(m2)) { l = m1; } else { r = m2; } } return f(l);}// 三分求极小值ld three_section_min(ld l, ld r, function<ld(ld)> f) { while (r - l > eps) { ld m1 = l + (r - l) / 3; ld m2 = r - (r - l) / 3; if (f(m1) > f(m2)) { l = m1; } else { r = m2; } } return f(l);}优化
按照上面的算法,每次减少三分之一的长度,但其实还可以通过在中点附近取点来优化,这样每次可以减少约二分之一的长度。
while (r - l > eps) { ld mid = (l + r) / 2; ld fl = f(mid - eps), fr = f(mid + eps); if (fl < fr) { r = mid; } else { l = mid; }}题单部分
基础
-
P1355 神秘大三角 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn) // 这题莫名卡输入格式,坏!
一些子问题 Q
- 如何求一组点中用一条直线穿过的最多的点数?$O(n^3)$做法、$O(n^2logm)$做法
难一些的
模版代码
#include<bits/stdc++.h>using namespace std;typedef long long ll;typedef double ld;typedef pair<ld, ld> pld;typedef pair<ll, ll> pll;const ld eps = 1e-18;
ld sqr(ld x) { return x * x; }
int sgn(ld x) { if (fabs(x) < eps) return 0; return x < 0 ? -1 : 1;}
struct point { ld x, y;
point operator+(const point& p) const { // 加 return { x + p.x, y + p.y }; } point operator-(const point& p) const { // 减 return { x - p.x, y - p.y }; } point operator*(const ld p) const { // 乘 return { x * p, y * p }; } point operator/(const ld p) const { // 除 return { x / p, y / p }; }
friend bool operator==(const point& a, const point& b) { return sgn(a.x - b.x) == 0 && sgn(a.y - b.y) == 0; }
point rotate(ld t)const { return { x * cos(t) - y * sin(t), x * sin(t) + y * cos(t) }; } point rot90()const { return { -y,x }; } point unit()const { // 单位向量 return *this / sqrt(sqr(x) + sqr(y)); }};
struct line { point s, t; //s起点 t终点};
point A(1, 0), B(3, 1), delta = B - A;line l(A, B);
ld dis(const point& a, const point& b) { return sqrt(sqr(a.x - b.x) + sqr(a.y - b.y));}
point C = A + (B - A).rot90();
ld dot(const point& a, const point& b) { return a.x * b.x + a.y * b.y;}ld det(const point& a, const point& b) { return a.x * b.y - a.y * b.x;}
bool turn_left(const point& a, const point& b, const point& c) { return sgn(det(b - a, c - a)) > 0;}
bool same_dir(const line& a, const line& b) { return sgn(det(b.t - b.s, a.t - a.s)) == 0 && sgn(dot(b.t - b.s, a.t - a.s)) > 0;}
bool parallel(const line& a, const line& b) { return sgn(det(b.t - b.s, a.t - a.s)) == 0;}
bool point_on_segment(const point& a, const line& l) { // 叉乘为0,点积小于0;l:BC, a:A BA?//AC return sgn(det(a - l.s, l.t - a)) == 0 && sgn(dot(a - l.s, l.t - a)) >= 0;}
// l:AB, c:C, d:D;ABxAC * ABxAD < 0,表明旋转方向不同(两点在线段两侧)bool two_side(const point& c, const point& d, const line& l) { return sgn(det(l.t - l.s, c - l.s)) * sgn(det(l.t - l.s, d - l.s)) < 0;}bool inter_judge(const line& a, const line& b) { if (point_on_segment(a.s, b) || point_on_segment(a.t, b) || point_on_segment(b.s, a) || point_on_segment(b.t, a)) return true; return two_side(a.s, a.t, b) && two_side(b.s, b.t, a);}// 判断平行(包括重合)bool parallel_judge(const line& a, const line& b) { return sgn(det(a.t - a.s, b.t - b.s)) == 0;}// 判断共线bool collinear_judge(const line& a, const line& b) { return parallel_judge(a, b) && sgn(det(a.t - a.s, b.t - a.s)) == 0;}
point line_intersect(const line& a, const line& b) { ld u = det(a.t - a.s, b.s - a.s); ld v = det(a.t - a.s, b.t - a.s); return (b.s * v + b.t * u) / (v + u);}
// bool ray_intersect_judge(const line& a, const line& b) {// // TODO: finish this// }
ld point_to_line(const point& p, const line& l) { if (sgn(dis(l.s, l.t)) == 0) // 线段退化为点 return dis(p, l.s); return abs(det(p - l.s, l.t - l.s)) / dis(l.s, l.t);}
ld point_to_segment(const point& p, const line& l) { if (sgn(dot(p - l.s, l.t - l.s)) < 0) return dis(p, l.s); if (sgn(dot(p - l.t, l.s - l.t)) < 0) return dis(p, l.t); return point_to_line(p, l);}
ld segment_to_segment(const line& a, const line& b) { if (inter_judge(a, b)) return 0; return min({ point_to_segment(a.s, b), point_to_segment(a.t, b), point_to_segment(b.s, a), point_to_segment(b.t, a) });}
vector<point>Andrew(vector<point>& p) { sort(p.begin(), p.end(), [](const point& a, const point& b) { return a.x == b.x ? a.y < b.y : a.x < b.x; }); vector<point> res; for (int i = 0; i < p.size(); i++) { // 下凸壳 while (res.size() > 1 && !turn_left(res[res.size() - 2], res.back(), p[i])) res.pop_back(); res.push_back(p[i]); } int k = res.size(); for (int i = p.size() - 2; i >= 0; i--) { // 上凸壳 while (res.size() > k && !turn_left(res[res.size() - 2], res.back(), p[i])) res.pop_back(); res.push_back(p[i]); } res.pop_back(); // 删除重复的点,即首尾相同 return res;}
bool turn_left(const point& a, const point& b, const point& c) { // 逆时针 return sgn(det(b - a, c - a)) > 0;}
vector<point> Graham(vector<point>& p) { point base = *min_element(p.begin(), p.end(), [](const point& a, const point& b) { return a.y == b.y ? a.x < b.x : a.y < b.y; }); sort(p.begin(), p.end(), [&](const point& u, const point& v) { int s = sgn(det(u - base, v - base)); if (s)return s > 0; return sgn(dis(u, base) - dis(v, base)) < 0; }); vector<point>res; for (auto i : p) { while (res.size() > 1 && !turn_left(res[res.size() - 2], res.back(), i)) res.pop_back(); res.push_back(i); } return res;}
// 求凸包的周长ld convex_perimeter(vector<point>& p) { ld res = 0; for (int i = 0; i < p.size(); i++) res += dis(p[i], p[(i + 1) % p.size()]); return res;}
void solve() {}
int main() { solve();
return 0;}



Comments
Quiet notes for this article.