浅析ACM竞赛中的计算几何常用代码模板

浅析ACM竞赛中的计算几何常用代码模板


1. 计算几何基本代码框架

#include <bits/stdc++.h>
using namespace std;
const double eps = 1e-6;//eps用于控制精度
const double pi = acos(-1.0);//pi
struct Point//点或向量
{
    double x, y;
    Point() {}
    Point(double x, double y) :x(x), y(y) {}
};
typedef Point Vector;
Vector operator + (Vector a, Vector b)//向量加法
{
    return Vector(a.x + b.x, a.y + b.y);
}
Vector operator - (Vector a, Vector b)//向量减法
{
    return Vector(a.x - b.x, a.y - b.y);
}
Vector operator * (Vector a, double p)//向量数乘
{
    return Vector(a.x*p, a.y*p);
}
Vector operator / (Vector a, double p)//向量数除
{
    return Vector(a.x / p, a.y / p);
}
int dcmp(double x)//精度三态函数(>0,<0,=0)
{
    if (fabs(x)<eps)return 0;
    else if (x>0)return 1;
    return -1;
}
bool operator == (const Point &a, const Point &b)//向量相等
{
  return dcmp(a.x - b.x) == 0 && dcmp(a.y - b.y) == 0;
}

2. 基本向量计算

double Dot(Vector a, Vector b)//内积
{
    return a.x*b.x + a.y*b.y;
}
double Length(Vector a)//模
{
    return sqrt(Dot(a, a));
}
double Angle(Vector a, Vector b)//夹角,弧度制
{
    return acos(Dot(a, b) / Length(a) / Length(b));
}
double Cross(Vector a, Vector b)//外积
{
    return a.x*b.y - a.y*b.x;
}
Vector Rotate(Vector a, double rad)//逆时针旋转
{
    return Vector(a.x*cos(rad) - a.y*sin(rad), a.x*sin(rad) + a.y*cos(rad));
}
double Distance(Point a, Point b)//两点间距离
{
    return sqrt((a.x - b.x)*(a.x - b.x) + (a.y - b.y)*(a.y - b.y));
}
double Area(Point a, Point b, Point c)//三角形面积
{
    return fabs(Cross(b - a, c - a) / 2);
}

模板解析

  • Rotate函数(求旋转后的坐标)

Rotate函数是根据坐标旋转公式

