二维凸包

凸包

形象地说,你现在有一块洞洞板,大概就是这样:

现在你要在上面插上很多木棍,比如说这样:

那么如果给这些木棍绷上橡皮筋,就会得到:

这就是凸包。

更正经地说,就是把这个洞洞板看成一个二维平面,那么凸包就是这个平面上能够覆盖已知的 \(n\) 个点的最小凸多边形。

注:凸多边形指的是所有内角都小于 \(180\) 度的多边形。

怎么求凸包

这里介绍最喜欢的 \(Graham\) 扫描法。

前置知识:向量的叉乘。

简单说明 \(Graham\) 扫描法要用到的叉乘的结论就是两条线段 \(p1\)\(p2\) 叉乘的结果 \(mul\) 大于零,那么 \(p2\)\(p1\) 的逆时针方向;若小于零,则在顺时针方向;特别地,若 \(mul\) 等于零,则 \(p1\)\(p2\) 在同一直线上。

如果想要深入了解向量,可以戳这里

至于叉积的计算:

double multi(node d1,node d2,node d3,node d4){
	return (d2.x-d1.x)*(d4.y-d3.y)-(d4.x-d3.x)*(d2.y-d1.y);
}

其中 \(d1\)\(d2\) 是线段 \(p1\) 的两端点,同理 \(d3\)\(d4\) 是线段 \(p2\) 的两端点。

前置知识讲完了,现在切入正题。

对于一个合格的凸包,很显然凸包上的任意一个点所连接的下一个点必然是向左拐的最靠右的一条。很显然右拐的边是不可能构成凸包的,因为它不凸,比如说下图中红色的边很显然比舍去中间那个点更劣:

而对于图中红色的点,很显然红色的线是最有可能成为凸包的众多边中的一条的:

所以 \(Graham\) 扫描法将从最下端的一个点开始,每次尝试将当前边加入,并删除相对更加不可能构成凸包的那些边。

下面将演示 \(Graham\) 扫描法构建凸包的过程。

首先对于这个例子,先找到最下端的点 \(p_{min}\),也就是纵坐标最小的一个点:

bool cmp1(node d1,node d2){
	return d1.y<d2.y||d1.y==d2.y&&d1.x<d2.x;
}

接着根据前面推出的小小结论,按照每个点和 \(p_{min}\) 构成的线段的方向排序,更加靠右的排在前面。特别地,如果共线则距 \(p_{min}\) 更近的排在前面:

bool cmp2(node d1,node d2){
	double mul=multi(p[1],d1,p[1],d2);
	return mul>0||mul==0&&dis(p[1],d1)<dis(p[1],d2);
}

这一步貌似不太好画图,可以自己意会一下(雾)。

排完序之后的顺序长这样:

数字的颜色不太好辨认,但是懒得改了。

然后按排好的顺序遍历每个点,将它和当前在凸包里的最后一个节点连边,依次删除不能要的边。以当前点是图中黑色点为例,尝试向半成品凸包连接这条红色的边:

发现与上图中绿色的边叉积小于零,也就表示它在顺时针方向,即右拐了。

所以将绿色的边删除,加入一条黄色的新边代替红绿两条边:

一直重复这一步,就可以构成一个封闭的凸多边形,即我们需要寻找的凸包。

for(int i=2;i<=n;i++){
		while(tp>1&&multi(sp[tp-1],sp[tp],sp[tp-1],p[i])<=0) tp--;
		sp[++tp]=p[i];
	}

当然,“删除不合格的边”的操作需要使用单调栈维护完成。

例题:洛谷 P2742 【模板】二维凸包 / [USACO5.1] 圈奶牛 Fencing the Cows

由题可知,二维凸包的板子。

周长就是将单调栈中剩下的点的距离加起来。

\(Code\)

