首发于 花朝月夕
计算几何中与多边形相关的基本问题

计算几何中与多边形相关的基本问题

摘要: 本文介绍计算几何中与多边形相关的基本问题
【对算法,数学,计算机感兴趣的同学,欢迎关注我哈,阅读更多原创文章】
我的网站: 潮汐朝夕的生活实验室
我的公众号: 算法题刷刷
我的知乎: 潮汐朝夕
我的github: FennelDumplings
我的leetcode: FennelDumplings

各位好,在文章 向量的实现 中,我们实现了一个二维向量的代码模板,主要是 Vector2 这个类。

此后基于二维向量的代码模板,我们解决了计算几何中关于 直线和线段 极角 中的常见问题,以及力扣中的几个相关题目。

本文我们介绍计算几何中关于多边形的常见问题,代码依然使用 Vector2。然后解决三个力扣中的题目:469、593、836。主要涉及以下内容:

  • 凸多边形判定
  • 凸多边形点集排序
  • 简单多边形的面积
  • 简单多边形内部外部判断
  • 两个简单多边形相交

1. 凸多边形判定

  • 简单多边形:边不相交的多边形
  • 凸多边形:每个内角都在 180 度以内

题目: 469. 凸多边形

给定 X-Y 平面上的一组点 points ,其中 points[i] = [xi, yi] 。这些点按顺序连成一个多边形。

如果该多边形为 凸 多边形(凸多边形的定义)则返回 true ,否则返回 false 。

你可以假设由给定点构成的多边形总是一个 简单的多边形(简单多边形的定义)。换句话说,我们要保证每个顶点处恰好是两条边的汇合点,并且这些边 互不相交 。

提示:

3 <= points.length <= 1e4
points[i].length == 2
-1e4 <= xi, yi <= 1e4
所有点都不同
示例 1: 输入: points = [[0,0],[0,5],[5,5],[5,0]] 输出: true
示例 2: 输入: points = [[0,0],[0,10],[10,10],[10,0],[5,5]] 输出: false

算法

枚举 i: 0,1,...,n,考察 p[i], p[(i+1)%n], p[(i+2)%n],向量 Vector2 a(p[i], p[(i+1)%n]) , Vector2 b(p[(i+1)%n], p[(i+2)%n])

b 相对于 a 的方向必须始终不变,两种情况:

  • b 始终在 a 的顺时针 180 度以内, 即 a, b 的叉积小于 0
  • b 始终在 a 的逆时针 180 度以内, 即 a, b 的叉积大于 0

代码 (C++)

