计算几何专题
蒟蒻的计算几何几乎不会,这里记录一些计算几何的常见概念,以作备忘:
三角函数与弧度制
弧度制:
- 使用 sin()、cos()、tan() 时,参数传入弧度制;
- 使用 asin()、acos()、atan() 时,参数传入 sin/cos/tan 值,返回值为弧度制。
向量
可以用一个二维坐标 \((x, y)\) 表示。比如点 \(A = (x, y)\),则 \(\overrightarrow{OA} = (x, y)\)
\(A(x_{1}, y_{1}), B(x_{2}, y_{2})\) \(\rightarrow\) \(\overrightarrow{AB} = (x_{2} - x_{1}, y_{2} - y_{1})\)
运算:\(\overrightarrow{AB} = \overrightarrow{OB} - \overrightarrow{OA}\)
二维向量运算可以看作是 \(x,y\) 两个方向上的加减运算复合,三维同理。
代码:
// 注意,Point类型既可以表示二维平面中的一个点,也可以表示一个向量
struct Point{
double x, y;
Point(double x, double y): x(x), y(y){}
};
// 向量加法
Point operator + (Point a, Point b){
return Point(a.x + b.x, a.y + b.y);
}
// 向量减法
Point operator - (Point a, Point b){
return Point(a.x - b.x, a.y - b.y);
}
// 数乘
Point operator * (Point a, double t){
return Point(a.x * t, a.y * t);
}
点积
形式:\(\overrightarrow{a} \cdot \overrightarrow{b} = x_{1}x_{2} + y_{1}y_{2} = |a||b| \cos\theta\)
几何意义:\(\overrightarrow{b}\) 在 \(\overrightarrow{a}\) 上的投影 与 \(\overrightarrow{a}\) 的长度 的乘积
代码:
// 求点积
double dot(Point a, Point b){
return a.x * b.x + a.y * b.y;
}
// 求模长
double len(Point a){ // 注意这里a代表一个向量
return sqrt(a.x * a.x + a.y * a.y);
}
// 求夹角
double angle(Point a, Point b){
return acos(dot(a, b) / len(a) / len(b));
}
应用:
- 判断两向量垂直:\(\overrightarrow{a} \cdot \overrightarrow{b} = 0\)
- 判断两向量平行(共线):\(\overrightarrow{a} \cdot \overrightarrow{b} = |\overrightarrow{a}||\overrightarrow{b}|\)
- 求两向量的夹角:\(\cos \theta = \frac{\overrightarrow{a} \cdot \overrightarrow{b}}{|\overrightarrow{a}||\overrightarrow{b}|}\)
叉积
形式:\(\overrightarrow{a} \times \overrightarrow{b} = x_{1}y_{2} - x_{2}y_{1} = |a||b| \sin\theta\)
几何意义:\(\overrightarrow{a}\) 与 \(\overrightarrow{b}\) 张成的平行四边形的有向面积。\(\overrightarrow{b}\) 在 \(\overrightarrow{a}\) 的顺时针方向(右侧)为负;\(\overrightarrow{b}\) 在 \(\overrightarrow{a}\) 的逆时针方向(左侧)为正。
注意,这里的左,右侧是相对于朝着向量方向而言。
代码:
// 求叉积
double det(Point a, Point b){
return a.x * b.y - b.x * a.y;
}
// 向量ab 与 向量ac 的叉积
double cross(Point a, Point b, Point c){
return det(b - a, c - a);
}
// 判断点c在直线ab的哪一侧(1为左侧,-1为右侧, 0为三点共线)
int sgn(double x){
if(x == 0) return 0;
return (x > 0 ? 1 : -1);
}
int direction(Point a, Point b, Point c){
return sgn(cross(a, b, c));
}
// 判断直线ab与线段cd是否有交点:
bool iscross(Point a, Point b, Point c, Point d){
return cross(a, b, c) * cross(a, b, d) <= 0;
}
// 判断线段ab与线段cd是否有交点
bool iscross(Point a, Point b, Point c, Point d){
// !!!对于两条线段共线的情况需要特判
if(cross(a, b, c) == 0 && cross(a, b, d) == 0){ // ab 与 cd 共线,此时等价于在一维数轴上看两条线段是否相交
double l1, r1, l2, r2;
if(a.x == b.x){ // 比较 y
l1 = a.y, r1 = b.y, l2 = c.y, r2 = d.y;
}
else{ // 比较 x
l1 = a.x, r1 = b.x, l2 = c.x, r2 = d.x;
}
if(l1 > r1) swap(l1, r1);
if(l2 > r2) swap(l2, r2);
if(l1 > l2) swap(l1, l2), swap(r1, r2);
return l2 <= r1;
}
return LL(cross(a, b, c)) * cross(a, b, d) <= 0 && LL(cross(c, d, a)) * cross(c, d, b) <= 0;
}
// 得到直线au与直线bv的交点坐标(共线时分母为0,需特判)
Point getcrossnode(Point a, Point u, Point b, Point v){
double t = det((a - b), v) / det(v, u); // 比例
return a + u * t;
}
应用:
- 判定点线之间的位置关系
- \((\overrightarrow{b} - \overrightarrow{a}) \times (\overrightarrow{c} - \overrightarrow{a}) > 0\) \(\rightarrow\) 点 \(c\) 在向量 \(\overrightarrow{ab}\) 的左侧
- \((\overrightarrow{b} - \overrightarrow{a}) \times (\overrightarrow{c} - \overrightarrow{a}) < 0\) \(\rightarrow\) 点 \(c\) 在向量 \(\overrightarrow{ab}\) 的右侧
- \((\overrightarrow{b} - \overrightarrow{a}) \times (\overrightarrow{c} - \overrightarrow{a}) = 0\) \(\rightarrow\) 三点共线
- 判定线线之间的位置关系
- 判断线段 \(ab\) 与 线段 \(cd\) 是否有交点:(注意两条线段共线时需要特判!!!)
(1) \(cross(a, b, c) * cross(a, b, d) > 0\) 或 \(cross(c, d, a) * cross(c, d, b) > 0\) \(\rightarrow\) 无交点
(2)\(cross(a, b, c) * cross(a, b, d) <= 0\) 并且 \(cross(c, d, a) * cross(c, d, b) <= 0\) \(\rightarrow\) 有交点 - 求两个线段的交点 \(\rightarrow\) 见代码部分即可。证明见下图:
交点 \(O\) 可以利用点 \(a\) 在 \(\overrightarrow{au}\) 方向上的偏移得出,\(|au|\) 已知,故只要知道 \(\frac{|oa|}{|ua|}\) 即可。发现它们的关系恰为两个平行四边形的高之比,而底相同,故又等于两个平行四边形的面积之比,而平行四边形的面积可以利用叉积快速得出。
注:上述做法只是在求多边形面积的大小。由叉积的正负性可知,其实多边形的面积也有正负性之分。而 多边形面积的正负性 可以 区分多边形中每条边两侧分别是多边形的外部/内部(一个性质:若将多边形中的所有边按照顺时针或逆时针标注方向,则它们的 左/右侧 与 多边形外部/内部 是一一对应的)。
极角排序
两种方式:
- 利用 cmath 库中的 \(atan(x)\) 或 \(atan2(x, y)\) 函数,配合一些分类讨论算出每个点的极角,然后直接按照算出值的大小升序排序。
- 优点:上手难度低,容易理解
- 缺点:会卡精度
- 利用叉积和一些简单的分讨来构造一个 cmp 函数,作为 sort 的参数:
ll det(Point a, Point b, Point o){ // 计算向量oa与向量ob的叉积
return 1ll * (a.x - o.x) * (b.y - o.y) - 1ll * (b.x - o.x) * (a.y - o.y);
}
Point o; // 排序前设置o为极点
// 极角相等时返回0,只有b的极角严格>a时返回1
bool cmp(Point a, Point b){ // 供排序函数使用,使得列表中的向量按照极角大小升序
int ah = (a.y - o.y < 0 || (a.y - o.y == 0 && a.x - o.x < 0));
int bh = (b.y - o.y < 0 || (b.y - o.y == 0 && b.x - o.x < 0));
if (ah != bh) return ah < bh; // 若 a=0 && b=1,则 a 在 y正半轴,b 在 y负半轴;显然 a 的极角一定小于 b
return det(a, b, o) > 0; // a 与 b 的叉积 > 0 当且仅当 b 在 a 的逆时针方向,也就是 b 的极角大小严格大于 a
}
需要特别注意上面对 \(y\) 方向上下半区的处理:
最大的优点就是无精度损失。对于所有输入坐标为整数的情况,能用这种就用这种。
例题:ABC442 E
凸包
定义:在平面上能包含所有给定点的最小凸多边形,称作给定点集的凸包。
Andrew 算法
应用:给定 \(n\) 个点的坐标 \((x, y)\),\(O(n\log n)\) 求出所有点构成的凸包(算法时间复杂度瓶颈在于排序,后面利用栈求凸包的过程是 \(O(n)\) 的)。
P2742
求凸包周长,模板题。
const int N = 100005;
struct Point{
double x, y;
};
bool cmp(Point a, Point b){
if(a.x == b.x) return a.y < b.y;
return a.x < b.x;
}
Point p[N], stk[N];
int n, top;
// 求向量ab与向量ac的叉积
double cross(Point a, Point b, Point c){
return (b.x - a.x) * (c.y - a.y) - (c.x - a.x) * (b.y - a.y);
}
double distance(Point a, Point b){
return sqrt((a.x - b.x) * (a.x - b.x) + (a.y - b.y) * (a.y - b.y));
}
void Andrew(){ // 最终 stk 内按逆时针顺序存放凸包中的点
sort(p + 1, p + 1 + n, cmp); // 排序
for(int i = 1; i <= n; i ++){ // 下凸包
while(top >= 2 && cross(stk[top - 1], stk[top], p[i]) <= 0) top --;
stk[++top] = p[i];
}
int t = top;
for(int i = n - 1; i >= 1; i --){ // 上凸包
while(top > t && cross(stk[top - 1], stk[top], p[i]) <= 0) top --;
stk[++top] = p[i];
}
}
void solve()
{
cin >> n;
for(int i = 1; i <= n; i ++){
cin >> p[i].x >> p[i].y;
}
Andrew();
double ans = 0;
for(int i = 1; i < top; i ++) ans += distance(stk[i], stk[i + 1]);
cout << fixed << setprecision(2) << ans << "\n";
}
P3829
蛮有意思的一道题。
求出所有信用卡的 \(4\) 个圆心构成的凸包,圆弧部分的贡献总和一定是整个圆的周长,即 \(2\pi r\)。
证明:从下图可以看出,所有圆弧的圆心角之和就是凸包的外角和,而凸多边形的外角和恒等于 \(360\)°。
答案即为凸包周长 + \(2\pi r\)。
2023ICPC济南M
凸包与极角排序结合的题目。







浙公网安备 33010602011771号