#include<bits/stdc++.h>
using namespace std;
struct node{
	double x,y;
};
node p[100005];
double multi(node d1,node d2,node d3,node d4){
	return (d2.x-d1.x)*(d4.y-d3.y)-(d4.x-d3.x)*(d2.y-d1.y);
}
double dis(node d1,node d2){
	double lenx=d1.x-d2.x,leny=d1.y-d2.y;
	return sqrt(lenx*lenx+leny*leny);
}
bool cmp1(node d1,node d2){
	return d1.y<d2.y||d1.y==d2.y&&d1.x<d2.x;
}
bool cmp2(node d1,node d2){
	double mul=multi(p[1],d1,p[1],d2);
	return mul>0||mul==0&&dis(p[1],d1)<dis(p[1],d2);
}
node sp[1000050];
int tp=0;
int main(){
	int n;
	scanf("%d",&n);
	for(int i=1;i<=n;i++) scanf("%lf%lf",&p[i].x,&p[i].y);
	sort(p+1,p+n+1,cmp1);
	sort(p+2,p+n+1,cmp2);
	sp[++tp]=p[1];
	for(int i=2;i<=n;i++){
		while(tp>1&&multi(sp[tp-1],sp[tp],sp[tp-1],p[i])<=0) tp--;
		sp[++tp]=p[i];
	}
	sp[tp+1]=p[1];
	double len=0;
	for(int i=1;i<=tp;i++) len+=dis(sp[i],sp[i+1]);
	printf("%.2lf\n",len);
	return 0;
}

练习题

洛谷 P3829 [SHOI2012] 信用卡凸包

把所有圆视为坐标为圆心的点,不难发现长度就是凸包周长加上一个圆的长度。

\(Code\)

#include<bits/stdc++.h>
using namespace std;
const double eps=1e-8;
double a,b,r;
struct node{
	double x,y;
};
node p[40005];
int m=0;
node cal(double x,double y,double theta,int dx,int dy){
    double h=(a-2*r)/2.0,w=(b-2*r)/2.0,c=cos(theta),s=sin(theta);
    double px=dx*w,py=dy*h,nx=px*c-py*s,ny=px*s+py*c;
    return {x+nx,y+ny};
}
double multi(node d1,node d2,node d3,node d4){
	return (d2.x-d1.x)*(d4.y-d3.y)-(d4.x-d3.x)*(d2.y-d1.y);
}
double dis(node d1,node d2){
	double lenx=d1.x-d2.x,leny=d1.y-d2.y;
	return sqrt(lenx*lenx+leny*leny);
}
bool cmp1(node d1,node d2){
	return d1.y<d2.y||fabs(d1.y-d2.y)<eps&&d1.x<d2.x;
}
bool cmp2(node d1,node d2){
	double mul=multi(p[1],d1,p[1],d2);
	return mul>eps||fabs(mul)<=eps&&fabs(dis(p[1],d1)-dis(p[1],d2))<eps;
}
node sp[1000050];
int tp=0;
int main(){
	int n;
	scanf("%d%lf%lf%lf",&n,&a,&b,&r);
	for(int i=1;i<=n;i++){
		double x,y,theta;
		scanf("%lf%lf%lf",&x,&y,&theta);
		p[++m]=cal(x,y,theta,1,1),p[++m]=cal(x,y,theta,-1,1),p[++m]=cal(x,y,theta,1,-1),p[++m]=cal(x,y,theta,-1,-1);
	}
	sort(p+1,p+m+1,cmp1);
	sort(p+2,p+m+1,cmp2);
	sp[++tp]=p[1];
	for(int i=2;i<=m;i++){
		while(tp>1&&multi(sp[tp-1],sp[tp],sp[tp-1],p[i])<=eps) tp--;
		sp[++tp]=p[i];
	}
	sp[tp+1]=sp[1];
	double len=0;
	for(int i=1;i<=tp;i++) len+=dis(sp[i],sp[i+1]);
	len+=2*acos(-1.0)*r;
	printf("%.2lf\n",len);
	return 0;
}

\(-\) ❀ 完结撒花 ❀ \(-\)

posted @ 2026-04-11 15:40  Rye-Whiskey  阅读(23)  评论(0)    收藏  举报