{ x = x cos θ y sin θ y = x sin θ + y cos θ

实现的,其中 ( x , y ) 是旋转前的坐标, ( x , y ) 是旋转后的坐标, θ 是绕原点逆时针旋转的角


3. 直线与线段

bool Intersect(Point A, Point B, Point C, Point D)//线段相交(不包括端点)
{
    double t1 = Cross(C - A, D - A)*Cross(C - B, D - B);
    double t2 = Cross(A - C, B - C)*Cross(A - D, B - D);
    return dcmp(t1) < 0 && dcmp(t2) < 0;
}
bool StrictIntersect(Point A, Point B, Point C, Point D) //线段相交(包括端点)
{
    return
        dcmp(max(A.x, B.x) - min(C.x, D.x)) >= 0
        && dcmp(max(C.x, D.x) - min(A.x, B.x)) >= 0
        && dcmp(max(A.y, B.y) - min(C.y, D.y)) >= 0
        && dcmp(max(C.y, D.y) - min(A.y, B.y)) >= 0
        && dcmp(Cross(C - A, D - A)*Cross(C - B, D - B)) <= 0
        && dcmp(Cross(A - C, B - C)*Cross(A - D, B - D)) <= 0;
}
double DistanceToLine(Point A, Point M, Point N)//点A到直线MN的距离,Error:MN=0
{
    return fabs(Cross(A - M, A - N) / Distance(M, N));
}
bool OnSegment(Point P1, Point P2, Point P3)//判断P2是否在线段P1P3上
{
    return
        P2.x >= min(P1.x, P3.x) && P2.x <= max(P1.x, P3.x)
        && P2.y >= min(P1.y, P3.y) && P2.y <= max(P1.y, P3.y)
        && dcmp(Cross(P2 - P1, P3 - P1)) == 0;
}
Point GetLineIntersection(Point P, Vector v, Point Q, Vector w)//两直线的交点
{
    Vector u = P - Q;
    double t = Cross(w, u) / Cross(v, w);
    return P + v*t;
}

模板解析

  • Intersect函数(判断两线段是否相交,不考虑端点)

图 线段相交

从上左图中我们可以看到,如果 A B C D 相交,那么 A B C D 的异侧, C D A B 的异侧,反过来如果 A B C D 的异侧, C D A B 的异侧,那么 A B C D 相交,这就找到了线段相交的一个充要条件。进一步,如果 A B C D 的异侧,那么 C A × C B D A × D B 异号,如果 C D A B 的异侧,那么 A C × A D B C × B D 异号。
* DistanceToLine函数(计算点到直线的距离)

图 点到直线的距离

如图所示,要计算点 A 到直线 M N 的距离,可以构建以 A M M N 为邻边的平行四边形,其面积

S = | M A × M N |

又 平行四边形的面积为底乘高,选取 M N 为底,高为
d = S | M N |

即为所求的A到直线 M N 的距离。
* GetLineIntersection函数(计算两直线交点)

在求两直线交点的实际应用中,通常的已知量是直线上某一点的坐标和直线的方向向量,对于两直线 l 1 , l 2 , P ( x 1 , y 1 ) , Q ( x 2 , y 2 ) 分别在 l 1 , l 2 上, l 1 , l 2 的方向向量分别为 v = ( a 1 , b 1 ) , w = ( a 2 , b 2 ) ,由此可以得到两直线的方程

l 1 : ( x x 1 , y y 1 ) × ( a 1 , b 1 ) = 0

l 2 : ( x x 2 , y y 2 ) × ( a 2 , b 2 ) = 0


l 1 : a 1 x b 1 y = a 1 x 1 b 1 y 1

l 2 : a 2 x b 2 y = a 2 x 2 b 2 y 2

联立两直线的方程,由克拉默法则得,方程组的解为
{ x = | a 1 x 1 b 1 y 1 b 1 a 2 x 2 b 2 y 2 b 2 | | a 1 b 1 a 2 b 2 | y = | a 1 a 1 x 1 b 1 y 1 a 2 a 2 x 2 b 2 y 2 | | a 1 b 1 a 2 b 2 |

进一步进行化简,得到
( x , y ) = P + w × u v × w v

其中 u = P Q


4.多边形

4.1 判断点是否在多边形内

/*模板说明:P[]为多边形的所有顶点,下标为0~n-1,n为多边形边数*/
Point P[1005];
int n;
bool InsidePolygon (Point A) //判断点是否在凸多边形内(角度和判别法)
{
    double alpha = 0;
    for (int i = 0; i < n; i++)
        alpha += fabs(Angle(P[i] - A, P[(i + 1) % n] - A));
    return dcmp(alpha - 2 * pi) == 0;
}

模板解析

我们常用角度和判断法来判断某个点是否在多边形内。
设有(凸) n ( n 3 ) 边形 P 0 P 2 P n 1 ,点的顺序为顺时针或逆时针,以及点 A ,记

θ i = { < A P i , A P i + 1 > , i < n 1 < A P n 1 , A P 0 > , i = n 1

A 在多边形内等价于
i = 0 n 1 θ i = 2 π

4.2 多边形面积

/*模板说明:P[]为多边形的所有顶点,下标为0~n-1,n为多边形边数*/
Point P[1005];
int n;
double PolygonArea()//求多边形面积(叉积和计算法)
{
    double sum = 0;
    Point O = Point(0, 0);
    for (int i = 0; i < n; i++)
        sum += Cross(P[i] - O, P[(i + 1) % n] - O);
    if (sum < 0)sum = -sum;
    return sum/2;
}

模板解析

设有 n ( n 3 ) 边形 P 0 P 2 P n 1 ,点的顺序为顺时针或逆时针,以及多边形内的一点 A ,其中

P i = ( x i , y i ) , i = 0 , 1 , , n 1

A = ( x A , y A )

把多边形切割成如下所示 n 个三角形

图 多边形面积

多边形的面积等于所有三角形(有向)面积之和,代入坐标计算得

S = | 1 2 i = 0 n 2 ( x i y i + 1 x i + 1 y i ) + 1 2 ( x n 1 y 0 x 0 y n 1 ) |

A 的坐标无关,因此 A 的位置可以任意取

4.3 三角形内心

Point TriCenter(Point A, Point B, Point C) //求三角形的内心
{
    double a = Distance(B, C), b = Distance(A, C), c = Distance(A, B);
    return Point((a*A.x + b*B.x + c*C.x) / (a + b + c), (a*A.y + b*B.y + c*C.y) / (a + b + c));
}

模板解析

A ( x A , y A ) , B ( x B , y B ) , C ( x C , y C ) 形成的三角形的内心坐标为

( a x A + b x B + c x c a + b + c , a y A + b y B + c y c a + b + c )

其中 a = | B C | , b = | A C | , c = | A B | ,读者可自行验证


5. 凸包

//求凸包
/*模板说明:n为所有点的个数,top为栈顶,P[]为所有点,下标为0~n-1,result[]为凸包上的点,下标为0~top,包含凸包边上的点,Error:有重复点*/
int n, top;
Point P[10005], result[10005];
bool cmp(Point A, Point B)
{
    double ans = Cross(A - P[0], B - P[0]);
    if (dcmp(ans) == 0)
        return dcmp(Distance(P[0], A) - Distance(P[0], B)) < 0;
    else
        return ans > 0;
}
void Graham()//Graham凸包扫描算法
{
    for (int i = 1; i < n; i++)//寻找起点
        if (P[i].y < P[0].y || (dcmp(P[i].y - P[0].y) == 0 && P[i].x < P[0].x))
            swap(P[i], P[0]);
    sort(P + 1, P + n, cmp);//极角排序,中心为起点
    result[0] = P[0];
    result[1] = P[1];
    top = 1;
    for (int i = 2; i < n; i++)
    {
        while (Cross(result[top]-result[top-1],P[i]-result[top-1])<0 && top>=1)
            top--;
        result[++top] = P[i];
    }
}

模板解析

凸包 :在一个实数向量空间 V 中,对于给定集合 X ,所有包含 X 的凸集的交集 S 被称为 X 的凸包。在ACM竞赛中,常出现二维平面的凸包问题。本文只介绍求凸包的算法之一——Graham’s scan算法。

第一步:找到最下边的点,如果有多个点纵坐标相同的点都在最下方,则选取最左边的,记为点 A 。这一步只需要扫描一遍所有的点即可,时间复杂度为 O ( n )

第二步:将所有的点按照 A P i 的极角大小进行排序。时间复杂度为 O ( n l o g n )

第三步:维护一个栈,以保存当前的凸包。按第二步中排序得到的结果,依次将点加入到栈中,如果当前点与栈顶的两个点不是“向左转”的,就表明当前栈顶的点并不在凸包上,而我们需要将其弹出栈,重复这一个过程直到当前点与栈顶的两个点是“向左转”的。这一步的时间复杂度为 O ( n )


6. 圆

struct Circle
{
    Point c;
    double r;
    Point point(double a)//基于圆心角求圆上一点坐标
    {
        return Point(c.x+cos(a)*r,c.y+sin(a)*r);
    }
};
double Angle(Vector v1)
{
    if(v1.y>=0)return Angle(v1,Vector(1.0,0.0));
    else return 2*pi-Angle(v1,Vector(1.0,0.0));
}
int GetCC(Circle C1,Circle C2)//求两圆交点
{
    double d=Length(C1.c-C2.c);
    if(dcmp(d)==0)
    {
        if(dcmp(C1.r-C2.r)==0)return -1;//重合
        else return 0;
    }
    if(dcmp(C1.r+C2.r-d)<0)return 0;
    if(dcmp(fabs(C1.r-C2.r)-d)>0)return 0;

    double a=Angle(C2.c-C1.c);
    double da=acos( (C1.r*C1.r+d*d-C2.r*C2.r)/(2*C1.r*d) );
    Point p1=C1.point(a-da),p2=C1.point(a+da);
    if(p1==p2)return 1;
    else return 2;
}

模板解析

  • Circle结构体

我们用圆心坐标 ( x 0 , y 0 ) 和圆的半径 r 可以唯一地表示一个圆,根据圆的参数方程

{ x = x 0 + r cos θ y = y 0 + r sin θ

可以由 θ 求得圆上一点的坐标

  • GetCC函数(计算两圆交点)

设两圆 C 1 , C 2 ,其半径为 r 1 , r 2 ( r 1 r 2 ) ,圆心距为 d ,则有
1. 两圆重合⟺ d = 0 r 1 = r 2
2. 两圆外离⟺ d > r 1 + r 2
3. 两圆外切⟺ d = r 1 + r 2
4. 两圆相交⟺ r 1 r 2 < d < r 1 + r 2
5. 两圆内切⟺ d = r 1 r 2
6. 两圆内含⟺ d < r 1 r 2
对于情形4,如下图所示,要求 A B 的坐标,只需求 A C 1 D B C 1 D ,进而通过上文中所述方法即可求得。

图 两圆交点

A C 1 D = C 2 C 1 D + A C 1 C 2

B C 1 D = C 2 C 1 D A C 1 C 2

C 2 C 1 D 可以通过 C 1 , C 2 的坐标求得,而 A C 1 C 2 可以通过 Δ A C 1 C 2 上的余弦定理求得。

对于情形3和情形5,用上述方法求出的两点坐标是相同的,即切点坐标

猜你喜欢

转载自blog.csdn.net/qq_39515621/article/details/80888074