夜深人静写算法(四)- 计算几何入门

一、前言

  最近听过感触最深的一句话:思想能够到达的地方比光速还要快!月球到了!太阳到了!你能知道的就是你能到达的,所以我们才要扩充我们的知识面,你要知道你才能到啊!
  太阳之外是什么?不知道了?到不了了,原因是什么?因为无知!因为我们把时间和精力花在了喝酒、吃饭、K歌。
  如果你有足够大的勇气去说再见,放弃一切娱乐,从现在开始每天学习,生活一定会奖励你一个新的开始。人生最难的是改变自己。凤凰涅槃浴火重生的人都很了不起。在这里插入图片描述

二、计算几何基本概念

  • 计算几何是计算机科学的一个分支,以往的解析几何,是用代数的方法,建立坐标系去解决问题,但是很多时候需要付出一些代价,比如精度误差,而计算几何更多的是从几何角度,用向量的方法来尽量减少精度误差,例如:将除法转化为乘法、避免三角函数等近似运算 等等。

1、浮点数精度

1)double 代替 float

  • c++ 中 double 的精度高于 float,对精度要求较高的问题,务必采用 double;

2)浮点数判定

  • 由于浮点数(小数)中是有无理数的,即无限不循环小数,也就是小数点后的位数是无限的,在计算机存储的时候不可能全部存下来,一定是近似的存储的,所以浮点数一定是存在精度误差的(实际上,就算是有理数,也是存在误差的,这和计算机存储机制有关,这里不再展开,有兴趣可以参见我博客的文章:C++ 浮点数精度判定);
  • 两个浮点数是否相等,可以采用两数相减的绝对值小于某个精度来实现:
const double eps = 1e-8;
bool EQ(double a, double b) {
    
    
    return fabs(a - b) < eps;
}
  • 并且可以用一个三值函数来确定某个数是零、大于零还是小于零:
int threeValue(double d) {
    
    
    if (fabs(d) < eps)
        return 0;
    return d > 0 ? 1 : -1;
}

3)负零判定

  • 因为精度误差的存在,所以在输出的时候一定要注意,避免输出 -0.00:
    double v = -0.0000000001;
    printf("%.2lf\n", v);
  • 避免方法是先通过三值函数确定实际值是否为0,如果是0,则需要取完绝对值后再输出:
    double v = -0.0000000001;
    if(threeValue(v) == 0) {
    
    
        v = fabs(v);
    }
    printf("%.2lf\n", v);

4)避免三角函数、对数、开方、除法等

  • c++ 三角函数运算方法采用的是 CORDIC算法,一种利用迭代的方式进行求解的算法,其中还用到了开方运算,所以实际的算力消耗还是很大的,在实际求解问题的过程中,能够避免不用就尽量不用。
  • 除法运算会带来精度误差,所以能够转换成乘法的也尽量转换为乘法运算。

2、点和向量

1)定义

  • 本章为计算几何入门级,所以只介绍二维的情况,对于二维的点,只需要两个浮点数表示即可,点的定义如下:
typedef double Type;
class Point2D {
    
    
private:
    Type x, y;
public:
    Point2D(Type _x, Type _y) : x(_x), y(_y) {
    
    }
    ...
};
  • 两点相减就变成了向量,向量支持平移(可以将起点平移到坐标系原点(0,0)),所以向量的程序运算和点几乎无差别,可以直接用 typedef 定义:
typedef Point2D Vector2D;
  • 数学表示如下:
    a ⃗ \vec a a

2)四则运算

  • 点(向量)的四则运算 加减乘除 如下:

  • 实现的是两个向量相加,代码如下:
Point2D Point2D::operator+(const Point2D& other) const {
    
    
    return Point2D(x + other.x, y + other.y);
}
  • 如图二-2-1所示,红色向量为两向量相加后的向量;
    图二-2-1

  • 实现的是两个向量相减,代码如下:
Point2D Point2D::operator-(const Point2D& other) const {
    
    
    return Point2D(x - other.x, y - other.y);
}
  • 如图二-2-2所示,红色向量为两向量相减后的向量;
    图二-2-2

  • 向量和向量的乘法分为点乘和叉乘,这个后面再展开;
  • 这里的乘法为 向量 和 数字 的乘法,代码实现如下:
