Loading

半平面交

前言

由于笔者对半平面交算法的很多数学细节不是非常清楚,所以这篇博客可能会有描述不到位或者错误的地方。欢迎读者向笔者指出文章中的不足,也希望初学半平面交的同学不要用这篇博客来入门,以免产生了一些错误的理解,对计算几何之后的学习产生了影响。

前置知识

我们知道一条直线可以将一个平面分成两部分,我们称其中的一部分为 半平面 。半平面是一个 点集 ,也就是由直线和直线的一侧构成的点集。当半平面包含这条直线时,我们称其为 闭半平面,反之称其为 开半平面。半平面在计算几何中用 向量 来表示,且统一以向量的某一侧为半平面。

多个半平面的交集称之为 半平面交。半平面交仍然是一个点集,它在平面直角坐标系中围成了一个区域。如果把多边形的边看作是首尾相连的向量,那么这些向量在多边形内侧方向的半平面交称之为 多边形的核

接下来引入和 极角排序 有关的概念。在平面上任取一点 \(O\),取其为 极点。再引出一条射线 \(Ox\) ,称为 极轴。设平面上有任意一点 \(M\),令线段 \(OM\) 的长度为 \(p\)\(\theta\) 表示 \(Ox\)\(OM\) 的夹角。我们称 \(p\) 是点 \(M\) 的极径,\(\theta\) 是点 \(M\) 的极角。我们在极角排序中取极轴为 \(x\) 轴,这样我们就可以把平面上的若干个点按照与 \(x\) 轴的夹角排序。具体实现可以使用 C 语言中 math.h 库里的 atan2 函数,调用格式为 atan2(double y, double x) ,其中 y 表示已知点的 \(y\) 坐标,x 同理。这个函数的返回值是这个点与远点连边后,这条连边与 \(x\) 轴正方向的夹角。

\(S\&I\) 算法

假设给定了多边形的顶点坐标,需要求出这个多边形的核。假设给定了下图中的多边形。

多边形

我们可以先处理出这个多边形的所有边,我们定义 逆时针方向 为正方向,然后把这些边极角排序。这样做可以方便后面双端队列的处理。

极角排序

我们可以先假定这个平面上所有的面积都属于半平面交,然后考虑多边形的边对于半平面交的影响。我们可以把多边形的边转化成若干向量,然后对于这些向量的左侧取交集。我们用一个双端队列来维护对当前的半平面交有影响的边,同时维护队列中相邻边的交点。

设双端队列中的第 \(k\) 条边为 \(q_k\) ,且当前共有 \(m\) 条边。对于当前需要加入的边 \(p_i\) 和上一次的交点 \(t\) ,如果 \(p\)\(t\) 的逆时针方向,说明当前的边 \(p\) 对于半平面交有影响,而构成上一次的交点的连边,也就是 \(q_{m - 1}\) 对半平面交没有影响,所以需要弹出。

当半平面交处理到最后几条边的时候,这些边可能会覆盖掉双端队列开头的影响。这时我们还需要判断是否需要弹出开头的边,例如下图中的情况:

当编号为 \(8\) 的边加入双端队列以后,编号为 \(1\) 的边对半平面交就没有影响了。因为边已经极角排序过了,所以此时编号为 \(1\) 的边应该在双端队列开头。因此我们需要比较队尾和开头,决定是否需要将开头弹出。另外,我们应该先处理队尾,再处理开头。我们可以感性理解一下:因为先处理开头可能会导致合法的范围扩大,因而原本对半平面交没有影响的边也会加入双端队列。因为队头被其后面的元素约束,队尾则没有,所以队尾应该优先考虑。

如果出现两条平行线,我们应该取在另一条平行线逆时针方向的平行线。因为我们取的是向量的左侧,所以这样做显然不会破坏算法的正确性。

最后,我们应该还要用队首来过滤掉队尾的不合法元素。假如队尾的元素在队首的右侧,那么队尾的元素就是没有影响的。因为我们对边进行过极角排序,所以可能会出现环。

当多边形的核的边数 \(\geq 3\) 时,我们就可以求出这个多边形的核的面积了。具体实现我们可以利用叉积的几何意义,因为笔者也不太熟悉叉积,因此无法对原理进行解释。具体请见代码注释。

