计算几何学习笔记
一、常量&约定
为了方便地比较浮点数,我们需要设计一个三态比较函数,如下。
#define IL inline
const double pi=acos(-1.0);
const double eps=1e-8;
IL int sign(double x){
if(fabs(x)<eps)return 0;
return (x<0)?-1:1;
}
IL int dcmp(double x,double y){return sign(x-y);}
另外,除非特殊说明,所有涉及旋转/角度的操作,都默认逆时针为正。
二、点&向量
2.1 加、减、数乘
在计算几何中,由于三角函数运算的值不够精确,我们一般采用坐标运算,我们首先需要重载最基本的坐标运算。
struct Point{
double x,y;
Point(double X=0,double Y=0):x(X),y(Y){}
inline void in(){scanf("%lf%lf",&x,&y);}
inline void out(){printf("%.2lf %.2lf\n",x,y);}
Point operator + (const Point &r) const{return Point(x+r.x,y+r.y);}
Point operator - (const Point &r) const{return Point(x-r.x,y-r.y);}
Point operator * (double p) const{return Point(x*p,y*p);}
Point operator / (double p) const{return Point(x/p,y/p);}
bool operator < (const Point &r) const{return x<r.x||(x==r.x&&y<r.y);}
bool operator == (const Point &r) const{return sign(x-r.x)==0&&sign(y-r.y)==0;}
};
typedef Point Vector;
2.2 点积
\(\boldsymbol{a}\cdot \boldsymbol{b}=|\boldsymbol{a}||\boldsymbol{b}|\cos \theta(\theta=\left\langle \boldsymbol{a},\boldsymbol{b}\right \rangle)=x_1 x_2 +y_1 y_2\)
IL double dot(Vector A,Vector B){return A.x*B.x+A.y*B.y;}
利用点积,我们可以比较方便地求出向量的模长,两点间的距离,向量的夹角,以及一个向量在另一个向量上的投影。
IL double length(Vector A){return sqrt(dot(A,A));}
IL double dis(Point A,Point B){return length(A-B);}
IL double angle(Vector A,Vector B){return acos(dot(A,B)/length(A)/length(B));}
IL double project(Point a,Point b,Point c){return dot(b-a,c-a)/length(b-a);}
2.3 叉积
\(\boldsymbol{a}\times \boldsymbol{b}=|\boldsymbol{a}||\boldsymbol{b}|\sin \theta(\theta=\left\langle \boldsymbol{a},\boldsymbol{b}\right \rangle)=x_1 y_2 -x_2 y_1\)
IL double cross(Vector A,Vector B){return A.x*B.y-A.y*B.x;}
叉积的结果可以表示两个向量\(\boldsymbol a\),\(\boldsymbol b\)共起点时,所构成平行四边形的有向面积,这个性质会在之后的许多操作中得以应用。此外,有向面积的正负可以帮助我们判断两个向量共起点以后的位置关系。
具体而言,如果\(\boldsymbol a\)在\(\boldsymbol b\)左侧,那么\(\boldsymbol{a}\times \boldsymbol{b}<0\)
IL double area(Point a,Point b,Point c){return cross(b-a,c-a);}
2.4 极角
利用C++函数库中的\(\mathtt{atan2(y,x)}\)可以帮助我们计算\(\arctan \frac{y}{x}\)的值,其值域为\((-\pi,\pi]\),即当点位于三四象限时其返回一个负值。
IL double polar_angle(Vector A){return atan2(A.y,A.x);}
2.5 旋转
IL Vector rotate(Vector A,double rad){
return Vector(A.x*cos(rad)-A.y*sin(rad),A.x*sin(rad)+A.y*cos(rad));
}
三、直线&线段
3.1 直线的表示&参数
我们常用点向式(直线的参数方程)来表示一个直线:
为了方便,我们提供两种构造直线的方式(传入两点,或者传入一点和一个向量),并根据需要记录\(a,b,\boldsymbol{v},r\)(极角)。
struct Line{
Point a,b;
Vector v;
double r;
Line(){}
Line(const Point &A,const Point &B,int op=0){
if(!op){a=A,b=B;v=b-a;r=polar_angle(v);}
else a=A,v=B;b=a+v;r=polar_angle(v);
}
Point point(double p){return a+v*p;}
};
3.2 点与直线(线段)的位置关系
如果一个点\(p\)满足\(\vec{ap}\times \boldsymbol{v}=0\),我们就可以判定\(p\)在直线上。
如果我们需要判定点在线段上,还需要额外满足\(\vec{pa}\cdot \vec{pb}<0\)
bool line_inc(const Point &p) const{
return sign(cross(p-a,v))==0;
}
bool seg_inc(const Point &p) const{
return sign(cross(a-p,b-p))==0&&sign(dot(a-p,b-p))<=0;
}
3.3 点在直线上的投影点(垂足)
用2.2中提到的方法计算,记得考虑要使用\(\boldsymbol{v}\)对应的单位向量。
IL Point point_line_proj(Point p,Line l){
return l.a+l.v*(dot(l.v,p-l.a)/dot(l.v,l.v));
}
3.4 点到直线(线段)的距离
我们采取面积/底边的方式计算点到直线的距离(高)。
如果要计算点到线段的距离,需要先判断高是否可以落在线段上,如果不在,应该取返回点与最近端点的距离。
IL double point_to_line(Point p,Line l){
return fabs(cross(l.v,p-l.a))/length(l.v);
}
IL double point_to_seg(Point p,Seg s){
if(s.a==s.b)return length(p-s.a);
Vector v1=s.b-s.a,v2=p-s.a,v3=p-s.b;
if(sign(dot(v1,v2))<0)return length(v2);
if(sign(dot(v1,v3))>0)return length(v3);
return point_to_line(p,Line(s.a,s.b));
}
3.5 直线(线段)间的位置关系
我们利用面积比来得到直线与直线的交点。
值得注意的是,为了防止被除数为零,我们需要预先判断两直线是否平行。
IL Point line_line_inter(Line x,Line y){//记得先判平行
Vector U=x.a-y.a;
double t=cross(y.v,U)/cross(x.v,y.v);//注意方向
return x.a+x.v*t;
}
如果两线段相交,则两线段必然相互跨立对方。我们通过跨立试验来判断,两线段是否相交(直线与线段是否相交类似)。我们同样需要预先判断两直线是否平行。
IL bool seg_seg_inter(Seg x,Seg y){
Point a1=x.a,a2=x.b,b1=y.a,b2=y.b;
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 sign(c1)*sign(c2)<=0&&sign(c3)*sign(c4)<=0;
}
IL bool line_seg_inter(Line x,Seg y){
Point a1=x.a,a2=x.b,b1=y.a,b2=y.b;
double c1=cross(a2-a1,b1-a1),c2=cross(a2-a1,b2-a1);
return sign(c1)*sign(c2)<=0;
}
四、圆
4.1 圆的表示&参数
我们可以使用圆心和半径表示一个圆。
struct Circle{
Point o;double r;
Circle(){}
Circle(const Point &O,double R):o(O),r(R){}
}
4.2 点与圆的位置关系
计算点与圆心的距离然后判断即可。
bool inc(const Point &p){
return dcmp(r,dis(o,p))>=0;
}
4.3 求三角形的外接圆
以任意两边中垂线的交点为圆心,交点到一点处的距离为半径作圆。
IL Line get_line(Point a,Point b){return Line((a+b)/2,rotate(b-a,pi/2),1);};
IL Circle get_circle(Point a,Point b,Point c){
Line u=get_line(a,b),v=get_line(a,c);
Point o=line_line_inter(u,v);
return Circle(o,dis(o,a));
}
4.4 最小圆覆盖
如果点\(p\)不在点集\(S\)的最小覆盖圆内,则\(p\)一定在\({S}\cup{p}\) 的最小覆盖圆上。
利用这个定理,我们可以在\(O(n)\)时间复杂度内求出一个点集的最小覆盖圆。
Circle min_circle(Point p[],int n){
random_shuffle(p,p+n);
Circle c=Circle(p[0],0);
for(int i=1;i<n;i++)
if(!c.inc(p[i])){
c=Circle(p[i],0);
for(int j=0;j<i;j++)
if(!c.inc(p[j])){
c=Circle((p[i]+p[j])/2,dis(p[i],p[j])/2);
for(int k=0;k<j;k++)
if(!c.inc(p[k]))
c=get_circle(p[i],p[j],p[k]);
}
}
return c;
}
4.5 伸缩变换
对于椭圆问题,我们可以对整个坐标系进行伸缩变换,再进行计算。
五、普通多边形
5.1 多边形的面积
我们用三角剖分求一个一般多边形的面积。
double poly_area(Point p[],int n){
double res=0;
for(int i=1;i<n-1;i++)
res+=cross(p[i]-p[0],p[i+1]-p[0]);
return res/2;
}
5.2 点与多边形的位置关系
我们采用射线法:考虑以该点为端点引出一条射线,如果这条射线与多边形有奇数个交点,则该点在多边形内部,否则该点在多边形外部,这被称为奇偶规则。
六、凸多边形
6.1 Andrew算法求凸包
我们采用Andrew算法可以\(O(n)\)地计算一个点集的凸包。
int top,st[N];
bool used[N];
void Andrew(Point p[],int cnt){
sort(p+1,p+1+cnt);
for(int i=1;i<=cnt;i++){
while(top>1&&area(p[st[top-1]],p[st[top]],p[i])<=0)used[st[top--]]=0;
st[++top]=i;used[i]=1;
}
used[1]=0;
for(int i=cnt;i;i--){
if(used[i])continue;
while(top>1&&area(p[st[top-1]],p[st[top]],p[i])<=0)top--;
st[++top]=i;
}
}
6.2 点与凸多边形的位置关系
只要一个点在所有边的左侧,点就在凸包内,这可以遍历一遍实现或者用二分实现。
6.3 旋转卡壳
旋转卡壳可以用于求凸包的直径。
double rotating_caliper(Point p[]){
double res=0;
for(int i=1,a=3;i<top;i++){
Point d=p[st[i]],e=p[st[i+1]];
while(dcmp(area(d,e,p[st[a]]),area(d,e,p[st[a+1]]))<0)a=a%(top-1)+1;
res=max(res,max(dis(d,p[st[a]]),dis(e,p[st[a]])));
}
return res;
}
至于求平面最近点对,这不是个计算几何问题,但也放在这里,这个问题除了放在下面的解法外,还有分治的解法,可以去OI-wiki上查看。
struct cmp_y{
bool operator()(const Point &a, const Point &b)const{return a.y<b.y;}
};
multiset<Point,cmp_y>s;
double GetMin(){
double res=1e16;
for(int i=1,l=1;i<=n;i++){
while(l<i&&dcmp(p[i].x-p[l].x,res)>=0)s.erase(s.find(p[l++]));
for(auto it=s.lower_bound(Point(0,p[i].y-res));
it!=s.end()&&dcmp(it->y-p[i].y,res)<0;it++)
if(dcmp(dis(*it,p[i]),res)<0)res=dis(*it,p[i]);
s.insert(p[i]);
}
return res;
}
另外,平面最近最远点对问题有一个随机化的贪心算法:将整个坐标系随机旋转一个角度,再贪心计算。
6.4 半平面交
计算每条直线左侧的平面的交集。
bool cmp(const Line& x,const Line& y){
if(sign(x.r-y.r)==0)return area(x.a,x.b,y.b)<0;
return x.r<y.r;
}
IL bool on_right(Line a,Line b,Line c){
Point o=line_line_inter(b,c);
return sign(area(a.a,a.b,o))<=0;
}
double half_plane_inter(Line l[],int cnt){
sort(l+1,l+1+cnt,cmp);
int L=0,R=-1;
vector<int>q(cnt);
for(int i=1;i<=cnt;i++){
if(i>1&&sign(l[i].r-l[i-1].r)==0)continue;
while(L+1<=R&&on_right(l[i],l[q[R-1]],l[q[R]]))R--;
while(L+1<=R&&on_right(l[i],l[q[L]],l[q[L+1]]))L++;
q[++R]=i;
}
while(L+1<=R&&on_right(l[q[L]],l[q[R-1]],l[q[R]]))R--;
while(L+1<=R&&on_right(l[q[R]],l[q[L]],l[q[L+1]]))L++;
q[++R]=q[L];
vector<Point>ans(R);
int k=0;
for(int i=L;i<R;i++)
ans[++k]=line_line_inter(l[q[i]],l[q[i+1]]);
double res=0;
for(int i=2;i+1<=k;i++)res+=area(ans[1],ans[i],ans[i+1]);
return res/2;
}
七、面积并的计算
7.1 扫描线
计算图形的面积并时,一种常用的思路是运用扫描线——将一维取出排序,然后挨个统计对应的另一维的答案。
7.2 自适应辛普森法
总体思路是根据辛普森公式不断地近似计算函数,直到得到精度合适的值。
辛普森公式:
代码模板如下:
IL double f(double x){
}
IL double simpson(double a,double b){
return ((b-a)*(f(a)+f(b)+4*f((a+b)/2)))/6;
}
double divide(double L,double R,double ans){
double mid=(L+R)/2,l=simpson(L,mid),r=simpson(mid,R);
if(dcmp(l+r,ans)==0)return ans;
return divide(L,mid,l)+divide(mid,R,r);
}
如果本题的\(f (x)\)计算较慢,需要根据题目需要将已经计算过的函数值作为参数传下去,避免重复计算。
如果精度不好保证,还需要将精度不断衰减保证效率。
附 :【模板】计算几何全家桶
#include<bits/stdc++.h>
using namespace std;
#define IL inline
const int N=1e5+5;
const double pi=acos(-1.0);
const double eps=1e-8;
IL int sign(double x){
if(fabs(x)<eps)return 0;
return (x<0)?-1:1;
}
IL int dcmp(double x,double y){return sign(x-y);}
struct Point{
double x,y;
Point(double X=0,double Y=0):x(X),y(Y){}
inline void in(){scanf("%lf%lf",&x,&y);}
inline void out(){printf("%.2lf %.2lf\n",x,y);}
Point operator + (const Point &r) const{return Point(x+r.x,y+r.y);}
Point operator - (const Point &r) const{return Point(x-r.x,y-r.y);}
Point operator * (double p) const{return Point(x*p,y*p);}
Point operator / (double p) const{return Point(x/p,y/p);}
bool operator < (const Point &r) const{return x<r.x||(x==r.x&&y<r.y);}
bool operator == (const Point &r) const{return sign(x-r.x)==0&&sign(y-r.y)==0;}
};
typedef Point Vector;
IL double dot(Vector A,Vector B){return A.x*B.x+A.y*B.y;}
IL double cross(Vector A,Vector B){return A.x*B.y-A.y*B.x;}
IL double length(Vector A){return sqrt(dot(A,A));}
IL double dis(Point A,Point B){return length(A-B);}
IL double angle(Vector A,Vector B){return acos(dot(A,B)/length(A)/length(B));}
IL double project(Point a,Point b,Point c){return dot(b-a,c-a)/length(b-a);}
IL double polar_angle(Vector A){return atan2(A.y,A.x);}
IL double area(Point a,Point b,Point c){return cross(b-a,c-a);}
IL Vector rotate(Vector A,double rad){
return Vector(A.x*cos(rad)-A.y*sin(rad),A.x*sin(rad)+A.y*cos(rad));
}
struct Line{
Point a,b;
Vector v;
double r;
Line(){}
Line(const Point &A,const Point &B,int op=0){
if(!op){a=A,b=B;v=b-a;r=polar_angle(v);}
else a=A,v=B;b=a+v;r=polar_angle(v);
}
Point point(double p){return a+v*p;}
bool line_inc(const Point &p) const{return sign(cross(p-a,v))==0;}
bool seg_inc(const Point &p) const{
return sign(cross(a-p,b-p))==0&&sign(dot(a-p,b-p))<=0;
}
};
typedef Line Seg;
IL Point point_line_proj(Point p,Line l){
return l.a+l.v*(dot(l.v,p-l.a)/dot(l.v,l.v));
}
IL double point_to_line(Point p,Line l){
return fabs(cross(l.v,p-l.a))/length(l.v);
}
IL double point_to_seg(Point p,Seg s){
if(s.a==s.b)return length(p-s.a);
Vector v1=s.b-s.a,v2=p-s.a,v3=p-s.b;
if(sign(dot(v1,v2))<0)return length(v2);
if(sign(dot(v1,v3))>0)return length(v3);
return point_to_line(p,Line(s.a,s.b));
}
IL Point line_line_inter(Line x,Line y){//记得先判平行
Vector U=x.a-y.a;
double t=cross(y.v,U)/cross(x.v,y.v);//注意方向
return x.a+x.v*t;
}
IL bool seg_seg_inter(Seg x,Seg y){
Point a1=x.a,a2=x.b,b1=y.a,b2=y.b;
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 sign(c1)*sign(c2)<=0&&sign(c3)*sign(c4)<=0;
}
IL bool line_seg_inter(Line x,Seg y){
Point a1=x.a,a2=x.b,b1=y.a,b2=y.b;
double c1=cross(a2-a1,b1-a1),c2=cross(a2-a1,b2-a1);
return sign(c1)*sign(c2)<=0;
}
struct Circle{
Point o;double r;
Circle(){}
Circle(const Point &O,double R):o(O),r(R){}
bool inc(const Point &p){
return dcmp(r,dis(o,p))>=0;
}
};
IL Line get_line(Point a,Point b){return Line((a+b)/2,rotate(b-a,pi/2),1);};
IL Circle get_circle(Point a,Point b,Point c){
Line u=get_line(a,b),v=get_line(a,c);
Point o=line_line_inter(u,v);
return Circle(o,dis(o,a));
}
Circle min_circle(Point p[],int n){
random_shuffle(p,p+n);
Circle c=Circle(p[0],0);
for(int i=1;i<n;i++)
if(!c.inc(p[i])){
c=Circle(p[i],0);
for(int j=0;j<i;j++)
if(!c.inc(p[j])){
c=Circle((p[i]+p[j])/2,dis(p[i],p[j])/2);
for(int k=0;k<j;k++)
if(!c.inc(p[k]))
c=get_circle(p[i],p[j],p[k]);
}
}
return c;
}
double poly_area(Point p[],int n){
double res=0;
for(int i=1;i<n-1;i++)
res+=cross(p[i]-p[0],p[i+1]-p[0]);
return res/2;
}
int top,st[N];
void Andrew(Point p[],int cnt){
vector<bool>used(cnt);
sort(p+1,p+1+cnt);
for(int i=1;i<=cnt;i++){
while(top>1&&area(p[st[top-1]],p[st[top]],p[i])<=0)used[st[top--]]=0;
st[++top]=i;used[i]=1;
}
used[1]=0;
for(int i=cnt;i;i--){
if(used[i])continue;
while(top>1&&area(p[st[top-1]],p[st[top]],p[i])<=0)top--;
st[++top]=i;
}
}
double rotating_caliper(Point p[]){
double res=0;
for(int i=1,a=3;i<top;i++){
Point d=p[st[i]],e=p[st[i+1]];
while(dcmp(area(d,e,p[st[a]]),area(d,e,p[st[a+1]]))<0)a=a%(top-1)+1;
res=max(res,max(dis(d,p[st[a]]),dis(e,p[st[a]])));
}
return res;
}
bool cmp(const Line& x,const Line& y){
if(sign(x.r-y.r)==0)return area(x.a,x.b,y.b)<0;
return x.r<y.r;
}
IL bool on_right(Line a,Line b,Line c){
Point o=line_line_inter(b,c);
return sign(area(a.a,a.b,o))<=0;
}
double half_plane_inter(Line l[],int cnt){
sort(l+1,l+1+cnt,cmp);
int L=0,R=-1;
vector<int>q(cnt);
for(int i=1;i<=cnt;i++){
if(i>1&&sign(l[i].r-l[i-1].r)==0)continue;
while(L+1<=R&&on_right(l[i],l[q[R-1]],l[q[R]]))R--;
while(L+1<=R&&on_right(l[i],l[q[L]],l[q[L+1]]))L++;
q[++R]=i;
}
while(L+1<=R&&on_right(l[q[L]],l[q[R-1]],l[q[R]]))R--;
while(L+1<=R&&on_right(l[q[R]],l[q[L]],l[q[L+1]]))L++;
q[++R]=q[L];
vector<Point>ans(R);
int k=0;
for(int i=L;i<R;i++)
ans[++k]=line_line_inter(l[q[i]],l[q[i+1]]);
double res=0;
for(int i=2;i+1<=k;i++)res+=area(ans[1],ans[i],ans[i+1]);
return res/2;
}