注:定理来自这篇博客,本文注重证明
向量基本运算
加法
向量
a
⃗
=
(
x
1
,
y
1
)
,
b
⃗
=
(
x
2
,
y
2
)
\vec{a}=\left(x_1,y_1\right),\vec{b}=\left(x_2,y_2\right)
a=(x1,y1),b=(x2,y2) 则
a
⃗
+
b
⃗
=
(
x
1
+
x
2
,
y
1
+
y
2
)
\vec{a}+\vec{b}=(x_1+x_2,y_1+y_2)
a+b=(x1+x2,y1+y2)
向量在加法中可以理解为一种移动效果,所以向量加法可以理解为下图:
叉积
叉积的计算
向量
a
⃗
=
(
x
1
,
y
1
)
,
b
⃗
=
(
x
2
,
y
2
)
\vec{a}=\left(x_1,y_1\right),\vec{b}=\left(x_2,y_2\right)
a=(x1,y1),b=(x2,y2) 则
a
⃗
×
b
⃗
=
x
1
y
2
−
x
2
y
1
\vec{a}\times \vec{b}=x_1y_2-x_2y_1
a×b=x1y2−x2y1,证明如下:
首先,根据叉积的定义:
a
⃗
×
b
⃗
=
∣
a
⃗
∣
⋅
∣
b
⃗
∣
⋅
sin
(
θ
)
\vec{a}\times \vec{b}=\left|\vec{a}\right|\cdot|\vec{b}|\cdot \sin\left(\theta\right)
a×b=∣a∣⋅∣b∣⋅sin(θ)(
∣
a
⃗
∣
\left|\vec{a}\right|
∣a∣表示
a
⃗
\vec{a}
a 的模长,也就是长度,
θ
\theta
θ 表示从
a
⃗
\vec{a}
a 旋转到
b
⃗
\vec{b}
b 经过的角度,顺时针为正,逆时针为负)
定义
a
r
c
t
a
n
2
(
x
,
y
)
arctan2(x,y)
arctan2(x,y) 表示从
(
1
,
0
)
(1,0)
(1,0) 顺时针旋转至
(
x
,
y
)
(x,y)
(x,y) 经过的角度(返回弧度,取值为
0
∼
2
π
0\sim 2\pi
0∼2π)
则原式可以表示为
x
1
2
+
y
1
2
⋅
x
2
2
+
y
2
2
⋅
sin
(
a
r
c
t
a
n
2
(
x
2
,
y
2
)
−
a
r
c
t
a
n
2
(
x
1
,
y
1
)
)
\sqrt{x_1^2+y_1^2}\cdot\sqrt{x_2^2+y_2^2}\cdot\sin\left(arctan2\left(x_2,y_2\right)-arctan2(x_1,y_1)\right)
x12+y12⋅x22+y22⋅sin(arctan2(x2,y2)−arctan2(x1,y1))
根据
sin
(
a
−
b
)
=
sin
(
a
)
cos
(
b
)
−
cos
(
a
)
sin
(
b
)
\sin(a-b)=\sin(a)\cos(b)-\cos(a)\sin(b)
sin(a−b)=sin(a)cos(b)−cos(a)sin(b) 可得:
原式
=
x
1
2
+
y
1
2
⋅
x
2
2
+
y
2
2
⋅
(
sin
(
a
r
c
t
a
n
2
(
x
2
,
y
2
)
)
cos
(
a
r
c
t
a
n
2
(
x
1
,
y
1
)
)
−
cos
(
a
r
c
t
a
n
2
(
x
2
,
y
2
)
)
sin
(
a
r
c
t
a
n
2
(
x
1
,
y
1
)
)
)
原式=\sqrt{x_1^2+y_1^2}\cdot\sqrt{x_2^2+y_2^2}\cdot\left(\sin(arctan2(x_2,y_2))\cos(arctan2(x_1,y_1))-\cos(arctan2(x_2,y_2))\sin(arctan2(x_1,y_1))\right)
原式=x12+y12⋅x22+y22⋅(sin(arctan2(x2,y2))cos(arctan2(x1,y1))−cos(arctan2(x2,y2))sin(arctan2(x1,y1)))
由于每个表示为
(
x
,
y
)
(x,y)
(x,y) 的向量放到平面直角坐标系上后实际上为:
通过
a
r
c
t
a
n
2
(
x
2
,
y
2
)
arctan2(x_2,y_2)
arctan2(x2,y2) 我们得到了
∠
B
A
D
\angle{BAD}
∠BAD 所以
sin
(
a
r
c
t
a
n
2
(
x
2
,
y
2
)
)
=
sin
(
∠
B
A
D
)
=
B
D
A
B
=
y
x
2
+
y
2
,
cos
(
a
r
c
t
a
n
2
(
x
2
,
y
2
)
)
=
cos
(
∠
B
A
D
)
=
A
D
A
B
=
x
x
2
+
y
2
\sin(arctan2(x_2,y_2))=\sin(\angle{BAD})=\frac{BD}{AB}=\frac{y}{\sqrt{x^2+y^2}},\cos(arctan2(x_2,y_2))=\cos(\angle{BAD})=\frac{AD}{AB}=\frac{x}{\sqrt{x^2+y^2}}
sin(arctan2(x2,y2))=sin(∠BAD)=ABBD=x2+y2y,cos(arctan2(x2,y2))=cos(∠BAD)=ABAD=x2+y2x
所以:
原式
=
x
1
2
+
y
1
2
⋅
x
2
2
+
y
2
2
⋅
(
y
2
x
2
2
+
y
2
2
⋅
x
1
x
1
2
+
y
1
2
−
x
2
x
2
2
+
y
2
2
⋅
y
1
x
1
2
+
y
1
2
)
=
x
1
2
+
y
1
2
⋅
x
2
2
+
y
2
2
⋅
x
1
y
2
−
x
2
y
1
x
1
2
+
y
1
2
⋅
x
2
2
+
y
2
2
=
x
1
y
2
−
x
2
y
1
\begin{aligned}原式&=\sqrt{x_1^2+y_1^2}\cdot\sqrt{x_2^2+y_2^2}\cdot\left(\frac{y_2}{\sqrt{x_2^2+y_2^2}}\cdot\frac{x_1}{\sqrt{x_1^2+y_1^2}}-\frac{x_2}{\sqrt{x_2^2+y_2^2}}\cdot\frac{y_1}{\sqrt{x_1^2+y_1^2}}\right) \\ &=\sqrt{x_1^2+y_1^2}\cdot\sqrt{x_2^2+y_2^2}\cdot\frac{x_1y_2-x_2y_1}{\sqrt{x_1^2+y_1^2}\cdot\sqrt{x_2^2+y_2^2}} \\ &=x_1y_2-x_2y_1 \end{aligned}
原式=x12+y12⋅x22+y22⋅(x22+y22y2⋅x12+y12x1−x22+y22x2⋅x12+y12y1)=x12+y12⋅x22+y22⋅x12+y12⋅x22+y22x1y2−x2y1=x1y2−x2y1
叉积的性质
画出
sin
x
\sin x
sinx 的曲线:
其正负性在
x
=
π
,
2
π
,
3
π
,
.
.
.
.
,
n
π
x=\pi,2\pi,3\pi,....,n\pi
x=π,2π,3π,....,nπ 位置发生改变,这里的
x
x
x 使用了弧度表示,相当于角度每增加或减少
180
°
180\degree
180° 发生一次改变。所以
a
⃗
×
b
⃗
\vec{a}\times\vec{b}
a×b 的值当从
a
⃗
\vec{a}
a 逆时针旋转至
b
⃗
\vec{b}
b 经过的角度小于
180
°
180\degree
180° 时为正,否则为负(相当于朝向
a
⃗
\vec{a}
a 方向时
b
⃗
\vec{b}
b 若在左侧则为正,否则为负,这个性质可以用来判断左右转),并且若角度为
0
°
0\degree
0° 或
180
°
180\degree
180° 时为
0
0
0(这个性质可以用来判断共线)。
点积
向量 a ⃗ = ( x 1 , y 1 ) , b ⃗ = ( x 2 , y 2 ) \vec{a}=\left(x_1,y_1\right),\vec{b}=\left(x_2,y_2\right) a=(x1,y1),b=(x2,y2) 则 a ⃗ ⋅ b ⃗ = x 1 x 2 + y 1 y 2 \vec{a}\cdot \vec{b}=x_1x_2+y_1y_2 a⋅b=x1x2+y1y2,其证明方法与叉积大致相同,读者感兴趣的话可以自行证明,提示:
- 点积的定义: a ⃗ ⋅ b ⃗ = ∣ a ⃗ ∣ ⋅ ∣ b ⃗ ∣ ⋅ cos ( θ ) \vec{a}\cdot \vec{b}=\left|\vec{a}\right|\cdot|\vec{b}|\cdot \cos\left(\theta\right) a⋅b=∣a∣⋅∣b∣⋅cos(θ)
- cos ( a − b ) = sin ( a ) sin ( b ) + cos ( a ) cos ( b ) \cos(a-b)=\sin(a)\sin(b)+\cos(a)\cos(b) cos(a−b)=sin(a)sin(b)+cos(a)cos(b)
另外,点积的性质为当 − 90 < θ < 90 -90<\theta<90 −90<θ<90 时值为负,当 90 < θ < 270 90<\theta<270 90<θ<270 或 − 270 < θ < − 90 -270<\theta<-90 −270<θ<−90 时值为正(判断有无掉头),当 x = 90 , − 90 , 270 , − 270 x=90,-90,270,-270 x=90,−90,270,−270 时值为 0 0 0 (判断垂直)。证明方法也类似于叉积,提示:
-
cos
x
\cos x
cosx 的曲线:
点与线
相交
判断线段是否相交
首先,感性理解一下,如果有两个线段相交,那么任意一条线段的两个端点一定在另一条线段的两侧,反之也成立(如下图)。
所以若将一条线段变成一个向量(一个端点指向另一端点),从终点向另外一条线段的两个端点走,必定一个向左拐,一个向右拐,所以叉积的正负性必定不同(如下图
C
D
→
,
D
A
→
,
D
B
→
\overrightarrow{CD},\overrightarrow{DA},\overrightarrow{DB}
CD,DA,DB,
C
D
→
×
D
A
→
\overrightarrow{CD}\times\overrightarrow{DA}
CD×DA 和
C
D
→
×
D
B
→
\overrightarrow{CD}\times\overrightarrow{DB}
CD×DB 的正负性不同):
当然,也会出现特殊情况,就是两条线段重合:
此时,两线段相交,但叉积都为0(两向量方向相同或相反时叉积为0),但如果直接特判,依然会出问题,两线段共线:
上图中线段CD与线段 EF共线,会出现错误的判断。所以,还需进一步完善判断条件——加入快速排斥实验。
快速排斥实验,其目的是排除两线段共线但不相交的情况,即判断两个分别包含两线段的矩形是否有公共部分(如下图情况没有),只有出现了公共部分,两线段才可能相交。
这样下来,我们便解决了特殊情况,下面是代码:
#include<bits/stdc++.h>
namespace geometry
{
using namespace std;
struct Vector{//向量
double x,y;
};
struct Point{//点
double x,y;
};
struct Segment{//线段
Point a,b;
};
struct Line{//直线
Point a;
Vector k;
};
double chk(double n)//判断n的正负性,n>0时返回1,n<0时返回-1,n=0时返回0
{
return (n>0?1:(n==0?0:-1));
}
double distance(Point a,Point b)//点之间的距离
{
return sqrt((b.x-a.x)*(b.x-a.x)+(b.y-a.y)*(b.y-a.y));
}
double operator *(Vector q,Vector h){//向量点乘
return q.x*h.x+q.y*h.y;
}
Vector operator *(Vector q,double h){//向量扩大
return (Vector){q.x*h,q.y*h};
}
Vector operator *(double q,Vector h){//向量扩大
return (Vector){q*h.x,q*h.y};
}
Vector operator +(Vector q,Vector h){//向量加法
return (Vector){q.x+h.x,q.y+h.y};
}
Point operator +(Point q,Vector h){//点加向量,实现位移
return (Point){q.x+h.x,q.y+h.y};
}
Point operator -(Point q,Vector h){//点减向量,实现位移
return (Point){q.x-h.x,q.y-h.y};
}
Vector operator -(Point q,Point h){//q-h,得到q->h的向量
return (Vector){h.x-q.x,h.y-q.y};
}
double operator ^(Vector q,Vector h){//向量叉乘
return q.x*h.y-q.y*h.x;
}
bool turn_left(Vector q,Vector h)//左转叉积大于0
{
return (q^h)>0;
}
bool turn_right(Vector q,Vector h)//右转叉积小于0
{
return (q^h)<0;
}
bool turn_back(Vector q,Vector h)//掉头点积小于0
{
return (q*h)<0;
}
bool vertical(Vector q,Vector h)//垂直点积等于0
{
return (q*h)==0;
}
bool segment_cross_check(Segment a,Segment b)//线段相交检测
{
if(min(a.a.x,a.b.x)>max(b.a.x,b.b.x)||min(a.a.y,a.b.y)>max(b.a.y,b.b.y)||min(b.a.x,b.b.x)>max(a.a.x,a.b.x)||min(b.a.y,b.b.y)>max(a.a.y,a.b.y))
return false;//快速排斥实验
double ans1=chk((a.a-a.b)^(b.a-a.a)),ans2=chk((a.a-a.b)^(b.b-a.a)),ans3=chk((b.a-b.b)^(a.a-b.a)),ans4=chk((b.a-b.b)^(a.b-b.a));
return ans1!=ans2&&ans3!=ans4||ans1==0;//跨立实验
}
}
using namespace std;
using namespace geometry;
int main()
{
Segment a,b;
cin>>a.a.x>>a.a.y>>a.b.x>>a.b.y>>b.a.x>>b.a.y>>b.b.x>>b.b.y;
cout<<segment_cross_check(a,b);
return 0;
}
直线相交
直线的表示
首先,介绍一下直线在计算几何中的常用表示方法:使用一个点和一个向量进行表示。
相信大家都学过一次函数,一次函数的格式为
y
=
k
x
+
b
(
k
≠
0
)
y=kx+b(k\ne 0)
y=kx+b(k=0),与一次函数的格式类似,这种方法的格式为
(
b
x
+
x
k
x
,
b
y
+
x
k
y
)
(b_x+xk_x,b_y+xk_y)
(bx+xkx,by+xky)(具体含义稍后解释)但
k
,
b
k,b
k,b 与一次函数中完全不同。
对于两个点
a
,
b
a,b
a,b 我们可以确定一条直线:
在
y
=
k
x
+
b
(
k
≠
0
)
y=kx+b(k\ne 0)
y=kx+b(k=0) 中,
k
k
k 是直线的斜率,也就是
B
y
−
A
y
B
x
−
A
x
\frac{B_y-A_y}{B_x-A_x}
Bx−AxBy−Ay 。而在
(
b
x
+
x
k
x
,
b
y
+
x
k
y
)
(b_x+xk_x,b_y+xk_y)
(bx+xkx,by+xky) 中,
k
k
k 是一个向量,从
A
A
A 指向
B
B
B 或从
B
B
B 指向
A
A
A,
x
x
x 为一个实数,用来控制向量的长度和方向,表示
x
x
x 和
y
y
y 方向的位移。
如此,我们便可以表示出该直线上的所有点。
为什么要这样表示一条线段?其实理由有很多,而其中我认为最重要的是:使用向量表示的直线,无需特殊考虑x方向差为0的情况(除0),当然,它也让许多计算更加简便。
求交点
如何求出上图两条直线的交点?读者可以先自行思考,再往下翻。(本文只介绍几何做法,当然,也存在代数做法,感兴趣的话可以尝试自己寻找答案)
首先,过我们过
A
,
B
A,B
A,B 两点做平行于
C
D
CD
CD 的直线:
容易发现,图中可以构造一对相似三角形(下图的
△
A
B
H
,
△
I
A
G
\bigtriangleup ABH,\bigtriangleup IAG
△ABH,△IAG),又因为
△
A
B
H
\bigtriangleup ABH
△ABH 的斜边实际上就是向量
A
B
→
\overrightarrow{AB}
AB,如果我们可以得到两三角形任意一边的比例,就可以得到向量
A
I
→
\overrightarrow{AI}
AI。那么如何得到边的比例呢?
在过
A
A
A 的平行线上截取一段长度等于
C
D
→
\overrightarrow{CD}
CD 模长的线段,分别向上和向下构造两个平行四边形。
又
∵
S
上
=
∣
C
D
→
∣
⋅
B
H
,
S
下
=
∣
C
D
→
∣
⋅
A
G
又\because S_上=|\overrightarrow{CD}|\cdot BH,S_下=|\overrightarrow{CD}|\cdot AG
又∵S上=∣CD∣⋅BH,S下=∣CD∣⋅AG
∴
S
上
:
S
下
=
B
H
:
A
G
\therefore S_上:S_下=BH:AG
∴S上:S下=BH:AG
我们可以通过向量叉积求出两平行四边形的面积,之后便可得出比例求出交点。
现在,考虑符号。我们发现,对于一条同直线,可以表示它向量有两个方向,
k
k
k 与
−
k
-k
−k。下图是上图的另几个情况:
经过计算可以发现,不管是哪一种情况,
C
D
→
×
C
A
→
\overrightarrow{CD}\times\overrightarrow{CA}
CD×CA 与
C
D
→
×
A
B
→
\overrightarrow{CD}\times\overrightarrow{AB}
CD×AB 如果符号相同,则
I
=
A
−
A
B
→
⋅
A
G
B
H
I=A-\overrightarrow{AB}\cdot\frac{AG}{BH}
I=A−AB⋅BHAG 否则
I
=
A
+
A
B
→
⋅
A
G
B
H
I=A+\overrightarrow{AB}\cdot\frac{AG}{BH}
I=A+AB⋅BHAG,代码如下(前面已经给出的内容不再重复出现):
bool line_cross_check(Line a,Line b)
{
return (a.k^b.k)!=0;//在同一平面内两直线不平行才相交
}
bool point_on_line(Point a,Line b)
{
return ((b.a-a)^b.k)==0;//三点共线
}
Point line_cross_get_cross_point(Line a,Line b)
{
Vector c=b.a-a.a;
return a.a-(b.k^c)/(b.k^a.k)*a.k;//得到带符号的比例
}
这个方法不仅可以求出直线交点,加上一些判断,它也可以求出线段交点。
距离
点到直线的距离
实际上这个问题十分简单,我们已经知道了叉积的几何意义是求出两向量所确定的平行四边形的带符号面积,那么可以构造出下图所示的平行四边形:
利用叉积求出它的面积,再除以
∣
K
L
→
∣
|\overrightarrow{KL}|
∣KL∣,可以得出
K
L
KL
KL 边上的高,也就是
J
J
J 到直线
K
L
KL
KL 的距离。
代码如下:
double distance_point_to_line(Point a,Line b)
{
Vector c=b.a-a;
return fabs((c^b.k)/distance(b.a,b.a+b.k));
}
点到线段的距离
与直线不同,线段是有长度限制的。所以一个点到一条线段的距离有两种情况:
首先,在线段两端点分别作两条垂直于该线段的直线,将平面分为绿色,红色和紫色三个部分。如果点出现在红色区域,距离相当于直接做垂线,计算方法同计算点到直线距离。
其实出现在绿色和紫色区域是等价的,其距离都是到线段两端点之一的距离(取最小值)。
那么如何判断出现再了哪一个区域?这时候,就要用到点积了!
根据点积的性质,
A
B
→
⋅
B
C
→
≥
0
\overrightarrow{AB}\cdot\overrightarrow{BC}\ge0
AB⋅BC≥0 说明从
A
B
→
\overrightarrow{AB}
AB 方向走到
B
C
→
\overrightarrow{BC}
BC 方向没有掉头,也就是点
C
C
C 位于紫色区域,否则位于红色区域,判断是否位于绿色区域也是同理(但
A
B
→
\overrightarrow{AB}
AB 方向需要改成
B
A
→
\overrightarrow{BA}
BA 方向)。那么下面就是代码:
double distance_point_to_segment(Point a,Segment b)
{
if(chk((b.a-b.b)*(b.b-a))==chk((b.b-b.a)*(b.a-a)))//判断是否在红色区域中
return distance_point_to_line(a,(Line){b.a,(b.b-b.a)});//在红色区域中,返回垂线段长度
else
return min(distance(a,b.a),distance(a,b.b));//不在红色区域中,返回到线段两端点的最近距离
}
垂足
点在直线上的垂足
我们需要求出一个垂直于向量
Q
R
→
\overrightarrow{QR}
QR 的向量
S
T
→
\overrightarrow{ST}
ST(
T
T
T 为直线
Q
R
QR
QR 上一点),根据点积的性质可以列出方程
Q
R
→
⋅
S
T
→
=
0
\overrightarrow{QR}\cdot\overrightarrow{ST}=0
QR⋅ST=0 。
怎么解出一个向量?比较困难。我们可以尝试将
S
T
→
\overrightarrow{ST}
ST 换一种形式表示。
定义点
a
(
x
1
,
y
1
)
a(x_1,y_1)
a(x1,y1) 减去点
b
(
x
2
,
y
2
)
b(x_2,y_2)
b(x2,y2) 的结果为向量
c
⃗
=
(
x
2
−
x
1
,
y
2
−
y
1
)
\vec{c}=(x_2-x_1,y_2-y_1)
c=(x2−x1,y2−y1) (从
a
a
a 连接到
b
b
b 的向量)。
所以
S
T
→
=
(
Q
−
S
)
→
+
Q
T
→
=
(
Q
−
S
)
→
+
x
⋅
Q
R
→
\overrightarrow{ST}=\overrightarrow{(Q-S)}+\overrightarrow{QT}=\overrightarrow{(Q-S)}+x\cdot\overrightarrow{QR}
ST=(Q−S)+QT=(Q−S)+x⋅QR (
x
x
x 为一实数,在此处扩大或缩小向量
Q
R
→
\overrightarrow{QR}
QR)。
现在,需要使用点积的分配律继续下一步推导。先证明一下:
a
⃗
=
(
x
1
,
y
1
)
,
b
⃗
=
(
x
2
,
y
2
)
,
c
⃗
=
(
x
3
,
y
3
)
\vec{a}=(x_1,y_1),\vec{b}=(x_2,y_2),\vec{c}=(x_3,y_3)
a=(x1,y1),b=(x2,y2),c=(x3,y3)
a
⃗
⋅
(
b
⃗
+
c
⃗
)
=
a
⃗
⋅
(
x
2
+
x
3
,
y
2
+
y
3
)
→
=
x
1
(
x
2
+
x
3
)
+
y
1
(
y
2
+
y
3
)
\vec{a}\cdot\left(\vec{b}+\vec{c}\right)=\vec{a}\cdot\overrightarrow{(x_2+x_3,y_2+y_3)}=x_1(x_2+x_3)+y_1(y_2+y_3)
a⋅(b+c)=a⋅(x2+x3,y2+y3)=x1(x2+x3)+y1(y2+y3)
a
⃗
⋅
b
⃗
+
a
⃗
⋅
c
⃗
=
x
1
x
2
+
y
1
y
2
+
x
1
x
3
+
y
1
y
3
=
x
1
(
x
2
+
x
3
)
+
y
1
(
y
2
+
y
3
)
\vec{a}\cdot\vec{b}+\vec{a}\cdot\vec{c}=x_1x_2+y_1y_2+x_1x_3+y_1y_3=x_1(x_2+x_3)+y_1(y_2+y_3)
a⋅b+a⋅c=x1x2+y1y2+x1x3+y1y3=x1(x2+x3)+y1(y2+y3)
∴
a
⃗
⋅
(
b
⃗
+
c
⃗
)
=
a
⃗
⋅
b
⃗
+
a
⃗
⋅
c
⃗
\therefore \vec{a}\cdot\left(\vec{b}+\vec{c}\right)=\vec{a}\cdot\vec{b}+\vec{a}\cdot\vec{c}
∴a⋅(b+c)=a⋅b+a⋅c
好的,现在实践运用一下:
Q
R
→
⋅
(
(
Q
−
S
)
→
+
(
x
⋅
Q
R
→
)
)
=
0
Q
R
→
⋅
(
Q
−
S
)
→
+
Q
R
→
(
x
⋅
Q
R
→
)
=
0
Q
R
→
⋅
(
Q
−
S
)
→
+
x
⋅
Q
R
→
⋅
Q
R
→
=
0
x
⋅
Q
R
→
⋅
Q
R
→
=
−
Q
R
→
⋅
(
Q
−
S
)
→
x
=
−
Q
R
→
⋅
(
Q
−
S
)
→
Q
R
→
⋅
Q
R
→
\begin{aligned} \overrightarrow{QR}\cdot\left(\overrightarrow{\left(Q-S\right)}+\left(x\cdot\overrightarrow{QR}\right)\right)&=0 \\\overrightarrow{QR}\cdot\overrightarrow{\left(Q-S\right)}+\overrightarrow{QR}\left(x\cdot\overrightarrow{QR}\right)&=0 \\\overrightarrow{QR}\cdot\overrightarrow{\left(Q-S\right)}+x\cdot\overrightarrow{QR}\cdot\overrightarrow{QR}&=0 \\x\cdot\overrightarrow{QR}\cdot\overrightarrow{QR}&=-\overrightarrow{QR}\cdot\overrightarrow{\left(Q-S\right)} \\x&=-\frac{\overrightarrow{QR}\cdot\overrightarrow{\left(Q-S\right)}}{\overrightarrow{QR}\cdot\overrightarrow{QR}} \end{aligned}
QR⋅((Q−S)+(x⋅QR))QR⋅(Q−S)+QR(x⋅QR)QR⋅(Q−S)+x⋅QR⋅QRx⋅QR⋅QRx=0=0=0=−QR⋅(Q−S)=−QR⋅QRQR⋅(Q−S)
现在,我们得出了向量
Q
T
→
=
x
⋅
Q
R
→
\overrightarrow{QT}=x\cdot\overrightarrow{QR}
QT=x⋅QR,定义点
a
(
x
,
y
)
a(x,y)
a(x,y) 加上向量
b
⃗
=
(
x
1
,
y
1
)
\vec{b}=(x_1,y_1)
b=(x1,y1) 的结果为点
c
(
x
+
x
1
,
y
+
y
1
)
c(x+x_1,y+y_1)
c(x+x1,y+y1),所以
T
=
Q
+
Q
T
→
=
Q
+
x
⋅
Q
R
→
T=Q+\overrightarrow{QT}=Q+x\cdot\overrightarrow{QR}
T=Q+QT=Q+x⋅QR,代码如下:
Point foot_point(Point a,Line b)
{
return b.a-b.k*(b.k*(a-b.a)/(b.k*b.k));//推导过程长,但结论简单
}
点对于一条直线的对称点
对于一条直线对称的两个点到这一条直线的距离相等。也就是说,当我们找到了这个点在直线上的垂足,再将它们相连得到向量,并且将其扩大一倍后加上原始点后,所得到的点就是所求得的对称点(见下图)。
代码:
Point symmetric_point(Point a,Line b)
{
Point c=foot_point(a,b);//得到垂足
return a+2*(a-c);//得到对称点
}
多边形
定义多边形:
struct Polygon{//多边形
vector<Point> a;//按照一定顺序存储多边形的顶点
};
多边形面积
具体证明可以参照这里
大概思路即按照顺时针或逆时针求出一点(通常使用原点)与多边形上某两相邻点所组成的三角形的带符号面积之和,取绝对值。
double area_of_polygon(Polygon a)//多边形点要求按照顺时针或逆时针顺序存储
{
double value=0;
for(int i=1;i<a.a.size();i++)
value=value+((Vector){a.a[i-1].x,a.a[i-1].y}^(Vector){a.a[i].x,a.a[i].y});//按照顺序遍历相邻的点,计算面积
value=value+((Vector){a.a[a.a.size()-1].x,a.a[a.a.size()-1].y}^(Vector){a.a[0].x,a.a[0].y});//最后一点与第一个点
return value/2.0*chk(value);//记得除2,叉积所求的是两向量可以围成的平行四边形面积,并不是三角形
}
凸包
凸包的定义:平面凸包是指覆盖平面上n个点的最小的凸多边形。
求多边形凸包
凸包是一个凸多边形,可以利用这个性质求图形的凸包。大致思路可以为:先找到一个一点在凸包上的点,接着按照一定顺序遍历,一直维护,使所求出的图形始终为凸多边形。
Graham
纵坐标最小的点中,横坐标最小的点显然一定在凸包上。接着,将剩下的点与该点连线,逆时针排序,遍历并且维护。详情见下图:
顺次遍历,并且将点放入答案数组:
同时,维护凸多边形。当多边形为凹多边形时:
删除最后加入的点:
然而,只删一次也许图形依然不是凸多边形:
于是我们应该继续删:
直到图形变成凸多边形。
判断是否继续删的条件也十分简单,如果从上一个点走到这一个点,再往下一个点走时是往右边转了,图形一定是不满足条件的(利用叉积性质判断),于是删除。
Polygon graham(vector<Point> a){
Point O=a[0];
int O_id=0;
for(int i=1;i<a.size();i++)
if(a[i].y<O.y){
O=a[i];
O_id=i;
}
else if(a[i].y==O.y&&a[i].x<O.x){
O=a[i];
O_id=i;
}
swap(a[0],a[O_id]);
//----------------------以上代码寻找起点----------------------------
sort(a.begin()+1,a.end(),[&](Point q,Point h)->bool{return chk((O-q)^(O-h))==1?true:(chk((O-q)^(O-h))==-1?false:(q.x!=h.x?q.x<h.x:q.y<h.y));});//利用叉积性质,按照逆时针排序
Polygon ans;
ans.a.push_back(a[0]);
for(int i=1;i<a.size();i++){
while(ans.a.size()>1&&chk((ans.a[ans.a.size()-2]-ans.a[ans.a.size()-1])^(ans.a[ans.a.size()-1]-a[i]))!=1)//维护凸多边形
ans.a.pop_back();
ans.a.push_back(a[i]);
}
return ans;
}
至于为什么要按照逆时针排序,也很简单。由于凸包是凸多边形,按照逆时针(或顺时针)排序后,才可以按照顺序访问到多边形上的每一个点:
Andrew
相比于上一种算法,它更加高效(常数上),并且可以只求出上凸壳或下凸壳。如果我们希望先求出下凸壳:
先按照x,y坐标排序,按照相似于上一种算法的思路维护凸壳,并求出答案:
由于我们所求的为下凸壳,其横坐标一定递增(要不然就出现了调头),所以这种方法可以保证正确性。
如果我们想要求得完美的凸包,反向遍历一遍即可。
Polygon andrew(vector<Point> a)
{
sort(a.begin(),a.end(),[&](Point q,Point h)->bool{if(q.x!=h.x)return q.x<h.x;return q.y<h.y;});
Polygon ans;
ans.a.push_back(a[0]);
for(int i=1;i<a.size();i++){
while(ans.a.size()>1&&chk((ans.a[ans.a.size()-2]-ans.a[ans.a.size()-1])^(ans.a[ans.a.size()-1]-a[i]))==-1)//注意细节!不是 "!=1"
ans.a.pop_back();
ans.a.push_back(a[i]);
}
for(int i=a.size()-1;i>=0;i--){//遍历到起点,最后再删掉,只是为了清除上凸壳结尾多余的点
while(ans.a.size()>1&&chk((ans.a[ans.a.size()-2]-ans.a[ans.a.size()-1])^(ans.a[ans.a.size()-1]-a[i]))==-1)//注意细节!不是 "!=1"
ans.a.pop_back();
ans.a.push_back(a[i]);
}
ans.a.pop_back();
return ans;
}
求凸包直径(旋转卡壳)
很抱歉不会做动图,请各位读者发挥想象力
想象两根平行的直线,夹住凸包,并且贴着它旋转:
那么整个过程中两直线所触碰到的每对点距离取最大值即可。
考虑两根直线分别触碰一条边和一个点的情况:
那么,我们需要更新答案:
我们可以枚举边,找到其对应(最远)点,更新答案。又由于他们都是单向转动的(例如一个顺时针枚举,另一个便会顺时针跟着它动),可以使用双指针来完成答案的更新。