class Solution {
public:
    bool isConvex(vector<vector<int>>& points) {
        int n = points.size();
        vector<int> p1 = points[0], p2 = points[1], p3 = points[2];
        Vector2 q1(p1[0], p1[1]);
        Vector2 q2(p2[0], p2[1]);
        Vector2 q3(p3[0], p3[1]);
        bool flag = ccw(q1, q2, q3) > EPS;
        for(int i = 0; i < n; ++i)
            vector<int> p1 = points[i], p2 = points[(i + 1) % n], p3 = points[(i + 2) % n];
            Vector2 q1(p1[0], p1[1]);
            Vector2 q2(p2[0], p2[1]);
            Vector2 q3(p3[0], p3[1]);
            if(flag && ccw(q1, q2, q3) < -EPS)
                return false;
            if(!flag && ccw(q1, q2, q3) > EPS)
                return false;
        return true;

2. 凸多边形点集排序

选定点集的重心作为极点,然后对点集做极角排序。

计算点集的重心 O
计算点 Pi 之间的大小关系

大小关系定义:OPj 如果在 OPi 的逆时针方向,则 Pj 小于 Pi

代码模板

struct Cmp
    Vector2 O;
    Cmp(Vector2 p0)
        O = p0;
    bool operator()(const Vector2& p1, const Vector2& p2) const
        return ccw(O, p1, p2) > 0;

题目: 593. 有效的正方形

给定2D空间中四个点的坐标 p1, p2, p3 和 p4,如果这四个点构成一个正方形,则返回 true 。

点的坐标 pi 表示为 [xi, yi] 。 输入没有任何顺序 。

一个 有效的正方形 有四条等边和四个等角(90度角)。

提示:

p1.length == p2.length == p3.length == p4.length == 2
-1e4 <= xi, yi <= 1e4
示例 1: 输入: p1 = [0,0], p2 = [1,1], p3 = [1,0], p4 = [0,1] 输出: True
示例 2: 输入:p1 = [0,0], p2 = [1,1], p3 = [1,0], p4 = [0,12] 输出:false
示例 3: 输入:p1 = [1,0], p2 = [-1,0], p3 = [0,1], p4 = [0,-1] 输出:true

算法

计算四个点重心, 如果某个点与重心重合,直接返回false

将点集排序。

然后枚举点 i,边1 (p[i], p[(i+1)%4]) ; 边2 (p[i], p[(i-1+4)%4])

  1. 模相同
  2. 夹角为 90 度(内积为零)

代码 (C++)

class Solution {
public:
    bool validSquare(vector<int>& p1, vector<int>& p2, vector<int>& p3, vector<int>& p4) {
        vector<Vector2> points;
        points.emplace_back(p1[0], p1[1]);
        points.emplace_back(p2[0], p2[1]);
        points.emplace_back(p3[0], p3[1]);
        points.emplace_back(p4[0], p4[1]);
        Vector2 O(0, 0);
        for(Vector2 p: points)
            O.x += p.x;
            O.y += p.y;
        O.x /= 4;
        O.y /= 4;
        for(Vector2 p: points)
            if(fabs(p.x - O.x) < EPS && fabs(p.y - O.y) < EPS)
                return false;
        Cmp cmp(O);
        sort(points.begin(), points.end(), cmp);
        double c = (points[1] - points[0]).norm();
        for(int i = 0; i < 4; ++i)
            cout << points[i].x << " " << points[i].y << endl;
            Vector2 a = points[(i + 1) % 4] - points[i];
            Vector2 b = points[(i - 1 + 4) % 4] - points[i];
            cout << a.x << " " << a.y << endl;
            cout << b.x << " " << b.y << endl;
            if(fabs(a.norm() - c) > EPS)
                return false;
            if(!a.perpendicular(b))
                return false;
        return true;

3. 简单多边形的面积

三角形

给出三角形三个顶点时,利用向量叉积就能求出面积。

凸多边形

选择多边形内部的一个点,用以该点为顶点的三角形分割多边形,求出各个三角形面积。

凹多边形

向量的叉积会根据不同方向而发生符号变化。利用此性质可以求凹多边形面积。

\vec{a} \times \vec{b} 若要大于零,需要 \vec{b} \vec{a} 的逆时针方向。

总结

假设简单多边形的 n 个顶点 \vec{p}_{0}, \vec{p}_{1}, ..., \vec{p}_{n-1} 已经按逆时针方向排列。

面积为

\frac{1}{2}(\vec{p}_{0} \times \vec{p}_{1} + \vec{p}_{1} \times \vec{p}_{2} + ... + \vec{p}_{n-2} \times \vec{p}_{n-1} + \vec{p}_{n-1} \times \vec{p}_{0}) \\

代码模板

// 简单多边形 p 的面积
double area(const vector<Vector2>& p)
    double ans = 0.0;
    int n = p.size();
    for(int i = 0; i < n; ++i)
        ans += p[i].cross(p[(i + 1) % n]);
    return fabs(ans) / 2.0;

题目: P1183 多边形的面积

本题是洛谷的题目,可以参考。有了 Vector2 和 ares 的代码模板,本题可以轻易解决,代码如下:

int main()
    int n;
    cin >> n;
    vector<Vector2> p;
    for(int i = 0; i < n; ++i)
        int x, y;
        cin >> x >> y;
        p.emplace_back(x, y);
    int ans = area(p);
    cout << ans << endl;

4. 内部外部判断

给定多边形和不在多边形边上的点 q, 判断 q 在多边形内部还是外部。有面积和法和射线法。

面积和法(只适用与凸多边形)

判断 q 与多边形每条边组成的三角形面积的和是否等于多边形面积。

射线法

从 q 画一条射线,并计算与多边形的所有边的交点数目。

假象从 q 出发向右的射线,枚举每条边,判断该边能否将射线纵向分割,然后判断交点是否在 q 的右侧。

q 在多边形的边上时,射线法的结果不确定,需要单独处理。

代码模板

// 点 q 是否包含在多边形 p 内部
// q 在多边形边上没有处理
bool isInside(const vector<Vector2>& p, Vector2 q)
    int crosses = 0;
    int n = p.size();
    for(int i = 0; i < n; ++i)
        int j = (i + 1) % n;
        // p[i], p[j] 能否纵向分割射线
        if((p[i].y > q.y) != (p[j].y > q.y))
            // 交点的横坐标
            double X = (p[j].x - p[i].x) * (q.y - p[i].y) / (p[j].y - p[i].y) + p[i].x;
            if(q.x < X)
                ++crosses;
    return crosses % 2 == 1;

题目: 凸多边形内点统计

本题是牛客的题目,可以参考。使用 Vector2 和射线法 isInside 的代码模板,本题比较好解决。

调用 isInside 前先判断点是否在多边形的边(线段)上,代码如下:

int main()
    int n;
    cin >> n;
    vector<Vector2> p;
    for(int i = 0; i < n; ++i)
        int x, y;
        cin >> x >> y;
        p.emplace_back(x, y);
    int m;
    cin >> m;
    int ans = 0;
    for(int i = 0; i < m; ++i)
        int x, y;
        cin >> x >> y;
        Vector2 q(x, y);
        bool flag = false;
        for(int i = 0; i < n; ++i)
            int j = (i + 1) % n;
            if(fabs(ccw(p[i], p[j], q)) < EPS)
                if((p[j] - p[i]).norm() > (p[j] - q).norm())
                    flag = true;
                    continue;
        if(flag)
            continue;
        if(isInside(p, q))
            ++ans;
    cout << ans << endl;

$5 多边形相交

判定两个多边形是否互相接触(相连或重叠)。只有一个点重叠也是重叠。

  1. 枚举一个多边形的所有点,判定该点是否在另一个多边形内部,若是,则返回 true,若不是,则继续找相交或重叠线段。
  2. 重叠 相交 的边(线段)。

代码模板

// 仅判断两条线段是否相交,不需要交点
bool segmentIntersection(Vector2 a, Vector2 b, Vector2 c, Vector2 d)
    double ab = ccw(a, b, c) * ccw(a, b, d);
    double cd = ccw(c, d, a) * ccw(c, d, b);
    // 两条线段在同一直线上或者端点相互重叠
    if(ab == 0 && cd == 0)
        if(b < a) swap(a, b);
        if(d < c) swap(c, d);
        return !(b < c || d < a);
    return ab <= 0 && cd <= 0;
// 判定对多边形 p 和 q 是否相交(相连或重叠)
// 只有一个点的重叠也是重叠
bool polygonIntersects(const vector<Vector2>& p, const vector<Vector2>& q)
    int n = p.size(), m = q.size();
    // 一个多边形的点在另一个多边形的内部
    if(isInside(p, q[0]) || isInside(q, p[0]))
        return true;
    for(int i = 0; i < n; ++i)
        for(int j = 0; j < m; ++j)
            if(segmentIntersection(p[i], p[(i + 1) % n], q[i], q[(i + 1) % n]))
                return true;
    return false;

题目 836. 矩形重叠

矩形以列表 [x1, y1, x2, y2] 的形式表示,其中 (x1, y1) 为左下角的坐标,(x2, y2) 是右上角的坐标。矩形的上下边平行于 x 轴,左右边平行于 y 轴。

如果相交的面积为 正 ,则称两矩形重叠。需要明确的是,只在角或边接触的两个矩形不构成重叠。

给出两个矩形 rec1 和 rec2 。如果它们重叠,返回 true;否则,返回 false 。

提示:

rect1.length == 4
rect2.length == 4
-1e9 <= rec1[i], rec2[i] <= 1e9
rec1 和 rec2 表示一个面积不为零的有效矩形
示例 1: 输入:rec1 = [0,0,2,2], rec2 = [1,1,3,3] 输出:true
示例 2: 输入:rec1 = [0,0,1,1], rec2 = [1,0,2,1] 输出:false
示例 3: 输入:rec1 = [0,0,1,1], rec2 = [2,2,3,3] 输出:false

代码 (C++)

需要特判矩形退化成线段的情况。

// 仅判断两条线段是否相交,不需要交点
bool segmentIntersection(Vector2 a, Vector2 b, Vector2 c, Vector2 d)
    double ab = ccw(a, b, c) * ccw(a, b, d);
    double cd = ccw(c, d, a) * ccw(c, d, b);
    // 两条线段在同一直线上或者端点相互重叠,不算相交
    if(fabs(ab) < EPS || fabs(cd) < EPS)
        return false;
    return ab < 0 && cd < 0;
// 点 q 是否包含在多边形 p 内部
// q 在多边形边上没有处理
bool isInside(const vector<Vector2>& p, Vector2 q)
    int crosses = 0;
    int n = p.size();
    for(int i = 0; i < n; ++i)
        int j = (i + 1) % n;
        // p[i], p[j] 能否纵向分割射线
        if((p[i].y > q.y) != (p[j].y > q.y))
            // 交点的横坐标
            double X = (p[j].x - p[i].x) * (q.y - p[i].y) / (p[j].y - p[i].y) + p[i].x;
            if(q.x < X)
                ++crosses;
    return crosses % 2 == 1;
// 判定对多边形 p 和 q 是否相交(相连或重叠)
// 只有一个点的重叠也是重叠
bool polygonIntersects(const vector<Vector2>& p, const vector<Vector2>& q)
    int n = p.size(), m = q.size();
    // 一个多边形的点在另一个多边形的内部
    if(isInside(p, q[0]) || isInside(q, p[0]))
        return true;
    for(int i = 0; i < n; ++i)
        for(int j = 0; j < m; ++j)
            if(segmentIntersection(p[i], p[(i + 1) % n], q[j], q[(j + 1) % m]))
                return true;
    return false;
class Solution {
public:
    bool isRectangleOverlap(vector<int>& rec1, vector<int>& rec2) {
        vector<Vector2> p, q;
        p.emplace_back(rec1[0], rec1[1]);
        p.emplace_back(rec1[0], rec1[3]);
        p.emplace_back(rec1[2], rec1[3]);
        p.emplace_back(rec1[2], rec1[1]);
        q.emplace_back(rec2[0], rec2[1]);
        q.emplace_back(rec2[0], rec2[3]);
        q.emplace_back(rec2[2], rec2[3]);
        q.emplace_back(rec2[2], rec2[1]);
        int n = p.size(), m = q.size();
        // 矩形退化成直线的情况
        for(int i = 0; i < n; ++i)
            if(fabs((p[(i + 1) % 4] - p[i]).norm()) < EPS)
                return false;
        for(int j = 0; j < m; ++j)
            if(fabs((q[(j + 1) % 4] - q[j]).norm()) < EPS)