Point2D Point2D::operator *(const double &k) const {
    
    
    return Point2D(x * k, y * k);
}
  • 如图二-2-3所示,向量乘法就是将向量扩大( k > 1 k > 1 k>1)或缩短( k < 1 k < 1 k<1) k k k 倍;
    图二-2-3

  • 向量和向量的之间不存在除法;
  • 向量 和 数字 的除法,可以转换成向量乘法(乘上 k k k 的倒数)。

2、向量的模

  • 两点之间的欧几里得距离,为两点组成向量的模,代码实现如下:
double Vector2D::len() {
    
    
    return sqrt(x*x + y*y);
}
  • 调用的时候,可以直接调用两点之间的减法,再调用 l e n len len 成员函数,实现如下:
Point2D a(0, 2), b(3, 4);
double dist = (b - a).len();
  • 优化:在单纯进行向量长度比较的时候,往往只需要知道哪个更长(或者更短),

3、标准化

  • 有时候为了方便计算,会把向量转换成 单位向量(模为 1 的向量),此所谓标准化。实现如下:
Point2D Vector2D::normalize() {
    
    
    double l = len();
    if (threeValue(l)) {
    
    
        x /= l, y /= l;
    }
    return *this;
}
  • 由于存在零向量,所以标准化过程中,在进行除法的时候需要先用三值函数判断向量的模是否为0,避免引起除零错误;

4、点乘

  • 点乘(又叫点积、数量积、内积),表示的是两个向量每一维相乘的加和,结果是个数字,二维的情况如下:
    a ⃗ = ( x a , y a ) \vec a = (x_a,y_a) a =(xa,ya) b ⃗ = ( x b , y b ) \vec b = (x_b,y_b) b =(xb,yb) a ⃗ ⋅ b ⃗ = x a x b + y a y b \vec a \cdot \vec b = x_ax_b+y_ay_b a b =xaxb+yayb
  • 代码实现如下:
Type Point2D::operator*(const Point2D& other) const {
    
    
    return x*other.x + y*other.y;
}
  • (当然,点积同样适用于三维、甚至多维的情况)
  • 点乘可以用来求两个向量的夹角,公式如下:
    a ⃗ ⋅ b ⃗ = ∣ a ⃗ ∣ ∣ b ⃗ ∣ c o s θ \vec a \cdot \vec b = |\vec a||\vec b|cos\theta a b =a b cosθ
  • 从以上公式可以看出,点乘的结果和夹角的关系:
  • 1)点乘为0,夹角为90度;
  • 2)点乘为1,夹角为0度;
  • 3)点乘为-1,夹角为180度;
  • 特殊的,任何向量和零向量的点乘都为 0;
  • 下文会介绍 点乘 在线段判交中的应用;

5、叉乘

  • 叉乘(又叫叉积、向量积,外积),表示的是两个向量经过运算后,和这两个向量垂直的向量(和点乘不同,它的结果还是一个向量),如下:
    a ⃗ = ( x a , y a , z a ) \vec a = (x_a,y_a,z_a) a =(xa,ya,za) b ⃗ = ( x b , y b , z b ) \vec b = (x_b,y_b,z_b) b =(xb,yb,zb) a ⃗ × b ⃗ = c ⃗ = ( y a z b − z a y b ) i ⃗ + ( z a x b − x a z b ) j ⃗ + ( x a y b − y a x b ) k ⃗ \vec a \times \vec b = \vec c = (y_az_b-z_ay_b) \vec i + (z_ax_b-x_az_b)\vec j + (x_ay_b-y_ax_b)\vec k a ×b =c =(yazbzayb)i +(zaxbxazb)j +(xaybyaxb)k
  • 其中 i ⃗ = ( 1 , 0 , 0 ) , j ⃗ = ( 0 , 1 , 0 ) , k ⃗ = ( 0 , 0 , 1 ) \vec i = (1, 0, 0), \vec j = (0, 1, 0), \vec k = (0, 0, 1) i =(1,0,0),j =(0,1,0),k =(0,0,1)
  • 当两个向量都在 z = 0 z = 0 z=0 的平面上时, z a = z b = 0 z_a=z_b=0 za=zb=0,退化为二维的情况,则有 a ⃗ × b ⃗ = ( x a y b − y a x b ) k ⃗ \vec a \times \vec b = (x_ay_b-y_ax_b)\vec k a ×b =(xaybyaxb)k
  • 代码实现如下(因为 k ⃗ \vec k k 方向已经确定,为垂直原向量,所以返回值可以定义为一个数值类型):
