今天我们要涉及计算几何中的凸包。凸包(Convex Hull)是一个计算几何(图形学)中的概念。
凸包这个东西,我们其实并不陌生,因为在斜率优化的时候我们就需要用到凸包来进行辅助。但是今天讲的是怎么求出凸包。
在一个实数向量空间V中,对于给定集合X,所有包含X的凸集的交集S被称为X的凸包。X的凸包可以用X内所有点(X1…Xn)的凸组合来构造.
在二维欧几里得空间中,凸包可想象为一条刚好包著所有点的橡皮圈。
那么首先给出凸包的正式定义:
对于一个集合D,所有包含D的凸集之交称为D的凸包。
用不严谨的话来讲,给定二维平面上的点集,凸包就是将最外层的点连接起来构成的凸多边形,它能包含点集中所有的点。
那么我们怎么求凸包呢?首先有的就是暴力穷举的想法。我们今天介绍的是一种更为优秀的方法:graham扫描法。
在知道这种方法前,我们需要知道一个概念:向量的叉积。
向量的叉积
向量(矢量)的叉积在数学,物理(又名叉乘),信息学中都有重要的应用。比如物理里面的力矩,就等于F矢量叉乘上r矢量。矢量叉积的表达式为 ,方向用右手螺旋定则判定。而如果我们将两个向量对应到平面极座标上,那么若向量A为点(a,b),向量B为点(c,d),那么A和B的叉积为 。我们发现当向量叉积为正时,向量A的幅角小于向量B的幅角。于是我们可以用向量的叉积判断两个向量的角度关系。
关于精度问题
由于double类型精度上还是存在一点问题,所以我们在a==b
时要写成fabs(a-b)<eps
,eps一般取1e-7
到1e-10
左右
同理。
Graham扫描法
首先我们可以立即发现凸包一个性质:所有点中最左下角的点一定是凸包上的点。那么我们就按所有点与最左下角的点(后文称为 )构成的向量的幅角的角度排序,如下图:
然后按照排序顺序根据相邻两边的拐向开始贪心。由于凸包的性质,每次拐都不能使叉积小于0。
具体实现可以利用一个栈存下当前的凸包上的点,每次取栈顶两个点和新的点进行拐向判断就行了。
CODE:
#include<bits/stdc++.h>
#define db double
#define MAXN 100005
using namespace std;
const db eps=1e-8;
struct node{
db x,y;
node (db xx=0,db yy=0){x=xx,y=yy;}
}F[MAXN];
node operator+(node a,node b){return node(a.x+b.x,a.y+b.y);}
node operator-(node a,node b){return node(a.x-b.x,a.y-b.y);}
db operator*(node a,node b){return a.x*b.y-a.y*b.x;}
double dis(node x,node y){
return sqrt((y.y-x.y)*(y.y-x.y)+(y.x-x.x)*(y.x-x.x));
}
int cmp(node a,node b){
db tmp=(a-F[1])*(b-F[1]);
if(tmp>eps) return 1;
if(fabs(tmp)<eps&&dis(a,F[1])-dis(b,F[1])<eps) return 1;
return 0;
}
int n,sta[MAXN],top;
db ans;
int main()
{
scanf("%d",&n);
for(int i=1;i<=n;i++){
scanf("%lf%lf",&F[i].x,&F[i].y);
if(F[i].y-F[1].y<-eps||fabs(F[i].y-F[1].y)<eps&&F[i].x-F[1].x<eps) swap(F[1],F[i]);
}
sort(F+2,F+1+n,cmp);
sta[++top]=1;sta[++top]=2;
for(int i=3;i<=n;i++){
while(top>1&&(F[sta[top]]-F[sta[top-1]])*(F[i]-F[sta[top-1]])<eps) top--;
sta[++top]=i;
}
for(int i=2;i<=top;i++) ans+=dis(F[sta[i-1]],F[sta[i]]); //统计所有凸包上的点的距离
ans+=dis(F[sta[top]],F[1]);
printf("%.2lf",ans);
return 0;
}