二维凸包 学习笔记

前置芝士

计算几何基础

板子题

题目传送门
农夫约翰想要建造一个围栏用来围住他的奶牛,可是他资金匮乏。他建造的围栏必须包括他的奶牛喜欢吃草的所有地点。对于给出的这些地点的坐标,计算最短的能够围住这些点的围栏的长度。奶牛数量 \(n\le 10^5\)

题目解析

显然就是求覆盖给出的点的最小凸包。
什么是凸包呢?其实就是凸多边形。显然我们发现凸包的距离是最短的。我们可以想象在一个平面上有一些钉子,现在又一根有弹性的橡皮筋要去要框住这些钉子,橡皮筋最后的形状显然就是一个最小凸包。

Graham 算法

Graham 算法的思路大致如下:
首先我们发现,\(x\) 坐标最小的点(若果有多个点的 \(x\) 坐标相等就取 \(y\) 坐标最小的)肯定是在凸包里面的。
然后其他的点按照与最初的点的极角的大小排序,极角相等则将距离小的点放在前面。
最后遍历每一个点,维护一个栈,栈里面是在凸包上的点。如果新进入的点导致图形不是凸包,那么就 退栈 来维护凸包。
算法复杂度 \(O\left(n\log n\right)\),瓶颈在于排序。

支线:探讨向量运算在 Graham 算法中的使用

这里主要是涉及到两个方面:极角排序中的 cmp 函数以及判断凸包凸性。

显然在极角排序中我们所写的 cmp 函数只需要返回两个点之间极角大小的比较。
显然我们可以直接算出两个向量的斜率,也就是正切值,然后使用 cmath 库内的 double atan(duoble) 函数获得极角的弧度值,然后比较。
如说使用正切函数的性质,我们就不需要使用 atan 函数,但是仍然避免不了除法,会有一定的精度损失(具体的原因需要去了解一下 C++ 底层浮点数的储存方法)。

那么我们是否存在一种比较方法不使用除法呢?
显然我们可以使用向量的运算来解决这个问题。因为我们知道向量的运算不存在除法。假设最初找到的点为 \(P\),要比较的两个点为 \(A\)\(B\)
由于点 \(P\) 是最下方、最左方的点,所以向量 \(\overrightarrow{PA}\)\(\overrightarrow{PB}\) 的极角大小在 \([0,\pi]\) 区间,因此我们可以直接用 \(\overrightarrow{PA}\times\overrightarrow{PB}\) 的正负来进行比较(当然在做半平面交的时候极角排序需要先判断向量极角大小所在的半平面)。

然后问题就是判断凸包的凸性了。当然记录两个向量的正切值也是可以的,不过一样有精度误差。
所以说我们也需要用向量的运算来判断凸包的凸性。
设加入的点为 \(A\),凸包上后面两个点(也就是栈顶的两个点)为 \(B\)\(C\),记 \(t=\overrightarrow{CB}\times\overrightarrow{BA}\)
我们发现,若 \(t>0\),则凸包满足凸性,不需要退栈;若 \(t<0\),则凸包不满足凸性,需要退栈;若 \(t=0\),则三点共线,退不退栈均可。

代码(建议使用较为宽的屏幕阅读):

#include<cmath>
#include<cstdio>
#include<algorithm>
#define I inline
#define db long double
#define U unsigned
#define R register
#define ll long long
#define RI register int
#define ull unsigned long long
#define abs(x) ((x)>0?(x):(-(x)))
#define max(a,b) ((a)>(b)?(a):(b))
#define min(a,b) ((a)<(b)?(a):(b))
#define Me(a,b) memset(a,b,sizeof(a))
#define EPS (1e-7)
#define INF (0x7fffffff)
#define LL_INF (0x7fffffffffffffff)
#define maxn 100039
//#define debug
using namespace std;
#define Type int
I Type read(){
	Type sum=0; int flag=0; char c=getchar();
	while((c<'0'||c>'9')&&c!='-') c=getchar(); if(c=='-') c=getchar(),flag=1;
	while('0'<=c&&c<='9'){ sum=(sum<<1)+(sum<<3)+(c^48); c=getchar(); }
	if(flag) return -sum; return sum;
}
struct Vector{
	db x,y;
	Vector operator + (const Vector &x) const{ return (Vector){this->x+x.x,this->y+x.y}; }
	Vector operator - (const Vector &x) const{ return (Vector){this->x-x.x,this->y-x.y}; }
	double operator * (const Vector &x) const{ return this->x*x.y - this->y*x.x;  }
	bool operator < (const Vector &x) const { if(this->y==x.y) return this->x < x.x; return this->y < x.y; }
}a[maxn];
int n,minx;
struct Stack{
	int data[maxn],_top;
	int top(){ return data[_top]; }
	void push(int x){ data[++_top]=x; return; }
	void pop(){ _top--; return; }
	void clear(){ _top=0; return; }
}s;
I db dis(Vector x,Vector y){ return sqrt((x.x-y.x)*(x.x-y.x)+(x.y-y.y)*(x.y-y.y)); }
I void swap(Vector &a,Vector &b){ Vector tmp=a; a=b; b=tmp; return; } 
I Vector get(Vector x,Vector y){ return y-x; } 
I int cmp(Vector x,Vector y){
	if(fabs(get(a[1],x)*get(a[1],y))<EPS) return dis(a[1],x) < dis(a[1],y);
	return get(a[1],x)*get(a[1],y)>0;//按照极角排序
}
int main(){
	n=read(); RI i; for(i=1;i<=n;i++) scanf("%Lf%Lf",&a[i].x,&a[i].y);
	minx=1; for(i=2;i<=n;i++) if(a[i]<a[minx]) minx=i; if(minx!=1) swap(a[1],a[minx]);
	sort(a+2,a+n+1,cmp); s.clear(); s.push(1); s.push(2);
	for(i=3;i<=n;i++){
		while(get(a[s.data[s._top-1]],a[s.top()])*get(a[s.top()],a[i])<0 && s._top>=2) s.pop();
		s.push(i); }
	db ans=dis(a[1],a[s.top()]);
	for(i=1;i<s._top;i++) ans+=dis(a[s.data[i]],a[s.data[i+1]]);
	printf("%0.2Lf",ans); return 0;
}

当然这题不用开 long double 也能过

posted @ 2021-09-26 20:30  jiangtaizhe001  阅读(29)  评论(0编辑  收藏  举报