Type Point2D::X(const Point2D& other) const {
    
    
    return x*other.y - y*other.x;
}
  • 在二维计算几何中,叉乘可以用来判断两个点是否在一个向量的同一侧,如图二-5-1所示:
图二-5-1
  • 对于向量 s ⃗ = O S \vec s = OS s =OS,假设有任意一个点 T ( x , y ) T(x,y) T(x,y),对原点到这个点做一个向量 t ⃗ \vec t t 。那么有叉乘:
    s ⃗ × t ⃗ = ( 10 y − x 10 ) k ⃗ \vec s \times \vec t = (10y-x10) \vec k s ×t =(10yx10)k

  • 1) s ⃗ × t ⃗ > 0 \vec s \times \vec t > 0 s ×t >0,则 ( x , y ) (x,y) (x,y) s ⃗ \vec s s 左侧(参考点A);

  • 2) s ⃗ × t ⃗ = 0 \vec s \times \vec t = 0 s ×t =0,则 ( x , y ) (x,y) (x,y) s ⃗ \vec s s 共线(参考点B);

  • 3) s ⃗ × t ⃗ < 0 \vec s \times \vec t < 0 s ×t <0,则 ( x , y ) (x,y) (x,y) s ⃗ \vec s s 右侧(参考点C);

    扫描二维码关注公众号,回复: 12393954 查看本文章
  • 所以,只要两个点分别和对应向量做叉乘,再将结果相乘,如果值大于0,则代表同侧;小于零代表异侧;

  • 叉乘还代表了两个向量组成的平行四边形的面积,有公式:
    a ⃗ × b ⃗ = ∣ a ⃗ ∣ ∣ b ⃗ ∣ s i n θ \vec a \times \vec b = |\vec a||\vec b|sin\theta a ×b =a b sinθ

  • 同样也代表了两个向量组成的三角形的面积的两倍。

