二维凸包 学习笔记
前置芝士
板子题
题目传送门
农夫约翰想要建造一个围栏用来围住他的奶牛,可是他资金匮乏。他建造的围栏必须包括他的奶牛喜欢吃草的所有地点。对于给出的这些地点的坐标,计算最短的能够围住这些点的围栏的长度。奶牛数量 。
题目解析
显然就是求覆盖给出的点的最小凸包。
什么是凸包呢?其实就是凸多边形。显然我们发现凸包的距离是最短的。我们可以想象在一个平面上有一些钉子,现在又一根有弹性的橡皮筋要去要框住这些钉子,橡皮筋最后的形状显然就是一个最小凸包。
Graham 算法
Graham 算法的思路大致如下:
首先我们发现, 坐标最小的点(若果有多个点的 坐标相等就取 坐标最小的)肯定是在凸包里面的。
然后其他的点按照与最初的点的极角的大小排序,极角相等则将距离小的点放在前面。
最后遍历每一个点,维护一个栈,栈里面是在凸包上的点。如果新进入的点导致图形不是凸包,那么就 退栈 来维护凸包。
算法复杂度 ,瓶颈在于排序。
支线:探讨向量运算在 Graham 算法中的使用
这里主要是涉及到两个方面:极角排序中的 cmp
函数以及判断凸包凸性。
显然在极角排序中我们所写的 cmp
函数只需要返回两个点之间极角大小的比较。
显然我们可以直接算出两个向量的斜率,也就是正切值,然后使用 cmath
库内的 double atan(duoble)
函数获得极角的弧度值,然后比较。
如说使用正切函数的性质,我们就不需要使用 atan
函数,但是仍然避免不了除法,会有一定的精度损失(具体的原因需要去了解一下 C++ 底层浮点数的储存方法)。
那么我们是否存在一种比较方法不使用除法呢?
显然我们可以使用向量的运算来解决这个问题。因为我们知道向量的运算不存在除法。假设最初找到的点为 ,要比较的两个点为 和 。
由于点 是最下方、最左方的点,所以向量 和 的极角大小在 区间,因此我们可以直接用 的正负来进行比较(当然在做半平面交的时候极角排序需要先判断向量极角大小所在的半平面)。
然后问题就是判断凸包的凸性了。当然记录两个向量的正切值也是可以的,不过一样有精度误差。
所以说我们也需要用向量的运算来判断凸包的凸性。
设加入的点为 ,凸包上后面两个点(也就是栈顶的两个点)为 和 ,记 。
我们发现,若 ,则凸包满足凸性,不需要退栈;若 ,则凸包不满足凸性,需要退栈;若 ,则三点共线,退不退栈均可。
代码(建议使用较为宽的屏幕阅读):
#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
也能过
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· AI技术革命,工作效率10个最佳AI工具