计算几何之 半平面交 代码模板及过程证明

半平面交定义

首先要了解半平面交是什么,简单来讲,就是一堆直线,我们只取他的一边,所构成的区域就是半平面交。比如,有4条有向直线,我们都只取直线左边的那一部分,所有直线左边平面的交集就是半平面交。如下图所示,蓝色区域就是四条直线左边平面的交集:
eg

半平面交求法

了解了什么是半平面交后,我们看一下他的求法。和求凸包一样,分为两步。

第一步: 将所有直线按倾角从小到大排序
第二步: 从小到大枚举每条边来维护一个双端队列。

看着很简单,但是如何维护每条边呢?
如下图,将队尾的边记作b,b的上一条边记作c,新边记作a,如果b与c的交点在直线a的右侧,那就删去队尾的边b,然后再将新边a入队;如果在左侧,就说明队尾的边也是我们要留的边,那就不用出队,直接将新边入队;如果交点就在直线a上,说明交点重合了,这就具体看题目要求删不删重复的边。注意这里是while操作,如果队尾出队了,我们要判断新的队尾满不满足要求,直到满足为止。(和求凸包类似)
注:以下只是假设的情况
eg

我们这里维护的是双端队列,所以每次都要对队尾和队首都判断交点在其左侧还是右侧,因为当我们转一圈过来后,就要判断是否保留队首的直线了,如下图所示:
eg
假设我们是从直线b开始维护的,b就是队首,c是队首的下一条边,a是新的边,新的边也要和队首的边判断,同样的判断:判断b和c的交点是否在a的右侧,是的话就删去队首的边(图中的情况);不是的话就不删,保留队首。(我们对队尾和队首都要维护,所以用的是双端队列)

记得循环完后,一定要再判断一下队首的边和队尾的两条边的交点,看看边界是否满足半平面交的要求(类比求凸包),否则可能会出现如下情况:
wr
如果只经过刚才队首队尾的维护,队尾的这条红边最后也会入队,就成了如上情况,但明显红边是不对的,所以最后我们还需要判断一下队首和队尾的两条边,来避免这种边界情况。

当我们维护完所有边后,队列中的边就是我们需要的边。但是可能有些人还有一些疑问,如何求直线的交点:基础知识中有,这里直线都用点向式存。如何判断点在直线的右侧还是左侧?向量的叉积法,也在基础知识中有。如何求直线的倾角?用atan2()这个函数,用atan()求倾角时,若直线是竖直的,则tan不存在,而atan2函数是arctan的一个封装,解决了竖直直线的情况,但要注意他的参数。
具体可以看看代码来理解每一步。

代码模板

模板题:Acwing 2803:凸多边形
只需要算所有多边形的所有边的半平面交即可

#include<iostream>
#include<cmath>
#include<algorithm>
using namespace std;
const int N = 510;
const double eps = 1e-8;
typedef pair<double,double> PDD;	//用pair来存点
struct Line{		//定义Line结构体存直线
	PDD st,ed;	//存直线上的两个点-起点和终点,虽然存的是两个点,但是是点想法表示的直线,起点就是点,终点-起点就是直线的向量
}line[N];		//line数组存直线

PDD p[N],ans[N];	//p存所有点,ans存最终半平面交的所有交点
int n,m,cnt,q[N];	//q是用来维护的双端队列,手动建双端队列

int judge(double a,double b)	//判断两个浮点数的大小,相等返回0,小于返回-1,大于返回1
{
	if(fabs(a-b) < eps)
		return 0;
	if(a < b)
		return -1;
	return 1;
}

PDD operator-(PDD a,PDD b)	//重载运算符减号,可以直接用pair相减
{
	return {a.first-b.first , a.second-b.second};
}

double angle(const Line& a)	//求一个直线角度的函数
{
	return atan2(a.ed.second-a.st.second , a.ed.first-a.st.first);	//注意atan2函数的参数分别为纵坐标y和横坐标x,这里xy是直线从原点出发的向量横纵坐标
}

