参考资料:
https://oi.wiki/geometry/half-plane/#%E7%BB%B4%E6%8A%A4%E5%8D%95%E8%B0%83%E9%98%9F%E5%88%97
半平面交的模板
- 将线段按照 (以:x轴的夹角为第一关键字, 如果角度相等,那么把两条线段中更靠左的排前面) 进行排序
- 将排好序的线段正序放入到队列中, 同时进行
维护(重点!!!)
- 将合法的线段插入到队列后,最后用队首的向量排除一下队尾多余的向量。因为队首的向量会被后面的约束,而队尾的向量不会。此时它们围成了一个环,因此队首的向量就可以约束队尾的向量
- 得到由多条线段包围的半平面交
- 求出它们的交点,然后固定一个点,旋转两个点,即可得到内部的面积之和,要注意的是area函数求出的平行四边形的面积,我们要的三角形的面积,因此答案 / 2
(1)为什么要使用:双端队列的原因:
因为后来加入的只可能会影响最开始加入的或最后加入的边(此时凸壳连通),只需要删除队首和队尾的元素,所以需要用单调队列
(2)on_right函数的判断实质
详细解释请看注释
#include <bits/stdc++.h>
using namespace std;
#define x first
#define y second
typedef pair<double, double> PDD;
const int N = 510;
const double eps = 1e-8;
int cnt;
struct Line //存线段
{
PDD st, ed; //起点,终点
} line[N];
PDD pg[N], ans[N];
int q[N]; //双端队列
int sign(double x) //符号函数
{
if (fabs(x) < eps)
return 0;
if (x < 0)
return -1;
return 1;
}
int dcmp(double x, double y) //判断相等
{
if (fabs(x - y) < eps)
return 0;
if (x < y)
return -1;
return 1;
}
double get_angle(const Line &a) //该向量对于 x轴的角度
{
// atan2(y, x) = 角度
return atan2(a.ed.y - a.st.y, a.ed.x - a.st.x);
}
PDD operator-(PDD a, PDD b)
{
return {a.x - b.x, a.y - b.y};
}
double cross(PDD a, PDD b) //叉积
{
return a.x * b.y - a.y * b.x;
}
double area(PDD a, PDD b, PDD c) //求(ab) (ac)两个向量的平行四边形的面积
{
return cross(b - a, c - a);
}
bool cmp(const Line &a, const Line &b) //按关于x轴的角度排序
{
double A = get_angle(a), B = get_angle(b);
if (!dcmp(A, B)) //如果角度不等
return area(a.st, a.ed, b.ed) < 0; //将更高左的排到前面
return A < B;
}
PDD get_line_intersection(PDD p, PDD v, PDD q, PDD w) //得到两直线的交点
{
auto u = p - q;
double t = cross(w, u) / cross(v, w);
return {p.x + v.x * t, p.y + v.y * t};
}
PDD get_line_intersection(Line a, Line b) //将向量转化为 点向式
{
return get_line_intersection(a.st, a.ed - a.st, b.st, b.ed - b.st);
}
bool on_right(Line &a, Line &b, Line &c) // bc的交点是否在a的左侧
{
auto o = get_line_intersection(b, c);
return sign(area(a.st, a.ed, o)) <= 0;
}
double half_plane_intersection()
{
sort(line, line + cnt, cmp); //按角度排序
int hh = 0, tt = -1;
for (int i = 0; i < cnt; i++)
{
if (i and !dcmp(get_angle(line[i]), get_angle(line[i - 1]))) //如果角度相同则跳过
continue;
/*
注意事项:
当出现一个可以把队列里的点全部弹出去的向量(即所有队列里的点都在该向量的右侧),
则我们 必须 先处理队尾,再处理队首。因此在循环中,我们先枚举 --r; 的部分,再枚举 ++l; 的部分,才不会错。原因如下
*/
while (hh + 1 <= tt && on_right(line[i], line[q[tt - 1]], line[q[tt]]))
tt--;
while (hh + 1 <= tt && on_right(line[i], line[q[hh]], line[q[hh + 1]]))
hh++;
q[++tt] = i;
}
//队尾更新队头
while (hh + 1 <= tt && on_right(line[q[hh]], line[q[tt - 1]], line[q[tt]]))
tt--;
//队头更新队尾
while (hh + 1 <= tt && on_right(line[q[tt]], line[q[hh]], line[q[hh + 1]]))
hh++;
q[++tt] = q[hh]; //闭合
int k = 0;
for (int i = hh; i < tt; i++)
ans[k++] = get_line_intersection(line[q[i]], line[q[i + 1]]); //最后得到的多条直线的交点即是所包围的面积
double res = 0;
for (int i = 1; i + 1 < k; i++)
res += area(ans[0], ans[i], ans[i + 1]);
return res / 2; //三角剖分,但实际上area是平行四边形的面积,因此 / 2
}
int main()
{
int n, m;
scanf("%d", &n);
while (n--)
{
scanf("%d", &m);
for (int i = 0; i < m; i++)
scanf("%lf%lf", &pg[i].x, &pg[i].y); //每个点的坐标
for (int i = 0; i < m; i++)
line[cnt++] = {pg[i], pg[(i + 1) % m]}; //存线段
}
double res = half_plane_intersection();
printf("%.3lf\n", res);
return 0;
}