平面(二维)凸包&&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;
}在这里插入代码片
posted on 2021-01-11 20:53  cgqzsyc  阅读(9)  评论(0)    收藏  举报