看了刘汝佳的《算法竞赛入门经典》之训练指南这本书的几何问题之后,受益匪浅,然后根据我自己平时的几何知识写下这篇关于二维几何基础的文章,希望对大家有用。
首先我们要理解什么是向量,向量就是有大小和方向的量。在平面坐标系中,向量用x,y表示。等于向量起点到终点的位移。以下是它们的常用定义:
struct Point
{
double x,y;
Point(double x=0,double y=0):x(x),y(y){}//构造函数
};
typedef Point Vector;
//向量+向量=向量,点+向量=点
Point operator+(Point A,Point B)
{
return Point(A.x+B.x,A.y+B.y);
}
//点-点=向量
Point operator-(Point A,Point B)
{
return Point(A.x-B.x,A.y-B.y);
}
//向量*数=向量
Point operator*(Point A,double p)
{
return Point(A.x*p,A.y*p);
}
//向量/数=向量
Point operator/(Point A,double p)
{
return Point(A.x/p,A.y/p);
}
double eps=1e-10;
//如果等于0,返回0,小于0返回-1,大于0,返回1
int dcmp(double x)
{
if(fabs(x)<eps)return 0;else return x<0?-1:1;
}
//判断是否相等
bool operator==(const Point& a,const Point &b)
{
return dcmp(a.x-b.x)==0&&dcmp(a.y-b.y)==0;
}
int main()
{
Point a,b,c;
double p=2;
a.x=4;
a.y=3;
b.x=2;
b.y=5;
c=a+b;//向量+向量=向量
c=a-b;//点-点=向量
c=a/p;//向量/数=向量
c=a*p;//向量*数=向量
return 0;
}
基本运算
点积是两个向量v和w的点积等于二者长度的乘积在乘上它们夹角的余弦。如果两向量垂直,点积就等于0。两个向量OA和OB的点积等于xa*xb+ya*yb。下面是点积计算方法,以及用点积计算向量长度和夹角的函数。
//点积计算方法
double Dot(Point A,Point B)
{
return A.x*B.x+A.y*B.y;
}
//点到原点的长度
double Lenth(Point A)
{
return sqrt(Dot(A,A));
}
//角度计算
double Angle(Point A,Point B)
{
return acos(Dot(A,B)/Lenth(A)/Lenth(B));
}
叉积就是两个向量v和w组成的三角形的有向面积的两倍。叉积的计算方法及三角形面积的两倍的计算方法如下:
//就算OA和OB的叉积
double Cross(Point A,Point B)
{
return A.x*B.y-A.y*B.x;
}
//计算三角形的面积的两倍
double Area(Point A,Point B,Point C)
{
return Cross(B-A,C-A);
}
向量的旋转。向量可以绕起点旋转,公式为x'=x*cosa-y*sina,y'=x*sina+y*cosa。其中a为逆时针旋转的角。代码如下:
//向量的旋转
Point Rotate(Point A,double rad)//rad是弧度
{
return Point(A.x*cos(rad)-A.y*sin(rad),A.x*sin(rad)+A.y*cos(rad));
}
下面的函数计算向量的单位法线,即左转90度以后把长度归一化。
//计算向量单位法线
Point Normal(Point A)
{
double l=Lenth(A);
return Point(-A.y/l,A.x/l);
}
点和直线
直线上一点可以用直线上一点p0和方向向量v来表示。直线上所有点p=p0+t*v.其中t为参数。如果已知直线上的两个不同点A和B,则方向向量为B-A,所以参数方程A+(B-A)*t。对于直线t没有限制,射线t>0,线段0<t<1。
直线交点。设直线分别为P+tv和Q+tw,设向量u=P-Q,交点在第一条直线的参数为t1,第二条直线的参数为t2,则x和y坐标可以列出一个方程,解得:
t1=cross(w,u)/cross(v,w),t2=cross(v,u)/cross(v,w)。
代码如下:
//直线交点公式
Point GetLineIntersection(Point P,Point V,Point Q,Point W)
{
Point u=P-Q;
double t=Cross(W,u)/Cross(V,W);
return P+V*t;
}
点到直线的距离。点到直线的距离是一个常用函数,可以用叉积算出,即用平行四边形的面积除以底,代码如下:
//点到直线的距离
double DistanceToLine(Point P,Point A,Point B)
{
Point v1=B-A,v2=P-A;
return fabs(Cross(v1,v2))/Lenth(v1);
}
点到线段的距离有两种情况,代码如下:
//点到线段的距离
double DistanceToSegment(Point P,Point A,Point B)
{
if(A==B) return Lenth((P-A));
Point v1=B-A,v2=P-A,v3=P-B;
if(dcmp(Dot(v1,v2))<0)return Lenth(v2);
else if(dcmp(Dot(v1,v3))>0)return Lenth((v3));
else return fabs(Cross(v1,v2))/Lenth(v1);
}
点在直线上的投影,代码如下:
//点P在直线AB上的投影
Point GetLineProjecton(Point P,Point A,Point B)
{
Point v=B-A;
return A+v*(Dot(v,P-A)/Dot(v,v));
}
另外一种实现点和向量的方法,即使用C++里的复数。
#include<complex>
using namespace std;
typedef complex<double> Point;
typedef Point Vector;
这样定义之后,我们拥有了构造函数、加减法和数量积。用real(p)和imag(p)访问实部和虚部,conj(p)返回共轭复数,即conj(a+bi)=a-bi。相关函数如下:
double Dot(Point A,Point B)
{
return real(conj(A)*B);
}
double Cross(Point A,Point B)
{
return imag(conj(A)*B);
}
Point Rotate(Point A,double rad)
{
return A*exp(Point(0,rad));
}
线段相交判定。给定两条线段,判断是否相交。我们定义“规范相交”为两线段恰好有一个公共点,且不在任何一条线段的端点。线段规范相交的充要条件是:每条线段的两个端点都在另一条线段的两侧。代码如下:
//判断线段相交(不含端点)
bool SegmentProperIntersection(Point a1,Point a2,Point b1,Point b2)
{
double c1=Cross(a2-a1,b1-a1),c2=Cross(a2-a1,b2-a1),
c3=Cross(b2-b1,a1-b1),c4=Cross(b2-b1,a2-b1);
return dcmp(c1)*dcmp(c2)<0&&dcmp(c3)*dcmp(c4)<0;
}
如果允许在端点出相交,情况就比较复杂了,有可能共线,还有可能某个端点在另外一条线段上。为了判断上述情况是否发生,还需要如下一段判断一个点是否在一条线段上(不含端点)的代码:
//判断一个点P是否在一条线段a1a2上
bool OnSegment(Point p,Point a1,Point a2)
{
return dcmp(Cross(a1-p,a2-p))==0&&dcmp(Dot(a1-p,a2-p));
}
多边形
计算多边形的面积。如果多变形是凸的,可以从第一个顶点出发把凸多边形分成n-2个三角形,然后把面积加起来。代码如下:
//计算多边形的面积
double ConvexPolygonArea(Point* p,int n)
{
double area=0;
for(int i=1;i<n-1;i++)
area+=Cross(p[i]-p[0],p[i+1]-p[0]);
return area/2;
}
可以另取p[0]点为划分顶点,一方面可以少算两个叉积,另一方面也减少乘法溢出的可能性,还不用特殊处理。代码如下:
double PolygonArea(Point* p,int n)//p是一个结构体数组
{
double area=0;
for(int i=1;i<n-1;i++)
area+=Cross(p[i]-p[0],p[i+1]-p[0]);
return area/2;
}
int main()
{
Point a[4];
a[0].x=0;
a[0].y=0;
a[1].x=3;
a[1].y=0;
a[2].x=3;
a[2].y=3;
a[3].x=0;
a[3].y=3;
double area=PolygonArea(a,4);
printf("%lf",area);
return 0;
}
将度转化为弧度代码:
double torad(double x)//将度转为弧度
{
double pi = acos(-1);
return x*pi/180;
}