6、旋转

  • 通过 叉乘 和 点乘 联立方程组,可以求出一个点绕着原点旋转 θ \theta θ 角度后的点的位置;
  • 令旋转前的点的位置为 S ( x s , y s ) S (x_s,y_s) S(xs,ys),旋转 θ \theta θ 角度后的点的位置 T ( x t , y t ) T(x_t,y_t) T(xt,yt),则有:
    { s ⃗ ⋅ t ⃗ = x s x t + y s y t = ( x s 2 + y s 2 ) c o s θ s ⃗ × t ⃗ = x s y t − y s x t = ( x s 2 + y s 2 ) s i n θ \begin{cases} \vec s \cdot \vec t = x_sx_t + y_sy_t = (x_s^2+y_s^2)cos\theta \\ \vec s \times \vec t = x_sy_t - y_sx_t = (x_s^2+y_s^2)sin\theta \end{cases} { s t =xsxt+ysyt=(xs2+ys2)cosθs ×t =xsytysxt=(xs2+ys2)sinθ
  • 两个方程,两个未知数,求解得到:
    { x t = x s c o s θ − y s s i n θ y t = x s s i n θ + y s c o s θ \begin{cases} x_t= x_scos\theta - y_ssin\theta\\ y_t = x_ssin\theta + y_scos\theta\\ \end{cases} { xt=xscosθyssinθyt=xssinθ+yscosθ
  • 表示成矩阵的形式为:
    [ x t y t ] = [ x s y s ] [ c o s θ s i n θ − s i n θ c o s θ ] \left[ \begin{matrix} x_t & y_t \end{matrix} \right] \quad = \left[ \begin{matrix} x_s & y_s \end{matrix} \right] \left[ \begin{matrix} cos\theta & sin\theta \\ -sin\theta & cos\theta \end{matrix} \right] [xtyt]=[xsys][cosθsinθsinθcosθ]
  • 代码实现如下:
Point2D Point2D::turn(double theta) {
    
    
    double costheta = cos(theta);
    double sintheta = sin(theta);
    return Point2D(
        x*costheta - y*sintheta, 
        x*sintheta + y*costheta
    );
}

三、计算几何基础算法

1、线段判交

  • 判断两个线段是否相交,在计算几何算法中有着广泛的应用,两个线段的相交情况有如下三种:
图三-1-1
* 这里用枚举来代表三种线段的相交关系:
enum SegCrossType {
    
    
    SCT_NONE = 0,          // 不相交
    SCT_CROSS = 1,         // 相交于内部
    SCT_ENDPOINT_ON = 2,   // 其中一条线段的端点在另一条上
};
  • 并且定义线段的数据结构如下:
class Segment2D {
    
    
    Point2D s, t;
public:
    ...
};

1)相交于内部

  • 首先对于两个线段,选取其中一个线段作为向量,另外一个线段的两端点作为测验点,如果这两个测验点在向量两边,那么可以肯定,这两个线段一定是不相交的(检测是否在向量两边可以用上文提到的叉乘),如图三-1-2所示:
    在这里插入图片描述
    图三-1-2
  • 然后,选取另外一个线段向量作为向量重复同样的测验;
  • 如果两次测验都满足在向量两边,则这两个线段必然有交点;
  • 这种测验被称为 跨立测验, c++ 代码实现如下:
Type Segment2D::cross(const Point2D& p) const {
    
    
    return (p - s).X(t - s);
}
bool Segment2D::lineCross(const Segment2D& other) const {
    
    
    return threeValue(cross(other.s)) * threeValue(cross(other.t)) == -1;
}
  • cross实现的是点 p p p 在向量 t − s t - s ts 的哪边;
  • lineCross判断两个点是否在向量的两边(通过取三值函数后,相乘为 -1 来实现);
图三-1-3
* 最后,需要两次跨立测验都成立,才算满足线段相交的条件,如图三-1-3所示;

2)相交于端点

  • 当一个线段的端点 P 在另一个线段 (S, T) 上时,只有三种情况:
  • 1)P = S;
  • 2)P = T;
  • 3)∠SPT = 180°;
  • 利用叉乘 和 点乘 进行组合判断即可,代码实现如下:
bool Segment2D::pointOn(const Point2D& p) const {
    
    
    // 满足两个条件:
    //  1.叉乘为0,    (p-s)×(t-s) == 0
    //  2.点乘为-1或0,(p-s)*(p-t) <= 0
    return cross(p) == 0 && (p - s)*(p - t) <= 0;
}
  • 然后对四个点分别进行端点相交判断;

3)线段判交实现

  • 两次 跨立测验 和 四次 端点测验,实现如下:
SegCrossType Segment2D::segCross(const Segment2D& other) {
    
    
    if (this->lineCross(other) && other.lineCross(*this)) {
    
    
        // 两次跨立都成立,则必然相交与一点
        return SCT_CROSS;
    }
    // 任意一条线段的某个端点是否在其中一条线段上,四种情况
    if (pointOn(other.s) || pointOn(other.t) ||
        other.pointOn(s) || other.pointOn(t)) {
    
    
        return SCT_ENDPOINT_ON;
    }
    return SCT_NONE;
}

2、多边形面积

  • 多边形的面积可以通过将多边形进行三角剖分后,计算每个三角形的面积加和后得到,如图三-2-1所示:
    图三-2-1
  • 求三角形面积采用叉乘来实现;
  • C++代码实现如下:
struct Polygon {
    
    
    int n;
    Point2D p[MAXP];
    double area();
};
double Polygon::area() {
    
    
    double sum = 0;
    p[n] = p[0];
    for (int i = 0; i < n; i++)
        sum += p[i].X(p[i + 1]);
    return sum / 2;
}
  • 这里的点是按照 p [ 0 ] p[0] p[0] 进行极角排序的,并且保证 p [ n ] = p [ 0 ] p[n] = p[0] p[n]=p[0]
  • 如果点是按照顺时针排序,或者凹多边形的情况,求出来的面积可能是负数,如果进行输出的时候需要取下绝对值;

