二维计算几何 知识讲解

来源:互联网 发布:欧洲文明概论网络课 编辑:程序博客网 时间:2024/05/17 01:48

二维计算几何 知识讲解

向量和叉积

大多数计算几何相关问题都需要用到向量。
向量的定义及加减数乘点积运算大家在高中应该已经学过了,这里就不再赘述,下面主要介绍下想向量的叉积。
设向量w(x1,y1),v(x2,y2),则w和v的叉积

w×v=x1y2x2y1
这是一个标量(或者可以解释为垂直于两向量所在平面的一个矢量).
他的几何意义为w,v组成的三角形的有向面积的两倍,即
|w×v|=|w||v|sinθ
所谓“有向面积”,当顺着第一个向量w看,如果v在w的左边,w×v>0,反之w×v<0;当w,v共线是则等于零。利用这个性质,可以很方便判断两向量之间顺逆时针关系

利用叉积的性质,还可以判断折线段的拐点方向
设P0,P1,P2为折线段P0P1P2的三个顶点,

  • (P1P0)×(P2P1)>0 则折线段在P1点向左拐
  • (P1P0)×(P2P1)>0 则折线段在P1点向右拐
  • (P1P0)×(P2P1)=0 则折线段三点共线

下面给出常用的定义和方法:

struct Point {//定义Point结构体  点和向量的表示    double x, y;    Point(double xx = 0, double yy = 0) :x(xx), y(yy) {};//构造函数     //重载“-”运算符     //点-点=向量 向量-向量=向量    Point operator - (const Point &b)const {        return Point(x - b.x, y - b.y);    }    //重载运算符计算叉积    double operator ^(const Point &b)const {        return x*b.y - y*b.x;    }    //重载运算符计算点积    double operator *(const Point &b)const {        return x*b.x + y*b.y;    }};

求多边形面积

对于凸多边形,我们可以任取多边形内一点为基准点,然后将多边形划分为多个三角形,进而求解出其面积,但在求解凹多边形面积时,这个方法貌似行不通了,因为很可能把凹进去的部分也算上,有没有什么方法能凹多边形面积可以像凸多边形那样求解呢?利用叉积可以很好的解决这个问题。
上文已经介绍,叉积等于矢量三角形有向面积的两倍,正因为面积是有向的,所以刚好可以把凹进去的部分抵消掉(想不清楚可以自己动手验证下)这样,凹多边形的面积问题也可以像凸多边形一样求解了。(实际上,不仅可以选取多边形内一点为基准点,选取平面内任意一点都是可行的,但实际操作中通常选取第一个点或坐标原点。)

下面附上求解代码(以坐标原点为基准点):

//计算多边形面积 点的编号0 - n-1double CalcArea(Point p[], int n) {    double res = 0;    for (int i = 0; i < n; i++)        res += (p[i] ^ p[(i + 1) % n]);    return fabs(res / 2);}

判断线段相交

要判断两线段是否相交,首先想到的是求出线段所在直线的交点,再判断交点是否在线段上,但这样做不仅麻烦,还要考虑许多特殊情况,实际上通常采用以下两步来判断:
1.判断两线段所在矩形是否相交(快速排斥实验)
2.判断一条线段的两端点是否在另一条线段两侧,可以利用叉积求出。(跨立实验)该检验需要对每条线段各做一次
注:如果是判断直线和线段是否相交,可以不进行快速排斥实验而且只需要做一次跨立实验(请自己思考是哪一次)

struct Line {    Point s, e;    Line() {}    Line(Point a, Point b) :s(a), e(b) {}};const double eps = 1e-8;int sgn(double x) {    if (fabs(x) < eps)return 0;    if (x < 0)return -1;    return 1;}bool inter(Line l1, Line l2) {    return max(l1.s.x, l1.e.x) >= min(l2.s.x, l2.e.x)        && max(l1.s.y, l1.e.y) >= min(l2.s.y, l2.e.y)        && max(l2.s.x, l2.e.x) >= min(l1.s.x, l1.e.x)        && max(l2.s.y, l2.e.y) >= min(l1.s.y, l1.e.y)//快速排斥实验        && sgn((l2.s - l1.e) ^ (l1.s - l1.e))*sgn((l2.e - l1.e) ^ (l1.s - l1.e)) <= 0//跨立实验1        && sgn((l1.s - l2.e) ^ (l2.s - l2.e))*sgn((l1.e - l2.e) ^ (l2.s - l2.e)) <= 0;//跨立实验2}

看完上面的代码可能有些同学会心存疑惑,因为上面出现了没有提起过的常量eps和函数sgn();其实他们的存在是因为浮点误差的存在,由于浮点数将数字表示成×2x的形式 ,对于一定范围内的整数,可以精确表示,但对于0.1这样的简单的小数,却无法精确表示。对于不能精确表示的数,只能通过所能表示的数中最接近的数近似表示,近似表示造成的误差称为舍入误差。
比较包含舍入误差的浮点数是所采用的方法,一般是选取合适的足够小的常数EPS,按如下规则处理:

