算法竞赛模板计算几何部分

计算几何基础模板:向量、线段、直线与多边形

本文整理二维计算几何中常用的基础知识与 C++ 模板,包括向量运算、To-Left 测试、线段相交、直线表示、投影、直线交点、多边形面积以及点与多边形的位置关系。


目录

  • [1. 二维基础](#1. 二维基础)
    • [1.1 点与向量](#1.1 点与向量)
    • [1.2 点积](#1.2 点积)
    • [1.3 叉积](#1.3 叉积)
    • [1.4 To-Left 测试](#1.4 To-Left 测试)
    • [1.5 二维向量旋转](#1.5 二维向量旋转)
  • [2. 线段](#2. 线段)
    • [2.1 点是否在线段上](#2.1 点是否在线段上)
    • [2.2 两线段是否相交](#2.2 两线段是否相交)
  • [3. 直线](#3. 直线)
    • [3.1 Point / Vector 基础模板](#3.1 Point / Vector 基础模板)
    • [3.2 直线的表示](#3.2 直线的表示)
    • [3.3 点在直线上的投影](#3.3 点在直线上的投影)
    • [3.4 两直线交点](#3.4 两直线交点)
  • [4. 多边形](#4. 多边形)
    • [4.1 三角形面积](#4.1 三角形面积)
    • [4.2 多边形面积](#4.2 多边形面积)
    • [4.3 判断点是否在多边形内](#4.3 判断点是否在多边形内)

1. 二维基础

1.1 点与向量

在二维计算几何中,点和向量都可以用一对坐标表示:

\P=(x,y) \\

因此实际写代码时,可以直接使用同一个 Point 结构体表示点和向量。

cpp 复制代码
struct Point {
    double x, y;

    Point(double x = 0, double y = 0) : x(x), y(y) {}
};

如果需要区分语义,也可以直接写:

cpp 复制代码
using Vector = Point;

1.2 点积

设两个二维向量分别为:

\\\vec a=(a_x,a_y),\\qquad \\vec b=(b_x,b_y) \\

它们的点积为:

\\\boxed{ \\vec a\\cdot\\vec b=a_xb_x+a_yb_y } \\

点积与夹角之间满足:

\\\boxed{ \\vec a\\cdot\\vec b = \|\\vec a\|\|\\vec b\|\\cos\\theta } \\

因此点积常用于:

  • 求向量夹角;
  • 求投影;
  • 判断两个向量是否垂直。

\\\vec a\\cdot\\vec b=0 \\

时,两向量垂直。

代码:

cpp 复制代码
double dot(Point a, Point b) {
    return a.x * b.x + a.y * b.y;
}

1.3 叉积

二维向量叉积通常取其标量部分:

\\\boxed{ \\vec a\\times\\vec b = a_xb_y-a_yb_x } \\

代码:

cpp 复制代码
double cross(const Point& a, const Point& b) {
    return a.x * b.y - a.y * b.x;
}

三个点时,经常需要计算:

\\\overrightarrow{P_1P_2} \\times \\overrightarrow{P_1P_3} \\

可以封装为:

cpp 复制代码
double cross(const Point& p1,
             const Point& p2,
             const Point& p3) {
    Point v1(p2.x - p1.x, p2.y - p1.y);
    Point v2(p3.x - p1.x, p3.y - p1.y);

    return cross(v1, v2);
}

几何意义

叉积绝对值等于两个向量构成的平行四边形面积:

\\\boxed{ S=\|\\vec a\\times\\vec b\| } \\

如果

\\\vec a\\times\\vec b=0 \\

说明两个向量平行。


1.4 To-Left 测试

对于有向直线

\A\\rightarrow B \\

和待判断点 \(P\),计算:

\\\overrightarrow{AB} \\times \\overrightarrow{AP} \\

也就是:

cpp 复制代码
cross(A, B, P)

根据叉积符号可以判断点位于有向直线的哪一侧:

  • cross(A, B, P) > 0:\(P\) 在 \(AB\) 左侧;
  • cross(A, B, P) < 0:\(P\) 在 \(AB\) 右侧;
  • cross(A, B, P) = 0:三点共线。

To-Left 测试是后续判断线段相交、点与多边形关系等问题的重要基础。


1.5 二维向量旋转

设向量:

\\\vec a=(a_x,a_y) \\

逆时针旋转 \(\theta\) 后:

\\\boxed{ \\begin{aligned} x'\&=a_x\\cos\\theta-a_y\\sin\\theta\\\\ y'\&=a_x\\sin\\theta+a_y\\cos\\theta \\end{aligned} } \\

C++ 中 sin()cos() 使用的是弧度。

角度转换成弧度:

\\\boxed{ \\text{radian} = \\text{degree}\\times\\frac{\\pi}{180} } \\

cpp 复制代码
const double PI = acos(-1.0);

double theta = 30.0 * PI / 180.0;

向量旋转模板:

cpp 复制代码
Point rotate(Point a, double theta) {
    return {
        a.x * cos(theta) - a.y * sin(theta),
        a.x * sin(theta) + a.y * cos(theta)
    };
}

逆时针旋转 \(90^\circ\):

cpp 复制代码
Point rotate90(Point a) {
    return {-a.y, a.x};
}

2. 线段

2.1 点是否在线段上

判断点 \(Q\) 是否在线段 \(P_1P_2\) 上,需要满足:

  1. 三点共线;
  2. \(Q\) 的横纵坐标都在线段端点对应的范围内。

代码:

cpp 复制代码
#include <bits/stdc++.h>
using namespace std;

const double eps = 1e-8;

struct Point {
    double x, y;

    Point(double x = 0, double y = 0) : x(x), y(y) {}
};

double cross(const Point& a, const Point& b) {
    return a.x * b.y - a.y * b.x;
}

double cross(const Point& p1,
             const Point& p2,
             const Point& p3) {
    return cross(
        Point(p2.x - p1.x, p2.y - p1.y),
        Point(p3.x - p1.x, p3.y - p1.y)
    );
}

// 点 q 是否在线段 p1p2 上
bool onSeg(const Point& p1,
           const Point& p2,
           const Point& q) {
    return fabs(cross(p1, p2, q)) < eps
        && q.x >= min(p1.x, p2.x) - eps
        && q.x <= max(p1.x, p2.x) + eps
        && q.y >= min(p1.y, p2.y) - eps
        && q.y <= max(p1.y, p2.y) + eps;
}

2.2 两线段是否相交

判断线段 \(AB\) 和 \(CD\) 是否相交,主要使用:

  • 快速排斥;
  • 跨立实验;
  • 端点与共线情况判断。

快速排斥

如果两条线段的包围盒都没有交集,那么线段一定不相交。

跨立实验

判断 \(C,D\) 是否分别位于直线 \(AB\) 两侧:

\cross(A,B,C)\\times cross(A,B,D)\<0 \\

同时判断 \(A,B\) 是否分别位于直线 \(CD\) 两侧:

\cross(C,D,A)\\times cross(C,D,B)\<0 \\

两者同时成立时,两线段规范相交。

完整模板

cpp 复制代码
bool segIntersect(Point a, Point b,
                  Point c, Point d) {
    // 快速排斥:包围盒不相交
    if (max(a.x, b.x) < min(c.x, d.x) - eps) return false;
    if (max(a.y, b.y) < min(c.y, d.y) - eps) return false;
    if (max(c.x, d.x) < min(a.x, b.x) - eps) return false;
    if (max(c.y, d.y) < min(a.y, b.y) - eps) return false;

    // 跨立实验
    double c1 = cross(a, b, c);
    double c2 = cross(a, b, d);
    double c3 = cross(c, d, a);
    double c4 = cross(c, d, b);

    // 规范相交
    if (c1 * c2 < -eps && c3 * c4 < -eps)
        return true;

    // 非规范相交:
    // 包括端点接触、共线重叠
    if (onSeg(a, b, c)) return true;
    if (onSeg(a, b, d)) return true;
    if (onSeg(c, d, a)) return true;
    if (onSeg(c, d, b)) return true;

    return false;
}

使用示例:

cpp 复制代码
int main() {
    Point a(0, 0), b(2, 2);
    Point c(0, 2), d(2, 0);

    if (segIntersect(a, b, c, d))
        cout << "相交\n";
    else
        cout << "不相交\n";

    return 0;
}

3. 直线

3.1 Point / Vector 基础模板

这一部分可以作为后续直线、多边形等内容共同使用的基础模板。

cpp 复制代码
#include <bits/stdc++.h>
using namespace std;

using db = double;

const db EPS = 1e-9;
const db PI = acos(-1.0);

int sgn(db x) {
    if (fabs(x) < EPS)
        return 0;

    return x < 0 ? -1 : 1;
}

struct Point {
    db x, y;

    Point() {}

    Point(db _x, db _y)
        : x(_x), y(_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 * (db k) const {
        return {x * k, y * k};
    }

    Point operator / (db k) const {
        return {x / k, y / k};
    }

    bool operator < (const Point& p) const {
        if (sgn(x - p.x) != 0)
            return x < p.x;

        return y < p.y;
    }

    bool operator == (const Point& p) const {
        return sgn(x - p.x) == 0
            && sgn(y - p.y) == 0;
    }
};

using Vector = Point;

// 点积
db dot(Vector a, Vector b) {
    return a.x * b.x + a.y * b.y;
}

// 叉积
db cross(Vector a, Vector b) {
    return a.x * b.y - a.y * b.x;
}

// OA × OB
db cross(Point O, Point A, Point B) {
    return cross(A - O, B - O);
}

// 向量长度平方
db len2(Vector a) {
    return dot(a, a);
}

// 向量长度
db len(Vector a) {
    return sqrt(len2(a));
}

// 两点距离
db dist(Point a, Point b) {
    return len(a - b);
}

// 单位向量
Vector normalize(Vector a) {
    return a / len(a);
}

// 逆时针旋转 90°
Vector rotate90(Vector a) {
    return {-a.y, a.x};
}

// 向量旋转 theta
Vector rotate(Vector a, db theta) {
    return {
        a.x * cos(theta) - a.y * sin(theta),
        a.x * sin(theta) + a.y * cos(theta)
    };
}

3.2 直线的表示

直线推荐使用点向式表示:

  • 直线上一点 \(P\);
  • 一个方向向量 \(\vec v\)。

即:

\\\boxed{ X=P+t\\vec v } \\

代码:

cpp 复制代码
struct Line {
    Point p;  // 直线上一点
    Point v;  // 方向向量
};

3.3 点在直线上的投影

设点 \(P\) 在直线 \(AB\) 上的投影点为 \(H\)。

直线方向向量:

\\\vec v=\\overrightarrow{AB}=B-A \\

从 \(A\) 指向 \(P\):

\\\overrightarrow{AP}=P-A \\

根据点积的投影性质,可以得到参数:

\\\boxed{ t= \\frac{\\overrightarrow{AP}\\cdot\\vec v} {\\vec v\\cdot\\vec v} } \\

因此投影点:

\\\boxed{ H=A+t\\vec v } \\

进一步写成:

\\\boxed{ H= A+ \\frac{(P-A)\\cdot(B-A)} {(B-A)\\cdot(B-A)} (B-A) } \\

模板:

cpp 复制代码
Point projection(Point P, Point A, Point B) {
    Point v = B - A;
    Point AP = P - A;

    double t = dot(AP, v) / len2(v);

    return A + v * t;
}

示例:

cpp 复制代码
int main() {
    Point P, A, B;

    cin >> P.x >> P.y;
    cin >> A.x >> A.y;
    cin >> B.x >> B.y;

    Point H = projection(P, A, B);

    cout << fixed << setprecision(10);
    cout << H.x << ' ' << H.y << '\n';

    return 0;
}

3.4 两直线交点

设两条直线分别为:

\P+t\\vec v \\

\Q+s\\vec w \\

交点模板:

cpp 复制代码
Point lineIntersection(Point P, Vector v,
                       Point Q, Vector w) {
    Vector u = P - Q;

    db t = cross(w, u) / cross(v, w);

    return P + v * t;
}

如果使用四个点 \(A,B,C,D\) 表示两条直线:

cpp 复制代码
Point lineIntersection(Point A, Point B,
                       Point C, Point D) {
    Vector v = B - A;
    Vector w = D - C;

    db t = cross(w, A - C) / cross(v, w);

    return A + v * t;
}

使用之前一般先判断:

cpp 复制代码
cross(B - A, D - C)

是否为 0

若为 0,说明两条直线平行或者重合。


4. 多边形

4.1 三角形面积

利用叉积可以直接计算三角形面积:

\\\boxed{ S_{\\triangle ABC} = \\frac12 \\left\| \\overrightarrow{AB} \\times \\overrightarrow{AC} \\right\| } \\

模板:

cpp 复制代码
db triangleArea(Point A, Point B, Point C) {
    return fabs(cross(B - A, C - A)) / 2.0;
}

4.2 多边形面积

多边形面积可以通过叉积累加计算。

模板:

cpp 复制代码
db polygonArea(vector<Point>& p) {
    int n = p.size();

    db ans = 0;

    for (int i = 0; i < n; i++) {
        ans += cross(p[i], p[(i + 1) % n]);
    }

    return fabs(ans) / 2.0;
}

4.3 判断点是否在多边形内

这里使用回转数思想判断点 \(P\) 与多边形的位置关系。

返回值约定:

text 复制代码
0 -> 多边形外
1 -> 多边形边界上
2 -> 多边形内

多边形顶点顺时针、逆时针给出都可以。

先判断点是否在边界上

cpp 复制代码
bool onSegment(Point p, Point a, Point b) {
    return sgn(cross(b - a, p - a)) == 0
        && sgn(dot(p - a, p - b)) <= 0;
}

第一部分:

cpp 复制代码
cross(b - a, p - a) == 0

判断三点共线。

第二部分:

cpp 复制代码
dot(p - a, p - b) <= 0

判断点 \(P\) 位于线段 \(AB\) 两端之间。

回转数判断

遍历多边形每一条有向边 \(a\rightarrow b\)。

向上穿过水平线时:

cpp 复制代码
a.y <= p.y && b.y > p.y

如果点 \(P\) 位于有向边左侧,则:

cpp 复制代码
wn++;

向下穿过水平线时:

cpp 复制代码
a.y > p.y && b.y <= p.y

如果点 \(P\) 位于有向边右侧,则:

cpp 复制代码
wn--;

最终:

  • wn == 0:点在多边形外;
  • wn != 0:点在多边形内。

其中 <=> 的搭配也可以避免水平射线恰好经过多边形顶点时,对同一个顶点重复统计。

完整模板

cpp 复制代码
// 判断点 p 是否在线段 ab 上
bool onSegment(Point p, Point a, Point b) {
    return sgn(cross(b - a, p - a)) == 0
        && sgn(dot(p - a, p - b)) <= 0;
}

/*
    判断点 p 与多边形的位置关系

    返回值:
    0 -> 多边形外
    1 -> 多边形边界上
    2 -> 多边形内

    多边形顶点可以顺时针,也可以逆时针
*/
int pointInPolygon(Point p, const vector<Point>& poly) {
    int n = poly.size();

    int wn = 0;

    for (int i = 0; i < n; i++) {
        Point a = poly[i];
        Point b = poly[(i + 1) % n];

        // 先判断是否恰好在边界上
        if (onSegment(p, a, b)) {
            return 1;
        }

        // 向上穿过水平射线
        // a 在 p 下方或同高度
        // b 严格在 p 上方
        if (a.y <= p.y && b.y > p.y) {
            // p 在有向边 a -> b 的左侧
            if (sgn(cross(b - a, p - a)) > 0) {
                wn++;
            }
        }

        // 向下穿过水平射线
        // a 严格在 p 上方
        // b 在 p 下方或同高度
        else if (a.y > p.y && b.y <= p.y) {
            // p 在有向边 a -> b 的右侧
            if (sgn(cross(b - a, p - a)) < 0) {
                wn--;
            }
        }
    }

    if (wn == 0)
        return 0;

    return 2;
}

时间复杂度:

\\\boxed{O(n)} \\

其中 \(n\) 为多边形顶点数量。


5. 常用公式与函数速查

内容 公式 / 函数
点积 dot(a, b)
叉积 cross(a, b)
三点叉积 cross(O, A, B)
向量长度平方 len2(a)
向量长度 len(a)
两点距离 dist(a, b)
单位向量 normalize(a)
逆时针旋转 90° rotate90(a)
任意角旋转 rotate(a, theta)
点在线段上 onSegment(p, a, b)
两线段相交 segIntersect(a, b, c, d)
点在直线上的投影 projection(P, A, B)
两直线交点 lineIntersection(...)
三角形面积 triangleArea(A, B, C)
多边形面积 polygonArea(poly)
点与多边形位置关系 pointInPolygon(p, poly)

6. 完整基础模板

如果比赛时希望直接复制一套基础模板,可以把常用的点、向量操作统一到一起:

cpp 复制代码
#include <bits/stdc++.h>
using namespace std;

using db = double;

const db EPS = 1e-9;
const db PI = acos(-1.0);

int sgn(db x) {
    if (fabs(x) < EPS)
        return 0;

    return x < 0 ? -1 : 1;
}

struct Point {
    db x, y;

    Point() {}

    Point(db _x, db _y)
        : x(_x), y(_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 * (db k) const {
        return {x * k, y * k};
    }

    Point operator / (db k) const {
        return {x / k, y / k};
    }

    bool operator < (const Point& p) const {
        if (sgn(x - p.x) != 0)
            return x < p.x;

        return y < p.y;
    }

    bool operator == (const Point& p) const {
        return sgn(x - p.x) == 0
            && sgn(y - p.y) == 0;
    }
};

using Vector = Point;

db dot(Vector a, Vector b) {
    return a.x * b.x + a.y * b.y;
}

db cross(Vector a, Vector b) {
    return a.x * b.y - a.y * b.x;
}

db cross(Point O, Point A, Point B) {
    return cross(A - O, B - O);
}

db len2(Vector a) {
    return dot(a, a);
}

db len(Vector a) {
    return sqrt(len2(a));
}

db dist(Point a, Point b) {
    return len(a - b);
}

Vector normalize(Vector a) {
    return a / len(a);
}

Vector rotate90(Vector a) {
    return {-a.y, a.x};
}

Vector rotate(Vector a, db theta) {
    return {
        a.x * cos(theta) - a.y * sin(theta),
        a.x * sin(theta) + a.y * cos(theta)
    };
}

bool onSegment(Point p, Point a, Point b) {
    return sgn(cross(b - a, p - a)) == 0
        && sgn(dot(p - a, p - b)) <= 0;
}

Point projection(Point P, Point A, Point B) {
    Vector v = B - A;

    db t = dot(P - A, v) / dot(v, v);

    return A + v * t;
}

Point lineIntersection(Point P, Vector v,
                       Point Q, Vector w) {
    Vector u = P - Q;

    db t = cross(w, u) / cross(v, w);

    return P + v * t;
}

db triangleArea(Point A, Point B, Point C) {
    return fabs(cross(B - A, C - A)) / 2.0;
}

db polygonArea(vector<Point>& p) {
    int n = p.size();

    db ans = 0;

    for (int i = 0; i < n; i++) {
        ans += cross(p[i], p[(i + 1) % n]);
    }

    return fabs(ans) / 2.0;
}

int pointInPolygon(Point p, const vector<Point>& poly) {
    int n = poly.size();

    int wn = 0;

    for (int i = 0; i < n; i++) {
        Point a = poly[i];
        Point b = poly[(i + 1) % n];

        if (onSegment(p, a, b))
            return 1;

        if (a.y <= p.y && b.y > p.y) {
            if (sgn(cross(b - a, p - a)) > 0)
                wn++;
        }
        else if (a.y > p.y && b.y <= p.y) {
            if (sgn(cross(b - a, p - a)) < 0)
                wn--;
        }
    }

    return wn == 0 ? 0 : 2;
}

总结

二维计算几何中最基础、最重要的两个运算是:

点积

主要用于:

  • 夹角;
  • 垂直;
  • 投影。

叉积

主要用于:

  • 判断方向;
  • To-Left;
  • 判断共线;
  • 面积;
  • 线段相交;
  • 多边形相关问题。

把点积、叉积和 Point / Vector 基础结构熟练掌握后,后面的直线、线段和多边形问题都会容易很多。