3、凸多边形判定

  • 对于一个凸多边形而言,任意一条边作为向量,其它所有的顶点一定是在他的同侧的,所以可以利用这个性质来判断一个多边形是否是凸多边形;
    图三-3-1
// 是否凸多边形
bool Polygon::isConvex() {
    
    
    bool s[3] = {
    
     false, false, false };
    p[n] = p[0], p[n + 1] = p[1];
    for (int i = 0; i < n; i++) {
    
    
        s[threeValue((p[i + 1] - p[i]) * (p[i + 2] - p[i])) + 1] = true;
        // 叉乘有左有右,肯定是凹的
        if (s[0] && s[2]) return false;
    }
    return true;
}

4、点在多边形内判定

  • 通过从无限远的地方引入一个随机点,然后和判定点做一条线段,把多边形的每条边和这条线段做判交检测,如果相交得到的交点数量,如果是奇数,说明在多边形内;如果是偶数,说明不在多边形内;
  • 射线法同样也适用于凹多边形;

5、多边形点的逆时针转换

  • 如果点是按照顺时针排布的,那么利用叉乘计算出来的面积为负数,根据这个特点可以将多边形的点进行反序,代码如下:
// 转成逆时针顺序
void Polygon::convertToCounterClockwise() {
    
    
    if (area() >= 0) {
    
    
        return;
    }
    for (int i = 1; i <= n / 2; ++i) {
    
    
        Point2D tmp = p[i];
        p[i] = p[n - i];
        p[n - i] = tmp;
    }
}

四、计算几何算法和应用

  • 介于篇幅,以下算法只简单进行应用介绍,具体的算法实现将在后续章节进行详细展开。

1、凸包

  • 凸包可想象为一条刚好包著所有点的橡皮圈,它一定是一个凸多边形。
  • 凸包一般用来进行点集筛选,比如求解点集中的 最大面积三角形、最大周长三角形,因为这些点一定是在点集的凸包上,可以先进行凸包筛选降低点集数量,减少算法运行时间。

2、旋转卡壳

  • 旋转卡壳一般配合凸包,用来求 凸多边形直径(点集的最远点对)、宽、凸多边形间最大距离、凸多边形间最小距离、最小面积外接矩形、最小周长外接矩形等等;

3、半平面交

  • 半平面交一般可以用来求解以下问题:
  • 1、两个多边形的交、并。
  • 2、多边形的核:求解一个区域,使得这个区域内的任意点可以看到给定图形的任意角落。
  • 3、求可以放进凸多边形的最大内接圆。
  • 4、一些线性规划问题。

4、圆和多边形交

  • 将多边形进行三角剖分以后,分别和圆进行求交,转换成三角形和圆相交的问题。

5、最小包围球

  • 最小覆盖圆的三维情况,利用四点确定一个球体,然后判断其它点是否在这个球体中,用随机算法进行优化。

6、模拟退火

  • 模拟退火是一种概率算法,在某个解空间内寻找最优解。比如求解三角形和圆的最大相交面积,寻找最近的最远点等等。

本文所有示例代码均可在以下 github 上找到:github.com/WhereIsHeroFrom/模板/计算几何


在这里插入图片描述


五、计算几何题集整理

点乘

题目链接 难度 解析
HDU 2080 夹角有多大II ★☆☆☆☆ 点乘

旋转

题目链接 难度 解析
HDU 2438 Turn the corner ★☆☆☆☆ 枚举 + 点旋转
HDU 1700 Points on Cycle ★★☆☆☆ 点的旋转
HDU 2898 旋转 ★★★☆☆ 3D点的旋转
HDU 3320 openGL ★★★☆☆ 3D点的旋转
HDU 4998 Rotate ★★★★☆ 2D点旋转

线段判交

题目链接 难度 解析
HDU 1086 You can Solve a Geometry Problem too ★☆☆☆☆ 线段判交
PKU 1127 Jack Straws ★★☆☆☆ 线段判交 + 并查集
PKU 2653 Pick-up sticks ★★☆☆☆ 线段判交
HDU 3803 gxx’s Problem ★★★★☆ 线段判交