double cross(PDD a,PDD b,PDD c)		//求向量b-a和c-a叉积的函数,参数是点,方便调用
{
	PDD u,v;
	u = b - a;	//两向量
	v = c - a;
	return u.first*v.second - v.first*u.second;	//叉积 = x1*y2 - x2*y1
}

bool cmp(const Line& a,const Line& b)		//比较函数,用来对直线按倾角排序
{
	double A = angle(a),B = angle(b);	//求出直线a、b的倾角
	if(judge(A,B) == 0)		//如果A和B相等的话
		return cross(a.st,a.ed,b.ed) < 0;	//将直线a、b按从左到右的顺序排,相同角度的直线只需要最左边的就行
	return A < B;	//按角度从小到大排
}

PDD intersection(const Line& a,const Line& b)	//求两直线交点的函数,基础知识里都有
{
	PDD p,q,w,v,u;
	p = a.st,q = b.st,v = a.ed - a.st,w = b.ed - b.st;	//正常的板子求法
	u = p - q;
	double t = cross({0,0},w,u) / cross({0,0},v,w);
	return {p.first+t*v.first , p.second+t*v.second};	//先将两条直线转换到板子需要的东西,然后按板子求就行,不懂得可以看一下基础知识的板子
}

bool onright(const Line& a,const Line& b,const Line& c)		//判断b、c的交点是不是在直线a的右边
{
	PDD f = intersection(b,c);	//先求出直线b、c的交点f
	return cross(a.st,a.ed,f) < 0;	//然后判断交点f是不是在a的右边,叉积小于0说明在右边
}

double half_plane_intersection()	//半平面交的函数
{
	sort(line,line+cnt,cmp);	//第一步先排序
	int hh = 0,tt = -1;		//手建头尾
	for(int i = 0;i < cnt;i++)	//循环每条边
	{
		if(i && judge(angle(line[i]),angle(line[i-1])) == 0)	//如果当前直线和上一条直线的角度相同就跳过,因为我们已经在cmp中将相同角度的直线按从左到右的顺序排过了,相同角度的直线只取最左边的就行,其余的都没用
			continue;
		while(hh+1 <= tt && onright(line[i],line[q[tt-1]],line[q[tt]]))	//维护队尾,当队列元素大于等于2,且队尾两直线的交点在新直线的右边时
			tt--;	//队尾出队
		while(hh+1 <= tt && onright(line[i],line[q[hh]],line[q[hh+1]]))	//同理维护队首,注意是while
			hh++;	//队首出队
		q[++tt] = i;	//新直线入队
	}
	while(hh+1 <= tt && onright(line[q[hh]],line[q[tt-1]],line[q[tt]]))
		tt--;		//最后判断边界问题的,就是上述的最后一种情况
	q[++tt] = q[hh];	//将队首入队尾,方便下一行求最开始的第一个交点
	int k = 0;
	for(int i = hh;i < tt;i++)	//循环队列中的每一条直线
		ans[k++] = intersection(line[q[i]],line[q[i+1]]);	//将相邻两条直线的交点求出并记入ans数组,队首和队尾的交点也在其中
	double area = 0;	//面积
	for(int i = 1;i < k-1;i++)	//循环ans数组中的所有点
		area += cross(ans[0],ans[i],ans[i+1]);	//叉积计算面积
	return area / 2;	//面积除以2
}

int main()
{
	cin >> n;	//n个凸多边形
	while(n--)
	{
		cin >> m;	//m条边
		for(int i = 0;i < m;i++)
			cin >> p[i].first >> p[i].second;	//将所有点存进p数组
		for(int i = 0;i < m;i++)
			line[cnt++] = {p[i],p[(i+1)%m]};	//将所有边存进line数组
	}
	double ans = half_plane_intersection();		//求半平面交
	printf("%.3lf\n",ans);		//输出面积

	return 0;
}

求半平面交虽然看着比较复杂,函数比较多,但其实理解了每一步后其实就是个套路,要想理解好还得多看。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值