  • a<0 -> a < -EPS
  • a<=0 -> a < EPS
  • a==0 -> abs(a) < EPS

判断点在多边形内

判断点在多边形内一般有两种方法:射线法转角法
射线法:从点向任意方向引一条射线,若射线与多边形边界有奇数个交点,则点在多边形内,否则,点在多边形外。注意如果射线在端点处和多边形相交或者射线和边重合的情况需要特殊处理。
转角法:基本思想是判断多边形和相对于点转了多少度。具体来说,我们把多边形每条边的转角加起来,如果是360 ,说明在多边形内;如果是0,说明在多边形外,如果是180;说明在多边形边界上。
转角法比射线法更方便,但如果直接按照定义实现,不仅速度慢而且会有精度误差,所以我们使用了一种更加巧妙地方法。因为最后计算到的角度值只可能是上面提到的三种,所以我们根本不必去计算,只要去判定是哪一种就好。具体方法:假想有一条向右的射线,统计多边形穿过这条射线正反多少次,把这个数计为绕数wn(Winding Number),逆时针穿过时wn加1.顺时针穿过时,wn减1。具体代码请参照《算法竞赛入门经典训练指南》P271.

凸包

凸包的定义:将给定点(点集)包围在内部的面积最小的凸多边形。
形象的说:用一个橡皮筋“套”住所有点后橡皮筋的形状就可以看成一个 凸包。
这里介绍一种比较容易实现的基于平面扫描法的Graham扫描算法。

实现过程:

首先,把点集按先x坐标再y坐标从小到大排序。那么排序后的第一个点和最后一个点必然是凸包上的顶点,它们之间的部分可以分成上下两条链分别求解。求下侧的链时只要从小到大处理排序后的点列,逐步构造凸包。在构造过程中的凸包末尾加上新的顶点后,可能会破坏凸性,此时只要将凹的部分的点从末尾去除就好。求上侧的链时也是一样地从大到小处理即可。

代码:

//比较大小bool cmp_x(const Point &p, const Point &q) {    if (p.x != q.x)return p.x < q.x;    return p.y < q.y;}//求凸包vector<Point> convex_hull(Point* ps, int n) {    sort(ps, ps + n, cmp_x);    int k = 0;    vector<Point> qs(n * 2);//构造中的凸包    //构造凸包的下侧    for(int i = 0; i < n; i++){        while (k > 1 && (qs[k - 1] - qs[k - 2])*(ps[i] - qs[k - 1]) <= 0)k--;        qs[k++] = ps[i];}    //构造凸包的上侧    for (int i = n - 2, t = k; i >= 0; i--) {        while(k > t && (qs[k - 1] - qs[k - 2])*(ps[i] - qs[k - 1]) <= 0)k--;        qs[k++] = ps[i];    }    qs.resize(k - 1);    return qs;}

以上即时这篇文章的全部内容,笔者才疏学浅,以上内容难免有疏漏之处,如有疑问,欢迎各位读者与我联系,相互探讨。

原创粉丝点击