多边形面积

题目链接 难度 解析
PKU 1265 Area ★☆☆☆☆ 多边形求面积模板
PKU 1654 Area ★☆☆☆☆ 多边形求面积模板
HDU 2036 改革春风吹满地 ★☆☆☆☆ 多边形求面积模板

点在多边形内

题目链接 难度 解析
PKU 1081 You Who? ★☆☆☆☆ 判定点在多边形内
PKU 2318 TOYS ★☆☆☆☆ 二分 + 判定点在多边形内
PKU 2398 Toy Storage ★☆☆☆☆ 二分 + 判定点在多边形内
PKU 1410 Intersection ★★☆☆☆ 判定点在多边形内
PKU 3788 Interior Points of Lattice Polygons ★★☆☆☆ 判定点在多边形内

凸包

题目链接 难度 解析
PKU 1228 Grandpa’s Estate ★☆☆☆☆ 凸包模板
HDU 1348 Wall ★☆☆☆☆ 凸包模板
HDU 1392 Surround the Trees ★☆☆☆☆ 凸包模板
HDU 2406 Doors and Penguins ★★☆☆☆ 凸包+线段判交
PKU 3348 Cows ★★☆☆☆ 凸包
HDU 2907 Diamond Dealer ★★★☆☆ 凸包
HDU 3285 Convex Hull of Lattice Points ★★★☆☆ 凸包
HDU 6325 Interstellar Travel ★★★★☆ 凸包
HDU 3021 Tree Fence ★★★★☆ 凸包 + DP

旋转卡壳

题目链接 难度 解析
HDU 2202 最大三角形 ★☆☆☆☆ 旋转卡壳模板
HDU 3934 Summer holiday ★★☆☆☆ 凸包 + 旋转卡壳
HDU 5251 矩形面积 ★★★☆☆ 旋转卡壳
HDU 5784 How Many Triangles ★★★☆☆ 旋转卡壳

半平面交

题目链接 难度 解析
PKU 1279 Art Gallery ★★☆☆☆ 半平面交
PKU 1474 Video Surveillance ★★☆☆☆ 半平面交
PKU 3130 How I Mathematician ★★☆☆☆ 半平面交
PKU 2540 Hotter Colder ★★☆☆☆ 半平面交
PKU 2451 Uyuw’s Concert ★★☆☆☆ 半平面交
PKU 3335 Rotating Scoreboard ★★☆☆☆ 半平面交
PKU 3525 Most Distant Point from the Sea ★★★☆☆ 半平面交
HDU 1632 Polygons ★★☆☆☆ 半平面交
HDU 3060 Area2 ★★☆☆☆ 半平面交

最小覆盖集

题目链接 难度 解析
ZJU 1450 Minimal Circle ★★★☆☆ 最小覆盖圆
HDU 3007 Buried memory ★★★☆☆ 最小覆盖圆
HDU 3932 Groundhog Build Home ★★★☆☆ 最小覆盖圆
BZOJ 2823 信号塔 ★★★☆☆ 最小覆盖圆
PKU 2069 Super Star ★★★★☆ 最小包围球
HDU 2226 Stars ★★★★☆ 最小包围球

模拟退火

题目链接 难度 解析
PKU 1379 Run Away ★★☆☆☆ 寻找最远的最近点
PKU 2069 Super Star ★★☆☆☆ 寻找最近的最远点(3维)
PKU 2420 A Star not a Tree? ★★☆☆☆ 寻找到所有点距离最小的点
PKU 2600 Gemetrical dreams ★★★☆☆ 模拟向量旋转
HDU 3932 Groundhog Build Home ★★★★☆ 寻找最近的最远点
HDU 3644 A Chocolate Manufacturer’s Problem ★★★★☆ 任意简单多边形的最大内接圆
HDU 4717 The Moving Points ★★★★☆ 寻找距离最小的最远点对
HDU 5017 Ellipsoid ★★★★☆ 曲面最小距离
HDU 3202 Circle and Triangle ★★★★★ 三角形和圆的最大相交面积(难题)

猜你喜欢

转载自blog.csdn.net/WhereIsHeroFrom/article/details/111829732