参考代码

例题链接

#include <cstdio>
#include <cmath>
#include <algorithm>
using namespace std;

const int maxn = 1e3 + 5;
const double eps = 1e-10;

int n, m, cnt;
int st, en;
double ans;

struct vec // 定义向量和基本运算 
{
	double x, y;
	vec(): x(), y() {}
	vec(double _x, double _y): x(_x), y(_y) {}
	vec operator + (const vec& rhs) const
	{
		return vec(x + rhs.x, y + rhs.y);
	}
	vec operator - (const vec& rhs) const
	{
		return vec(x - rhs.x, y - rhs.y);
	}
	vec operator += (const vec& rhs)
	{
		this -> x += rhs.x;
		this -> y += rhs.y;
	}
	vec operator -= (const vec& rhs)
	{
		this -> x -= rhs.x;
		this -> y -= rhs.y;
	}
	vec operator * (const double& rhs) const
	{
		return vec(x * rhs, y * rhs);
	}
	vec operator / (const double& rhs) const
	{
		return vec(x / rhs, y / rhs);
	}
	vec operator *= (const double& rhs)
	{
		this -> x *= rhs;
		this -> y *= rhs;
	}
	vec operator /= (const double& rhs)
	{
		this -> x /= rhs;
		this -> y /= rhs; 
	}
} a[maxn], t[maxn]; // a ->读入的若干多边形的顶点,t ->半平面交中相邻边的交点 

struct line // 定义多边形的边
{
	vec a, b;
	double k;
	line(): a(), b() {}
	line(vec _a, vec _b): a(_a), b(_b) {k = atan2(b.y, b.x);} // k ->极角 
	bool operator < (const line& rhs) const // 极角排序 
	{
		return k < rhs.k;
	}
} p[maxn], q[maxn]; // p ->若干多边形的边,q ->双端队列 

double cross(vec a, vec b) // 计算叉积 
{
	return a.x * b.y - b.x * a.y;
}

vec get(line a, line b) // 计算边 a 和边 b 的交点 
{
	vec c = a.a - b.a;
	double t = cross(b.b, c) / cross(a.b, b.b);
	return a.a + a.b * t; 
}

void work() // 求半平面交 
{
	st = en = 1;
	q[st] = p[1];
	for (int i = 2; i <= cnt; i++)
	{
		while (st < en && cross(p[i].b, t[en - 1] - p[i].a) <= eps) // 当前边是否在队尾交点的逆时针方向 
			en--;
		while (st < en && cross(p[i].b, t[st] - p[i].a) <= eps) // 当前边是否在队首交点的逆时针方向 
			st++;
		q[++en] = p[i];
		if (fabs(cross(q[en].b, q[en - 1].b)) <= eps) // 判断平行线的情况 
		{
			en--;
			if (cross(q[en].b, p[i].a - q[en].a) > eps) // 当前边在平行线的逆时针方向(合法) 
				q[en] = p[i];
		}
		if (st < en) // 如果边数 > 1 则加入交点 
			t[en - 1] = get(q[en - 1], q[en]);
	}
	while (st < en && cross(q[st].b, t[en - 1] - q[st].a) <= eps) // 用队首来过滤队尾 
		--en;
	if (en - st <= 1) // 半平面交是否存在面积 
		return;
	t[en] = get(q[st], q[en]);
}

int main()
{
	scanf("%d", &n);
	for (int i = 1; i <= n; i++)
	{
		scanf("%d", &m);
		for (int j = 1; j <= m; j++)
			scanf("%lf%lf", &a[j].x, &a[j].y);
		for (int j = 1; j <= m; j++)
		{
			cnt++;
			int to = (j == m ? 1 : j + 1);
			p[cnt] = line(a[j], a[to] - a[j]);
		}
	}
	sort(p + 1, p + cnt + 1);
	work();
	for (int i = st; i <= en; i++)
	{
		int to = (i == en ? st : i + 1);
		ans += cross(t[i], t[to]); // 叉积求半平面交面积 
	}
	ans /= 2;
	printf("%.3lf\n", ans);
	return 0;
}

posted @ 2021-07-24 23:46  kymru  阅读(314)  评论(0编辑  收藏  举报