凸包的定义是,包住所有给定点的最小凸多边形。他有一个性质是,凸包肯定是所有包住所有点的多边形中周长最小的,因此还可以有一种定义是:包住所有点的,周长最小的多边形
怎么求凸包,首先凸多边形有一个性质:顺着它的边走,肯定是一直在左拐的,例如边上三个相邻点 A , B , C A,B,C A,B,C,那他们形成的向量肯定有 A B × B C > 0 AB×BC>0 AB×BC>0,这里就是利用矢量叉乘判断,叉乘大于零说明左拐的度数小于 180 180 180,也就是左拐而没有右拐。
那么利用这个性质,我们可以在每次加入一个点的时候,看他和凸包里前两个点构成的向量,是不是左拐,如果是右拐,就把凸包里前一个点删掉,然后继续判断,直到是左拐,或凸包中点不足两个
这个过程也可以用图形来理解,如下图,左边三个点显然出现右拐了,那么就把中间那个点删掉了。这是因为出现右拐,这个地方就变成凹多边形了,但是我们在求的是个凸多边形,所以要把这个凹进去的点删掉
具体实现上,保存的都是满足条件的点,每次把栈顶的不满足要求的点删掉,直到合法,这其实是类似单调栈的方式。实际上我们也确实是在维护一个单调的量:边所在直线的斜率。注意到在上半凸包和下半凸包里,边斜率的变化都是单调的,所以我们也就是要维护一个斜率的单调栈。这里需要注意,斜率单调只对一半的凸包是成立的,所以上下两半我们要分开求。
此外实现的时候,我们需要按x第一关键字,y第二关键字排序,再跑单调栈
void solve() {
int n;
cin>>n;
vector<vector<db>>a;
rep(i,1,n){
db x,y;
cin>>x>>y;
a.push_back({x,y});
}
sort(a.begin(),a.end(),[&](vector<db>&x,vector<db>&y){
if(x[0]==y[0])return x[1]<y[1];
return x[0]<y[0];
});
stack<int>s;
auto cross=[&](vector<db>x,vector<db>y)->db{
return x[0]*y[1]-x[1]*y[0];
};
auto check=[&](int x)->bool{
auto t1=s.top();
s.pop();
auto t2=s.top();
s.push(t1);
return cross({a[t1][0]-a[t2][0],a[t1][1]-a[t2][1]},{a[x][0]-a[t1][0],a[x][1]-a[t1][1]})<0;
};
rep(i,0,n-1){
while(s.size()>=2&&check(i)){
s.pop();
}
s.push(i);
}
vi res,res1;
while(s.size()){
res.push_back(s.top());
s.pop();
}
reverse(res.begin(),res.end());
rep1(i,n-1,0){
while(s.size()>=2&&check(i)){
int x=s.top();
s.pop();
}
s.push(i);
}
while(s.size()){
res1.push_back(s.top());
s.pop();
}
reverse(res1.begin(),res1.end());
for(int x:res1){
res.push_back(x);
}
db ans=0;
auto dis=[&](vector<db>x,vector<db>y)->db{
return sqrt((x[0]-y[0])*(x[0]-y[0])+(x[1]-y[1])*(x[1]-y[1]));
};
int m=res.size();
rep(i,1,m-1){
ans+=dis(a[res[i]],a[res[i-1]]);
}
printf("%.2lf",ans);
}