计算几何总结
前置知识
三角函数
如图:
\(tan(θ)=AB/BO\)
\(sin(θ)=AB/AO\)
\(cos(θ)=BO/AO\)
值的正负性:
向量
严格的解释见这里
形象的解释就是一个表示位移的没有固定位置的箭头
一些约定
- 如没有特别说明单位,角度默认为弧度
- 如没有特别说明,多边形点的存储方式为逆时针储存。
二维几何
基础
基本定义
struct Point
{
double x,y;
Point(double x=0,double y=0):x(x),y(y){}
};
typedef Point Vector;
const double Pi=acos(-1);
inline double Angle(const Vector &A)//向量极角
{ return atan2(A.y,A.x); }
inline double R_to_D(double rad)
{ return 180/Pi*rad; }
inline double D_to_R(double D)
{ return Pi/180*D; }
inline Vector Rotate(const Vector &A,double rad)//将向量A逆时针旋转rad
{
double csr=cos(rad),sir=sin(rad);
return Vector(A.x*csr-A.y*sir,A.x*sir+A.y*csr);
}
inline Vector Normal(const Vector &A)//单位法向量(单位向量左转90°)
{
double L=Length(A);
return Vector(-A.y/L,A.x/L);
}
inline Vector Format(const Vector &A)//单位向量
{
double L=Length(A);
return Vector(A.x/L,A.y/L);
}
基本四则运算
inline Vector operator+(const Vector &A,const Vector &B)
{ return Vector(A.x+B.x,A.y+B.y); }
inline Vector operator-(const Point &a,const Point &b)
{ return Vector(a.x-b.x,a.y-b.y); }
inline Vector operator*(const Vector &A,double p)
{ return Vector(A.x*p,A.y*p); }
inline Vector operator/(const Vector &A,double p)
{ return Vector(A.x/p,A.y/p); }
inline bool operator<(const Point &a,const Point &b)
{ return a.x<b.x||(a.x==b.x&&a.y<b.y); }
const double ops=1e-10;
inline int dcmp(double x)
{ return (x>0?x:-x)<ops?0:(x>0?1:-1); }
inline bool operator==(const Point &a,const Point &b)
{ return dcmp(a.x-b.x)==0&&dcmp(a.y-b.y)==0; }
点积与叉积
点积定义:两个向量v和w的点积等于二者长度的乘积乘上它们夹角的余弦。
点积性质:
1.满足交换律
2.对于向量加减法满足分配律(\(u*(v+w)=u*v+v*w\))
3.见“正负性总结”
叉积定义:两个向量v和w的叉积等于v和w组成的三角形的有向面积的两倍
如图:
面积就是红色平行四边形的面积,有向则是指:若w在v的左边,则叉积为正,反之为负。
叉积性质:
1.\(cross(v,w)=-cross(w,v)\)
2.见“正负性总结”
正负性总结:
代码:
inline double Dot(const Vector &A,const Vector &B)
{ return A.x*B.x+A.y*B.y; }
inline double Length(const Vector &A)
{ return sqrt(Dot(A,A)); }
inline double Angle(const Vector &A,const Vector &B)//A和B之间的夹角
{ return acos(Dot(A,B)/Length(A)/Length(B)); }
inline double Cross(const Vector &A,const Vector &B)
{ return A.x*B.y-A.y*B.x; }
inline double Area2(const Point &a,const Point &b,const Point &c)
{ return Cross(b-a,c-a); }
两直线交点
设直线分别为\(P+tv\)和\(Q+tw\),设向量\(u=P-Q\),则交点在第一条直线的参数为\(t_1\),解得:
\(\Large t_1=\frac{cross(w,u)}{cross(v,w)}\)
即图中蓝色部分面积除以红色部分面积
代码:
inline Point GetLineIntersection
(const Point &A1,const Point &B1,const Point &A2,const Point &B2)
{
Vector u=A1-A2,v=B1-A1,w=B2-A2;
double t=Cross(w,u)/Cross(v,w);
return A1+v*t;
}
点到直线距离
点到直线距离:用平行四边形的面积除以底即可。
点到线段距离:注意用点积判断与从端点引出的线段法向量之间的相对位置
代码:
inline double DistanceToLine
(const Point &P,const Point &A,const Point &B)
{
Vector v1=B-A,v2=P-A;
return fabs(Cross(v1,v2))/Length(v1);
}
inline double DistanceToSegment
(const Point &P,const Point &A,const Point &B)
{
if(A==B) return Length(P-A);
Vector v1=B-A,v2=P-A,v3=P-B;
if(dcmp(Dot(v1,v2))<0) return Length(v2);
else if(dcmp(Dot(v1,v3))>0) return Length(v3);
else return fabs(Cross(v1,v2))/Length(v1);
}
点在直线上的投影(垂足)
首先,把直线\(AB\)写成参数式\(A+tv\)(v=\(\overrightarrow{A B}\)),并设投影点\(Q\)的参数为\(t_0\),显然\(Q=A+t_0 v\)。根据\(PQ\)垂直于\(AB\),两个向量点积为0,即\(Dot(v,P-(A+t_0 v))=0\)。根据分配律有\(Dot(v,P-A)-t_0 *Dot(v,v)=0\),这样就可以解出$\Large t_0 = \frac{Dot(v,P-A)}{Dot(v,v)} \(,从而得到\)Q$点。
代码:
inline Point GetLineProjection
(const Point &P,const Point &A,const Point &B)
{
Vector v=B-A;
return A+v*(Dot(v,P-A)/Dot(v,v));
}
线段相交、点于线段的关系
用叉积判断端点之间的相对位置即可
代码:
inline bool SegmentProperIntersection
(const Point &a1,const Point &a2,const Point &b1,const 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;
}
inline bool OnSegment
(const Point &p,const Point &a1,const Point &a2)//不包含端点
{ return dcmp(Cross(a1-p,a2-p))==0&&dcmp(Dot(a1-p,a2-p))<0; }
多边形有向面积
用叉积计算即可。
代码:
inline double PolygonArea(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;
}
圆的基本定义
struct Circle
{
Point c;
double r;
Circle(Point c=Point(),double r=0):c(c),r(r){}
inline Point point(double a)//通过极角获取圆上一点
{ return Point(c.x+cos(a)*r,c.y+sin(a)*r); }
};
直线与圆的交点
解个方程组即可。
代码:
inline int GetLineCircleIntersection
(const Point &A,const Point &B,const Circle &C,vector<Point> &sol)
{
double d=DistanceToLine(C.c,A,B);
int mode=dcmp(d-C.r);
if(mode>0) return 0;//相离
Point P=GetLineProjection(C.c,A,B);
if(mode==0)//相切
{
sol.push_back(P);
return 1;
}
double dist=sqrt(C.r*C.r-d*d);
Vector v=Format(B-A);
sol.push_back(P-v*dist);
sol.push_back(P+v*dist);
return 2;
}
圆与的交点
解方程即可。
代码:
inline GetCircleCircleIntersection
(Circle C1,Circle C2,vector<Point> &sol)
{
if(C1.r<C2.r) swap(C1,C2);//to make sure C1 is bigger than C2
double D=Length(C1.c-C2.c);
if(dcmp(D)==0)
return dcmp(C1.r-C2.r)==0?-1:0;
if(dcmp(C1.r+C2.r-D)<0) return 0;
if(dcmp(fabs(C1.r-C2.r)-D)>0) return 0;
double d1=((C1.r*C1.r-C2.r*C2.r)/D+D)/2;
double x=sqrt(C1.r*C1.r-d1*d1);
Point O=C1.c+Format(C2.c-C1.c)*d1;
Point P1=O+Normal(O-C2.c)*x,P2=O-Normal(O-C2.c)*x;
sol.push_back(P1);
if(P1==P2) return 1;
sol.push_back(P2);
return 2;
}
过圆外一点作圆的切线
细节见代码:
inline int GetTangents
(const Point P,const Circle C,vector<Point> &v)
{
Vector u=C.c-P;
double dist=Length(u);
int mode=dcmp(dist-C.r);
if(mode<0) return 0;//点在圆内
if(mode==0)//点在圆上
{
v.push_back(P+Normal(u));
return 1;
}
double x=sqrt(dist*dist-C.r*C.r);//解出点到切点的距离
Circle C2(P,x);//作圆
return GetCircleCircleIntersection(C,C2,v);//求交点
}
求圆的公切线
详见代码:
inline int _getTangents
(Circle A,Circle B,Point *a,Point *b)
{
int cnt=0;
if(A.r<B.r) { swap(A,B); swap(a,b); }
double d2=(A.c.x-B.c.x)*(A.c.x-B.c.x)+(A.c.y-B.c.y)*(A.c.y-B.c.y);
double rdiff=A.r-B.r;
double rsum=A.r+B.r;
if(d2<rdiff*rdiff) return 0;//内含
double base=atan2(B.c.y-A.c.y,B.c.x-A.c.x);
if(d2==0&&A.r==B.r) return -1;//无限多条切线
if(d2==rdiff*rdiff)//内切,一条公切线
{
a[cnt]=A.point(base); b[cnt]=B.point(base); cnt++;
return 1;
}
double ang=acos((A.r-B.r)/sqrt(d2));
a[cnt]=A.point(base+ang); b[cnt]=B.point(base+ang); cnt++;
a[cnt]=A.point(base-ang); b[cnt]=B.point(base-ang); cnt++;//两条外公切线
if(d2==rsum*rsum)//两圆相切,一条内公切线
{
a[cnt]=A.point(base);b[cnt]=B.point(Pi+base); cnt++;
}
else if(d2>rsum*rsum)//两条内公切线
{
double ang=acos((A.r+B.r)/sqrt(d2));
a[cnt]=A.point(base+ang); b[cnt]=B.point(Pi+base+ang); cnt++;
a[cnt]=A.point(base-ang); b[cnt]=B.point(Pi+base-ang); cnt++;
}
return cnt;
}
inline int GetTangents
(const Circle &A,const Circle &B,vector<Point> &va,vector<Point> &vb)//接口
{
Point a[4],b[4];
int cnt=_getTangents(A,B,a,b);
for(int i=0;i<cnt;i++) va.push_back(a[i]);
for(int i=0;i<cnt;i++) vb.push_back(b[i]);
return cnt;
}
三角形内接圆
代码:
Circle NeiJieYuan(Point p1,Point p2,Point p3)
{
double a=Length(p2-p3);
double b=Length(p3-p1);
double c=Length(p1-p2);
Point p=(p1*a+p2*b+p3*c)/(a+b+c);
return Circle(p,DistanceToLine(p,p1,p2));
}
三角形外接圆
代码:
Circle WaiJieYuan(Point p1,Point p2,Point p3)
{
double Bx=p2.x-p1.x,By=p2.y-p1.y;
double Cx=p3.x-p1.x,Cy=p3.y-p1.y;
double D=2*(Bx*Cy-By*Cx);
double ansx=(Cy*(Bx*Bx+By*By)-By*(Cx*Cx+Cy*Cy))/D+p1.x;
double ansy=(Bx*(Cx*Cx+Cy*Cy)-Cx*(Bx*Bx+By*By))/D+p1.y;
Point p(ansx,ansy);
return Circle(p,Length(p1-p));
}
算法
点在多边形内判定
凸多边形:直接判断点是否在每一条边左边(假设逆时针储存每一条边)
普通多边形:假想有一条向右的射线,统计多边形穿过这条射线正反多少次,逆时针穿过时绕数加1,反之减1。
代码:
inline int PointInPolygon(const Point &p,Point *poly,int n)
{
int wn=0;
for(int i=0;i<n;i++)
{
if(PointOnSegment(p,poly[i],poly[(i+1)%n])||p==poly[i]) return -1;//在边界上
int k=dcmp(Cross(poly[(i+1)%n]-poly[i],p-poly[i]));
int d1=dcmp(poly[i].y-p.y);
int d2=dcmp(poly[(i+1)%n].y-p.y);
if(k>0&&d1<=0&&d2>0) wn++;
if(k<0&&d2<=0&&d1>0) wn--;
}
if(wn!=0) return 1;
return 0;
}
凸包
顾名思义,就是求一个包含所有给定点的面积最小的凸多边形。
首先把所有点以x坐标为第一关键字、y坐标为第二关键字排序,删除重复的点后得到序列\(p_1,p_2,...,p_n\),然后把\(p_1\)和\(p_2\)放入凸包中。从\(p_3\)开始,当新点在凸包“前进”方向的左边时将其放入凸包,否则依次删除最近放入凸包的点直至新点在左边。
代码:
int ConvexHull(Point *p,int n,Point *ch)
{
sort(p,p+n);
int m=0;
for(int i=0;i<n;i++)
{
while(m>1&&Cross(ch[m-1]-ch[m-2],p[i]-ch[m-2])<=0) m--;
ch[m++]=p[i];
}
int k=m;
for(int i=n-2;i>=0;i--)
{
while(m>k&&Cross(ch[m-1]-ch[m-2],p[i]-ch[m-2])<=0) m--;
ch[m++]=p[i];
}
if(n>1) m--;
return m;
}
凸包直径 - 旋转卡壳
凸包直径:凸包中两点间距离的最大值
先求出凸包然后不断地模拟两条刚好卡住凸包的直线的旋转过程,细节见代码:
double Diameter(Point *p,int n)
{
if(n<=1) return 0;
if(n==2) return Length(p[0]-p[1]);//特判
double res=0;
for(int u=0,v=1;u<n;u++)//u:当前处理地点 v:u对应的边的代表点
{
while(true)
{
int diff=dcmp(Cross(p[(u+1)%n]-p[u],p[(v+1)%n]-p[v]));
if(diff<=0)//到达最远点,距离开始变近
{
res=max(res,Length(p[u]-p[v]));
if(diff==0) res=max(res,Length(p[u]-p[(v+1)%n]));//处理一下细节
break;//处理下一个点
}
v=(v+1)%n;//调整
}
}
return res;
}
半平面交
半平面:一条有向直线的左边的部分
半平面交:所有半平面的公共部分
类似于凸包,用双端队列维护,细节详见代码:
struct Line
{
Point P;
Vector v;
double ang;//极角
Line(Point P=Point(),Vector v=Vector()):P(P),v(v) { ang=atan2(v.y,v.x); }
inline bool operator<(const Line &L) const
{ return ang<L.ang; }
};
inline bool OnLeft(const Line &L,const Point &p)//判断点是否在有向直线的左边
{ return dcmp(Cross(L.v,p-L.P))>0; }
Point GetLineIntersection(const Line &a,const Line &b)
{
Vector u=a.P-b.P;
double t=Cross(b.v,u)/Cross(a.v,b.v);
return a.P+a.v*t;
}
int HalfplaneIntersection(Line *L,int n,Point *poly)
{
sort(L,L+n);//按极角排序
int first,last;//双端队列 [first,last]
Point *p=new Point[n];//p[i]为q[i]和q[i+1]的交点
Line *q=new Line[n];//双端队列
q[first=last=0]=L[0];
for(int i=1;i<n;i++)
{
while(first<last&&!OnLeft(L[i],p[last-1])) last--;//队尾无效
while(first<last&&!OnLeft(L[i],p[first])) first++;//队首无效
q[++last]=L[i];
if(dcmp(Cross(q[last].v,q[last-1].v))==0)
{//两向量平行且相同,取内侧的一个
last--;
if(OnLeft(q[last],L[i].P)) q[last]=L[i];
}
if(first<last) p[last-1]=GetLineIntersection(q[last-1],q[last]);
}
while(first<last&&!OnLeft(q[first],p[last-1])) last--;//删除无用平面
if(last-first<=1) return 0;//空集
p[last]=GetLineIntersection(q[last],q[first]);
int m=0;
for(int i=first;i<=last;i++) poly[m++]=p[i];
return m;
}
参考资料:
- 刘汝佳《算法竞赛入门经典训练指南》
- 网络(太多了,贴不下。。。)
本作品由happyZYM采用知识共享 署名-非商业性使用-相同方式共享 4.0 (CC BY-NC-SA 4.0) 国际许可协议(镜像(简单版)镜像(完整版))进行许可。
转载请注明出处:https://www.cnblogs.com/happyZYM/p/11379697.html (近乎)全文转载而非引用的请在文首添加出处链接。