凸包
凸包
前置知识
凸多边形与凸包
在了解凸包之前需要先了解凸多边形 (Convex Polygon)。直观的来看,一个凸多边形就是没有任何凹陷位的多边形,例如三角形、正方形、平行四边形、正五边形、正六边形等等。但是下面的这个“凸”字形却并非凸多边形,因为箭头所指之处实际是一个凹陷位。
在数学上,凸多边形有另一个严格的定义。假设在一个多边形上(包括多边形的边界及边界围封的区域)任意取两点,并以一条线连接该两点,如果线段上的每一点,均在该多边形上,那么我们便认为这个多边形是凸多边形。
根据上述定义,便可以判断“凸”字形并非凸多边形,下图连接 A、B 点的线段有一部分并不在该多边形上。
凸包概念
在了解了凸多边形之后,便可深入凸包 (Convex Hull)了。给定平面上的一个有限点集,这个点集的凸包就是包含点集中所有点的最小面积的凸多边形。用不严谨的话来讲,给定二维平面上的点集,凸包就是将最外层的点连接起来构成的凸多边型,它能包含点集中所有的点。
例如下图的点集共包含了12个点,图中的五边形便是该点集的凸包,构成五边形的五个点称为“凸包上的点”,其余点并非“凸包上的点”。
向量叉乘
定义
$$\vec C = \vec A \times \vec B$$
向量的叉乘,即求同时垂直两个向量的向量,即$\vec C$垂直于$\vec A$,且$\vec C$垂直于$\vec B$。
假设向量 $\vec A=(a.x, a.y, a.z),\vec B=(b.x, b.y, b.z),\vec C=(c.x, c.y, c.z)$,则
$$\vec C = \vec A \times \vec B = (a.y * b.z - a.z * b.y, a.z * b.x - a.x * b.z, a.x * b.y - a.y * b.x)$$
参考下图:
几何意义
$$|C| = |A| \times |B| = |A|\cdot|B|\cdot \sin\theta$$
$\theta$ 为两向量的夹角,$|C|$ 为两向量构成的平行四边形的面积。
应用
假设有两个二维向量$\vec A$、$\vec B$,可以直接把他们视作三维向量(a.z = b.z = 0)
此时,$\vec A$、$\vec B$ 的叉乘结果为
$$|C| = |A| \times |B| = (0, 0, a.x * b.y - a.y * b.x)$$
令$k = c.z = a.x * b.y - a.y * b.x$
可以由 $k$ 得到:
- 计算 $\vec A$、$\vec B$ 向量构成的平行四边形的面积。
向量 $\vec A$、$\vec B$ 的叉乘的模等于由 $\vec A$、$\vec B$ 组成的平行四边形的面积。 - 判断旋转角度。
如果 $k>0$时,那么$\vec A$正旋转到$\vec B$的角度为<180°,即$\vec B$在$\vec A$左侧;
如果 $k<0$,那么$\vec A$正旋转到$\vec B$的角度为>180°;
如果 $k=0$,那么$\vec A$、$\vec B$向量平行。
算法介绍
Graham扫描法
思路:先找到凸包上的一个点,然后从那个点开始按逆时针方向逐个找凸包上的点,实际上就是进行极角排序,然后对其查询使用。
实现步骤:
- 把所有点放在二维坐标系中,则纵坐标最小的点一定是凸包上的点,如图中的P0。
- 把所有点的坐标平移一下,使 P0 作为原点,如上图。
- 计算各个点相对于 P0 的幅角 α ,按从小到大的顺序对各个点排序。当 α 相同时,距离 P0 比较近的排在前面。例如上图得到的结果为 P1,P2,P3,P4,P5,P6,P7,P8。我们由几何知识可以知道,结果中第一个点 P1 和最后一个点 P8 一定是凸包上的点。
- 连接P0和栈顶的那个点,得到直线 L 。看当前点是在直线 L 的右边还是左边。如果在直线的右边就执行步骤5;如果在直线上,或者在直线的左边就执行步骤6。
- 如果在右边,则栈顶的那个元素不是凸包上的点,把栈顶元素出栈。执行步骤4。
- 当前点是凸包上的点,把它压入栈,执行步骤7。
- 检查当前的点 P2 是不是步骤3那个结果的最后一个元素。是最后一个元素的话就结束。如果不是的话就把 P2 后面那个点做当前点,返回步骤4。
最后,栈中的元素就是凸包上的点了。
下图为用 Graham 扫描法求解的过程:
代码实现
赶时间,实在是不想写了😤。直接 copy 现成的代码,出处见结尾参考链接3。
1 | int cross(const std::vector<int>& p, const std::vector<int>& x, const std::vector<int>& y) {//计算向量px × py, 当返回值大于0时点y在向量px左边 |