平面(二维)凸包&&luoguP2742 【模板】二维凸包
定义
平面(二维)凸包指覆盖平面上 n n n个点的周长最小的(凸)多边形。
需要注意的是:由于三角形两边之和大于第三边(基本三角不等式),故覆盖平面上 n n n个点的周长最小的多边形一定是一个凸多边形,即无凹陷处。
性质
如果按逆时针方向看,凸包上每两条相邻的边都是向左拐的。比如说,与边 a i ⃗ \vec{a_i} ai顺时针方向相邻的是边 a i + 1 ⃗ \vec{a_{i+1}} ai+1,那么对于任意 i ∈ [ 1 , n ) i\in[1,n) i∈[1,n)( n n n为凸包上点的个数),有:
a i ⃗ × a i + 1 ⃗ > 0 , a n ⃗ × a 1 ⃗ > 0 \vec{a_i}\times\vec{a_{i+1}}>0, \vec{a_n}\times\vec{a_{1}}>0 ai×ai+1>0,an×a1>0
这是因为,用右手从 a i ⃗ \vec{a_i} ai的方向顺小于平角的一边握向 a i + 1 ⃗ \vec{a_{i+1}} ai+1,大拇指总会指向外边。易证多边形上存在 a i ⃗ × a i + 1 ⃗ > 0 \vec{a_i}\times\vec{a_{i+1}}>0 ai×ai+1>0的地方定有凹陷,故凸包有此性质。
(其实讲完性质,Andrew算法的核心思想也就引出来了)
求法
常用的求法有 Graham 扫描法和 Andrew 算法,这里主要介绍 Andrew 算法。
Andrew 算法
首先把所有点以横坐标为第一关键字,纵坐标为第二关键字排序。
显然排序后最小的元素和最大的元素一定在凸包上;而且,因为是凸多边形,我们如果从一个点出发逆时针走,轨迹总是“左拐”的,一旦出现右拐,就说明这一段不在凸包上。因此我们可以用一个单调栈来维护上下凸壳。
因为从左向右看,上下凸壳所旋转的方向不同,为了让单调栈起作用,我们以 x x x坐标为键值,首先升序枚举求出下凸壳,然后降序枚举求出上凸壳。
求凸壳时,一旦发现即将进栈的点( P P P)和栈顶的两个点( S 1 , S 2 S_1,S_2 S1,S2,其中 S 1 S_1 S1为栈顶)行进的方向向右旋转,即叉积小于 0 0 0: S 2 S 1 ⃗ × S 1 P ⃗ < 0 \vec{S_2S_1}\times\vec{S_1P}<0 S2S1×S1P<0(此处需画图),则弹出栈顶,回到上一步继续检测,直到 S 2 S 1 ⃗ × S 1 P ⃗ ≥ 0 \vec{S_2S_1}\times\vec{S_1P}\ge0 S2S1×S1P≥0或栈内仅剩一个元素为止。
最后注意:通常情况下不需要保留位于凸包边上的点,因此上面一段中 S 2 S 1 ⃗ × S 1 P ⃗ < 0 \vec{S_2S_1}\times\vec{S_1P}<0 S2S1×S1P<0这个条件中的“ < < <”可以视情况改为 ≤ \le ≤(实际做题时大多情况都需如此),同时后面一个条件应改为 > > >。
正确性
根据前文所述,我们可以通过扫描过程一定保证局部最优的角度去证明;但简单来讲,我们可以根据凸包对点集的排序方法及扫描过程自己手玩,相信一定会有所理解和收获。
核心代码
void Andrew()//本代码中凸包上的点用vector存储
{
sort(pts+1,pts+n+1);
C.push_back(Point(0,0));//防止C内点的下标从0开始
//C:convex hull 凸包
C.push_back(pts[1]); C.push_back(pts[2]); tot=2;
for(ri i=3;i<=n;++i)//构建下凸壳
{
while(tot>=2&&Cross(C[tot]-C[tot-1],pts[i]-C[tot])<=0) { C.pop_back(); --tot; }//“逆时针拐弯”的全部去掉
//循环条件tot>=2:下凸壳至少有1+1=2个点
//(tot=2时tot尚可自减至1,但还在tot=1基础上再添一点,故至少为2个点)
++tot; C.push_back(pts[i]);
}
C.push_back(pts[n-1]); cnt=++tot;
for(ri i=n-2;i>=1;--i)//构建上凸壳
{
while(tot>=cnt&&Cross(C[tot]-C[tot-1],pts[i]-C[tot])<=0) { C.pop_back(); --tot; }
//循环条件tot>=下凸壳tot+1:防止上凸壳点集的改变丢失了下凸壳点集的信息
++tot; C.push_back(pts[i]);
}
}在这里插入代码片
Code
模板题:luoguP2742 [USACO5.1]圈奶牛Fencing the Cows /【模板】二维凸包
题意:求凸包周长
#include<cstdio>
#include<cmath>
#include<iostream>
#include<algorithm>
#include<vector>
#define ri register int
using namespace std;
const int MAXN=100020;
struct Point{
double x,y;
Point(double x=0,double y=0):x(x),y(y){}
//构造方法:之后单独定义一个Point结构体可写做Point(x,y)而非传统写法(Point){x,y}
}pts[MAXN];//pts:经去重后的初始点集
typedef Point Vector;
typedef vector<Point> Polygon;
int n,tot,cnt;
double ans;
Polygon C;//C:convex(凸包),显然凸包是个多边形
const double eps=1e-8;
bool dcmp(double x,double y){
return fabs(x-y)<=eps;
}
bool operator <(const Point &a,const Point &b){
return a.x < b.x || (dcmp(a.x, b.x) && a.y < b.y);
}
bool operator ==(const Point &a,const Point &b){
return dcmp(a.x-b.x,0.0)&&dcmp(a.y-b.y,0.0);
}
Vector operator +(Vector A,Vector B) { return Vector(A.x+B.x,A.y+B.y); }
Vector operator -(Vector A,Vector B) { return Vector(A.x-B.x,A.y-B.y); }
double Cross(Vector A,Vector B) { return A.x*B.y-A.y*B.x; }
double Norm(Vector A) { return A.x*A.x+A.y*A.y; }
double Length(Vector A) { return sqrt(Norm(A)); }
void Andrew()
{
sort(pts+1,pts+n+1);
C.push_back(Point(0,0));//防止C内点的下标从0开始
C.push_back(pts[1]); C.push_back(pts[2]); tot=2;
for(ri i=3;i<=n;++i)//构建下凸壳
{
while(tot>=2&&Cross(C[tot]-C[tot-1],pts[i]-C[tot])<=0) { C.pop_back(); --tot; }
//循环条件tot>=2:下凸壳至少有1+1个点
++tot; C.push_back(pts[i]);
}
C.push_back(pts[n-1]); cnt=++tot;
for(ri i=n-2;i>=1;--i)//构建上凸壳
{
while(tot>=cnt&&Cross(C[tot]-C[tot-1],pts[i]-C[tot])<=0) { C.pop_back(); --tot; }
//循环条件tot>=下凸壳tot+1:防止上凸壳点集的改变丢失了下凸壳点集的信息
++tot; C.push_back(pts[i]);
}
}
int main()
{
scanf("%d",&n);
for(ri i=1;i<=n;++i) scanf("%lf%lf",&pts[i].x,&pts[i].y);
Andrew();
for(ri i=1;i<tot;++i) ans+=Length(C[i+1]-C[i]);
//根据上面Andrew算法的具体算法流程易得:C序列为凸包点集+C[1](C[1]对应的点出现在C[1]与C[tot])
printf("%.2lf",ans);
return 0;
}在这里插入代码片