向量是一类既有大小又有方向的量,如加速度,速度,位移等等
向量的编程表示:
typedef struct{
double x;
double y;
}Vector;
注:平面中的点也是用一对x,y来表示的,和向量是一样的,所以常常如下操作:
typedef struct{
int x;
int y;
}Point;
typedef Point Vector;
既避免了混淆两种变量,又使程序看起来非常简洁
重载运算符:
Vector operator - (Point a,Point b){
return Vector {a.x-b.x,a.y-b.y};
}
↑两个点相减得到向量,便于书写
叉积又称叉乘,符号为×。
两个向量叉乘的结果还是一个向量
其运算规则如下:
①所得向量的模为:
∣ a × b ∣ = ∣ a ∣ ⋅ ∣ b ∣ ⋅ s i n θ |a×b|=|a|\cdot|b|\cdot sinθ ∣a×b∣=∣a∣⋅∣b∣⋅sinθ
其中 θ θ θ为向量a与向量b的夹角
②所得向量的方向为:
与a,b所在平面垂直,且满足右手定则(由a转向b角度小于180,大拇指所指方向即为叉乘所得向量的方向)
向量a: ( a x , a y ) (a_x,a_y) (ax,ay)
向量b: ( b x , b y ) (b_x,b_y) (bx,by)
a × b = a x ⋅ b y − a y ⋅ b x a×b=a_x\cdot b_y-a_y\cdot b_x a×b=ax⋅by−ay⋅bx
证明:
①叉乘满足分配律:
a × ( b + c ) = a × b + a × c a×(b+c)=a×b+a×c a×(b+c)=a×b+a×c
②令 e 1 e_1 e1表示x轴方向的单位向量, e 2 e_2 e2表示y轴方向的单位向量,则向量 a = a x ⋅ e 1 + a y ⋅ e 2 a=a_x\cdot e_1+a_y\cdot e_2 a=ax⋅e1+ay⋅e2,向量 b = b x ⋅ e 1 + b y ⋅ e 2 b=b_x\cdot e_1+b_y\cdot e_2 b=bx⋅e1+by⋅e2
③ a × b a×b a×b
= ( a x ⋅ e 1 + a y ⋅ e 2 ) ⋅ ( b x ⋅ e 1 + b y ⋅ e 2 ) =(a_x\cdot e_1+a_y\cdot e_2)\cdot(b_x\cdot e_1+b_y\cdot e_2) =(ax⋅e1+ay⋅e2)⋅(bx⋅e1+by⋅e2)
= ( a x ⋅ b x ⋅ e 1 ⋅ e 1 ) + ( a x ⋅ b y ⋅ e 1 ⋅ e 2 ) + ( a y ⋅ b x ⋅ e 2 ⋅ e 1 ) + ( a y ⋅ b y ⋅ e 2 ⋅ e 2 ) =(a_x\cdot b_x\cdot e_1\cdot e_1)+(a_x\cdot b_y\cdot e_1\cdot e_2)+(a_y\cdot b_x\cdot e_2\cdot e_1)+(a_y\cdot b_y\cdot e_2\cdot e_2) =(ax⋅bx⋅e1⋅e1)+(ax⋅by⋅e1⋅e2)+(ay⋅bx⋅e2⋅e1)+(ay⋅by⋅e2⋅e2)
由 ∣ a × b ∣ = ∣ a ∣ ⋅ ∣ b ∣ ⋅ s i n θ |a×b|=|a|\cdot|b|\cdot sinθ ∣a×b∣=∣a∣⋅∣b∣⋅sinθ可得:
e 1 ⋅ e 1 = 0 ; e 2 ⋅ e 2 = 0 e_1\cdot e_1=0;e_2\cdot e_2=0 e1⋅e1=0;e2⋅e2=0(夹角为0)
e 1 ⋅ e 2 = 1 ; e 2 ⋅ e 1 = − 1 e_1\cdot e_2=1;e_2\cdot e_1=-1 e1⋅e2=1;e2⋅e1=−1(夹角90°)
由右手定则可得,前者叉乘方向指向z轴正方向,所以值为正。后者反之,值为负。
综上,即得 a × b = a x ⋅ b y − a y ⋅ b x a×b=a_x\cdot b_y-a_y\cdot b_x a×b=ax⋅by−ay⋅bx
①由向量叉积的方向可得:
如果向量b在向量a的左边/上边(逆时针方向小于180°),则它们的叉乘结果方向垂直纸面向外(从坐标系的角度考虑,即z轴正方向);
反之,如果向量b在向量a的右边/下边(逆时针方向大于180°),则它们的叉乘结果方向垂直纸面向里(z轴负方向);
建议画图理解
所以在坐标系中,叉乘结果为正代表向量b在向量a的逆时针小于180°方向;结果为负代表向量b在向量a的逆时针大于180°方向(为0共线/平行)
↑也就是叉乘在求凸包中的应用了,即求得向量的顺逆时针关系
②由向量叉积的模可得:
高中学过的三角形面积公式:
1 2 ∣ a ∣ ⋅ ∣ b ∣ ⋅ s i n θ \frac{1}{2} |a|\cdot|b|\cdot sinθ 21∣a∣⋅∣b∣⋅sinθ
(因为 ∣ b ∣ ⋅ s i n θ |b|\cdot sinθ ∣b∣⋅sinθ即三角形中边a的高)
那么叉积的值也就是向量a,b构成的平行四边形的面积
double Cross(Vector a,Vector b){
return A.x*B.y-A.y*B.x;
}
tips:叉乘英文为Cross Product
将给定点包围在内部的面积最小的凸多边形
因为要满足面积最小,凸包必然是由这给定点中的若干个组成的
①将所有点按x坐标从小到大排序(如果x相同,则y从小到大排序),可以通过反证法证明:x坐标最小的那个点必然是凸包上的一个点
②不妨令坐标都被存放在数组p(排过序)中,并用一个Ch数组存放凸包(tips:凸包英文Convexhull),取出数组p中的前两个点 p 1 p_1 p1、 p 2 p_2 p2加入凸包
③判断 p 3 p_3 p3与 p 1 p 2 p_1p_2 p1p2( p 1 p_1 p1、 p 2 p_2 p2构成的向量)的关系:
如果是第1种情况, p 3 p_3 p3在向量 p 1 p 2 p_1p_2 p1p2的下方(即 p 1 p 3 p_1p_3 p1p3在 p 1 p 2 p_1p_2 p1p2的顺时针小于180°的位置),那么我们就可以判断p2必定不是凸包上的点,于是把 p 2 p_2 p2点从凸包中删除
如果是第2种情况,则 p 2 p_2 p2仍可能是凸包上的点,不做处理
然后将 p 3 p_3 p3加入凸包
④不断重复上述操作,判断新的点是否在“前进方向”的上方,(即最近加入凸包的两个点形成的向量),如果不是,则需要删除最近加入凸包的那个点,注意删除完后还需要继续判断
用栈来表示可以更好理解,不断向栈加入点,遇到新点时和栈顶的两个点进行形成的向量比较位置关系,若在下方则删除栈顶的点,然后继续和栈顶的两个点比较,直到新点处于上方为止(或者栈中点不足两个)。
⑤这样不断操作直到点 p n p_n pn,就求出了凸包的下半部分,然后反过来从 p n p_n pn开始,重复操作就能求出凸包的上半部分。合起来就得到了一个完整的凸包
①判断新点与“前进方向”的位置关系通过前文所讲的向量叉积实现,根据结果正负性判断方向
②这道题用了栈的思想,但是用数组模拟也可以
#include
#include
#include
#include
using namespace std;
const double eps=1e-8;
typedef struct{
int x,y;
}Point;
typedef Point Vector;
Point P[50005];
Point Ch[50005];
Vector operator - (Point a,Point b){
return Vector {a.x-b.x,a.y-b.y};
}
int Cross(Vector a,Vector b){
return a.x*b.y-a.y*b.x;
}
bool cmp(Point a,Point b){
if(a.x!=b.x) return a.x<b.x;
return a.y<b.y;
}
int main()
{
int N;
scanf("%d",&N);
for(int i=0;i<N;i++)
scanf("%d%d",&P[i].x,&P[i].y);
sort(P,P+N,cmp);
int cnt=0;
for(int i=0;i<N;i++){
while(cnt>1&&Cross(Ch[cnt-2]-Ch[cnt-1],Ch[cnt-2]-P[i])<=0)
cnt--;
Ch[cnt++]=P[i];
}
int vis=cnt;
for(int i=N-2;i>=0;i--){
while(cnt>vis&&Cross(Ch[cnt-2]-Ch[cnt-1],Ch[cnt-2]-P[i])<=0)
cnt--;
Ch[cnt++]=P[i];
}
return 0;
}