本文基于fxj巨佬的计算几何全家桶,并基于原文进行了自己的一些整理了经验补充,阅读本文前请前往支持巨佬fxj。
直线与线段的表示采用两点+方向向量的表示方法。
此外,一般来说,直线采用两点或一点+方向向量表示,而线段必须用两点表示。
一般来说,方向向量用单位向量比较简单,因此下面的直线的方向向量默认为单位向量。
在以下算法中,如无特殊说明或者题目要求,不强调针对的是直线还是线段,毕竟算法原理是相通的。
struct line
{
V d,a,b;
};//存储
inline line trans(double a,double b,double c,double d)
{
V dd(c-a,b-d),x(a,b),y(c,d);
dd=dd/len(dd);
return (line){dd,x,y};
}//将两点转化为一条线(直线或线段)
inline line trans(const V &a,const V &b)
{
V dd(a-n);
dd=dd/len(dd);
return (line){dd,a,b};
}//也是转化函数,存在的意义是用起来更方便(不必专门把点存为某一种格式后在转化)
inline void input(const line &l)
{
double a=read(),b=read(),c=read(),d=read();
l=trans(a,b,c,d);
return;
}//输入函数,模块化,用着方便
把线上的代表点移动投影的距离。
这里的投影是带符号的,符号体现移动的方向。
对于这部分内容,当你不能理解一些问题时,画图和特殊值带入能让你更好地理解它们。
inline V cui(const line &l,const V &o)
{
return l.a+((o-l.a)*l.d)*l.d;
}
求出垂足后用中点公式
inline V mid(const line &l,const V &o)
{
V u=cui(l,o);
return (V){2*u.x-o.x,2*u.y-o.y};
}
共线可以通过叉积等于0来判断。
不共线时,可以通过叉积的正负判断是在直线的哪一侧。
共线时,如果是线段或射线,还可以利用点积的正负判断与端点的位置关系。(点在线段的延长线还是反向延长线上)
(对于线段而言,可以直接通过模长判断是否在线段上)
inline bool on_line(const line &l,const V &o)
{
return abs(l.d^(o-l.a))<eps;
}//共线
inline bool on_seg(const line &l,const V &o)
{
return abs(len(o-l.a)+len(o-l.a)-len(l.a-l.b))<eps;
}//在线段上
inline int pos(const line &l,const V &o)
{
if(!on_line(l,o))
{
if((o-l.a)^l.d)>0) return 1;
else return 2;
}//不共线
else
{
if(((o-l.a)*(o-l.b))<=0) return 5;
else if(((o-l.a)*l.d)<0) return 3;
else return 4;
}
}//1,2,5状态常用,3,4只在特定题目中使用
直线:叉积等面积法(叉积求出平行四边形面积 s s s, d i s = s / 底 面 边 长 dis=s/底面边长 dis=s/底面边长。
线段时,若垂足在线段上,相当于点到直线的距离;否则是到两端点距离的最小值。
垂足是否在线段上可以利用是否有钝角来判断。
inline bool dun(const V &a,const V &o,const V &b)
{
V x=b-a,y=o-a;
return (x*y)<-eps;
}//之前出现过的判断钝角函数
inline double dis(const line &l,const V &o,int op=0) //op=0:dis on line,op=1:dis on segment
{
if(op && (dun(l.a,o,l.b) || dun(l.b,o,l.a))) return min(len(o-l.a),len(o-l.b));
else return abs((o-l.a)^(o-l.b)) / len(l.a-l.b);
}
共线:
方向向量叉积为0则共线。
有时需判断平行还是重合,只需取一个直线的 l . a l.a l.a,求它到另一个直线的距离即可。
若距离为 0 0 0则重合,否则平行。
inline bool gongxian(const line &a,const line &b){return abs(a.d^b.d)<eps;}
inline bool chonghe(const line &a,const line &b){return abs(dis(a,b.a,0))<eps;}
垂直:
方向向量点积为 0 0 0则垂直。
inline bool chuizhi(const line &a,const line &b){return abs(a.d*b.d)<eps;}
有两个步骤:
实现上,快速排斥实验可以用坐标 min、max 的比较轻松解决,跨立实验则可以用前面讲的叉积求点与线的关系来实现,注意叉积为0(端点在线段上)是符合条件的。
一开始会感觉跨立实验足以判断线段与线段是否相交,但是事实上跨立实验在判断时认为叉积为 0 0 0是合法的,且本质上跨立实验相当于判断两点与直线的位置关系,因此就会出现如下图的问题。
而多出的两种情况:即共线且不相交,和两端点位于直线两侧,但与线段没有交点.
这两种情况都会被快速排斥实验ban掉,因此算法的正确性是有保证的。
ps:因此fxj在原文中所述的“快速排斥实验可以用特判共线替代”是不正确的(逃
更新:据fxj描述,最后一种情况不会被快速排斥实验ban掉,故必须尽量两次跨立,如函数最后一步。
因而”快速排斥实验可以被特判共线替代“是正确的,但是由于特判共线的实现难度大于快速排斥实验,
因此巨佬fxj推荐使用"快速排斥实验+两次跨立实验"的做法,即如下的函数中的内容。
inline bool jiao(const line &u,const line &v)
{
if(min(u.a.x,u.b.x)>max(v.a.x,v.b.x)+eps||max(u.a.x,u.b.x)<min(v.a.x,v.b.x)-eps||
min(u.a.y,u.b.y)>max(v.a.y,v.b.y)+eps||max(u.a.y,u.b.y)<min(v.a.y,v.b.y)-eps) return false;//rapid rejection test
return ((u.a-v.a)^v.d)*((u.b-v.a)^v.d)<eps&&((v.a-u.a)^u.d)*((v.b-u.a)^u.d)<eps;//straddle test
}
首先应当保证两直线有交点:保证不共线(特判是否共线)
设两直线为 l 1 l_1 l1, l 2 l_2 l2,两直线上各有一个点 x 1 x_1 x1, x 2 x_2 x2,方向向量分别为 d 1 d_1 d1, d 2 d_2 d2.
设交点 p p p = = = x 1 + k d 1 x_1+kd_1 x1+kd1,则有 ( p − x 2 ) × d 2 = 0 (p-x_2) \times d2=0 (p−x2)×d2=0.
根据叉积的分配律,可得 k = ( x 2 − x 1 ) × d 2 d 1 × d 2 k=\frac{(x_2-x_1) \times d_2}{d_1 \times d_2} k=d1×d2(x2−x1)×d2
再把求出的k带回原式即可。
inline V jiaodian(const line &u,const line &v)
{
double k=((u.a-v.a)^v.d)/(u.d^v.d);
return u.a+(k*u.d);
}
已知三点 A A A, B B B, C C C,求 ∠ B A C \angle BAC ∠BAC的角平分线
求 A B AB AB, A C AC AC的方向向量 d 1 d_1 d1, d 2 d_2 d2,角平分线的方向向量 d d d = = = d 1 + d 2 d_1+d_2 d1+d2,再放入点 A A A和点 A + d A+d A+d即可。
inline line pingfen(const V &a,const V &b,const V &c)
{
V d1=(b-a)/len(b-a),d2=(c-a)/len(c-a),d=(d1+d2)/len(d1+d2);
return (line){d,a,a+d};
}
找到中点和与原线段垂直的方向向量即可。
inline line zhongcui(const V &a,const V &b)
{
V x=min(a,b),d=danwei(cui(b-a));
return (line){d,x,x+d};
}