加载中...

计算几何专题

蒟蒻的计算几何几乎不会,这里记录一些计算几何的常见概念,以作备忘:

oi-wiki

三角函数与弧度制

弧度制:

\[\frac{180}{角度} = \frac{\pi}{弧度} \]

  • 使用 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;
}

应用:

  1. 判定点线之间的位置关系
  • \((\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\) 三点共线
  1. 判定线线之间的位置关系
  • 判断线段 \(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\) 见代码部分即可。证明见下图:

pV0nM8g.png

交点 \(O\) 可以利用点 \(a\)\(\overrightarrow{au}\) 方向上的偏移得出\(|au|\) 已知,故只要知道 \(\frac{|oa|}{|ua|}\) 即可。发现它们的关系恰为两个平行四边形的高之比,而底相同,故又等于两个平行四边形的面积之比,而平行四边形的面积可以利用叉积快速得出。

  1. 求任意多边形的面积:
    pV0nDM9.png

注:上述做法只是在求多边形面积的大小。由叉积的正负性可知,其实多边形的面积也有正负性之分。而 多边形面积的正负性 可以 区分多边形中每条边两侧分别是多边形的外部/内部(一个性质:若将多边形中的所有边按照顺时针或逆时针标注方向,则它们的 左/右侧 与 多边形外部/内部 是一一对应的)。

例题:2025杭电多校9 1005 code

极角排序

两种方式:

  1. 利用 cmath 库中的 \(atan(x)\)\(atan2(x, y)\) 函数,配合一些分类讨论算出每个点的极角,然后直接按照算出值的大小升序排序。
  • 优点:上手难度低,容易理解
  • 缺点:会卡精度
  1. 利用叉积和一些简单的分讨来构造一个 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\) 方向上下半区的处理:

pZ21Dlq.png

最大的优点就是无精度损失。对于所有输入坐标为整数的情况,能用这种就用这种。

例题:ABC442 E

凸包

定义:在平面上能包含所有给定点的最小凸多边形,称作给定点集的凸包。

Andrew 算法

应用:给定 \(n\) 个点的坐标 \((x, y)\)\(O(n\log n)\) 求出所有点构成的凸包(算法时间复杂度瓶颈在于排序,后面利用栈求凸包的过程是 \(O(n)\) 的)。

算法流程:
pZz28eA.png

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

pZz2vOH.png

蛮有意思的一道题。

求出所有信用卡的 \(4\) 个圆心构成的凸包,圆弧部分的贡献总和一定是整个圆的周长,即 \(2\pi r\)

证明:从下图可以看出,所有圆弧的圆心角之和就是凸包的外角和,而凸多边形的外角和恒等于 \(360\)°。

答案即为凸包周长 + \(2\pi r\)

贴一个看上去比较清楚的图:
pZzRkp8.png

2023ICPC济南M

凸包与极角排序结合的题目。

code

posted @ 2025-08-15 23:42  小橘奏  阅读(80)  评论(0)    收藏  举报