- admin's blog
【模板】 凸包
- @ 2025-1-8 14:52:34
【模板】凸包
Andrew 算法求凸包
常用的求法有 Graham 扫描法和 Andrew 算法,这里主要介绍 Andrew 算法。
性质
该算法的时间复杂度为 ( O(n\log n) ),其中 ( n ) 为待求凸包点集的大小,复杂度的瓶颈在于对所有点坐标的双关键字排序。
过程
首先把所有点以横坐标为第一关键字,纵坐标为第二关键字排序。
显然排序后最小的元素和最大的元素一定在凸包上。而且因为是凸多边形,我们如果从一个点出发逆时针走,轨迹总是「左拐」的,一旦出现右拐,就说明这一段不在凸包上。因此我们可以用一个单调栈来维护上下凸壳。
因为从左向右看,上下凸壳所旋转的方向不同,为了让单调栈起作用,我们首先 升序枚举 求出下凸壳,然后 降序 求出上凸壳。
求凸壳时,一旦发现即将进栈的点(( P ))和栈顶的两个点(( S_1, S_2 ),其中 ( S_1 ) 为栈顶)行进的方向向右旋转,即叉积小于 0:
$$\overrightarrow{S_2S_1} \times \overrightarrow{S_1P} < 0$$则弹出栈顶,回到上一步,继续检测,直到
$$\overrightarrow{S_2S_1} \times \overrightarrow{S_1P} \ge 0$$或者栈内仅剩一个元素为止。
通常情况下不需要保留位于凸包边上的点,因此上面一段中
$$\overrightarrow{S_2S_1} \times \overrightarrow{S_1P} < 0$$这个条件中的「<」可以视情况改为 (\le),同时后面一个条件应改为 (> )。
#include<bits/stdc++.h>
const int N=1e6+12;
using namespace std;
struct point
{
double x,y;
bool operator < ( const point &other )
const
{
if( x==other.x )
{
return y<other.y;
}
return x<other.x;
}
double operator *( const point &other )
const
{
return x*other.y-y*other.x;
}
point operator - ( const point &other )
const
{
return { x-other.x,y-other.y };
}
}p[N];
double dis(point p1,point p2)
{
return sqrt( (p1.x-p2.x)*(p1.x-p2.x)+(p1.y-p2.y)*(p1.y-p2.y) );
}
int sta[N],top,n;
bool u[N];
double ans;
int main(){
cin>>n;
for(int i=1;i<=n;i++)
{
cin>>p[i].x>>p[i].y;
}
sort(p+1,p+1+n);
sta[++top]=1;
for(int i=2;i<=n;i++)
{
while( top>=2 &&
( (p[ sta[top] ]-p[ sta[top-1] ]) * ( p[i]-p[sta[top]] ) ) <=0
)
u[ sta[top--] ]=0;
++top;
sta[top]=i;
u[i]=1;
}
int tmp=top;
for(int i=n-1;i;i--)
{
if( !u[i] )
{
while( top>tmp &&
( (p[ sta[top] ]-p[ sta[top-1] ]) * ( p[i]-p[sta[top]] ) ) <=0
)
u[ sta[top--] ]=0;
++top;
sta[top]=i;
u[i]=1;
}
}
sta[top+1]=1;
for(int i=1;i<=top;i++)
{
ans+=dis( p[sta[i]],p[sta[i+1]] );
}
printf("%.2lf\n",ans);
}
Graham 扫描法
性质
与 Andrew 算法相同,Graham 扫描法的时间复杂度为 ( O(n\log n) ),复杂度瓶颈也在于对所有点排序。
过程
首先找到所有点中,纵坐标最小的一个点 ( P )。根据凸包的定义我们知道,这个点一定在凸包上。然后将所有的点以相对于点 ( P ) 的极角大小为关键字进行排序。
和 Andrew 算法类似地,我们考虑从点 ( P ) 出发,在凸包上逆时针走,那么我们经过的所有节点一定都是「左拐」的。形式化地说,对于凸包逆时针方向上任意连续经过的三个点 ( P_1, P_2, P_3 ),一定满足:
$$\overrightarrow{P_1 P_2} \times \overrightarrow{P_2 P_3} \ge 0$$新建一个栈用于存储凸包的信息,先将 ( P ) 压入栈中,然后按照极角序依次尝试加入每一个点。如果进栈的点 ( P_0 ) 和栈顶的两个点 ( P_1, P_2 )(其中 ( P_1 ) 为栈顶)行进的方向「右拐」了,那么就弹出栈顶的 ( P_1 ),不断重复上述过程直至进栈的点与栈顶的两个点满足条件,或者栈中仅剩下一个元素,再将 ( P_0 ) 压入栈中。
#include<bits/stdc++.h>
using namespace std;
const int N=1e6+12;
int n,top,sta[N];
struct point
{
double x,y,ang;
point operator - ( const point &p ) const
{
return {x-p.x,y-p.y,0};
}
}p[N];
double dis(point p1,point p2)
{
return sqrt( (p1.x-p2.x)*(p1.x-p2.x)+(p1.y-p2.y)*(p1.y-p2.y) );
}
bool cmp(point p1,point p2)
{
if( p1.ang==p2.ang )
{
return dis(p1,p[1])<dis(p2,p[1]);
}
return p1.ang<p2.ang;
}
double cross(point p1,point p2)
{
return p1.x*p2.y-p2.x*p1.y;
}
int main(){
cin>>n;
for(int i=1;i<=n;i++)
{
cin>>p[i].x>>p[i].y;
}
for(int i=2;i<=n;i++)
{
if( p[i].y<p[1].y || ( p[i].y==p[1].y && p[i].x<p[1].x ) )
{
swap(p[1],p[i]);
}
}
for(int i=2;i<=n;i++)
{
p[i].ang=atan2(p[i].y-p[1].y,p[i].x-p[1].x);
}
sort(p+2,p+1+n,cmp);
sta[++top]=1;
for(int i=2;i<=n;i++)
{
while( top>=2 &&
cross( p[sta[top]]-p[sta[top-1]],p[i]-p[sta[top]] )<=0
)
top--;
sta[++top]=i;
}
sta[top+1]=sta[1];
double ans=0;
for(int i=1;i<=top;i++)
{
ans+=dis( p[sta[i]],p[sta[i+1]] );
}
printf("%.2lf",ans);
}