UESTC 2022ACM暑假集训数学与几何专题题解(计算几何部分)
C 回转数
方法
1.角度(TLE)
- atan2:计算一个点 \(i\) 到另一点 \(j\) 相对于"原点" \(O\) 转过的角度,可以通过\(atan2(y,x)\)分别算出两点角度然后相减得到转过的角度 \(p\) 。
struct VEC
{
int x, y;
} vec[N];
dbl arg(VEC v) //这里返回的角度范围为[0,2pi)
{
dbl r = atan2(v.y, v.x);
return r < 0 ? 2 * pi + r : r;
}
- 特判1——EDGE:根据叉积与点积判断:叉积=0且点积>0则两个向量共线反向——即原点在某条边上。
if (1ll * vec[j].x * vec[j - 1].y - 1ll * vec[j].y * vec[j - 1].x == 0 && 1ll * vec[j].x * vec[j - 1].x <= 0) //判定有无过“原点”的线段
{
......
}
- 特判2——旋转一圈:旋转角度范围在 \((-pi,pi)\) ,不可能出现转的角比平角还大的情况(这时候就是反方向转一个在范围内的角度来达到)。
dbl tmp = arg(vec[j]) - arg(vec[j - 1]);
if (tmp < -pi)
tmp += 2 * pi;
else if (tmp > pi)
tmp -= 2 * pi;
- 复杂度:\(O(nm)\) 。遍历m个询问点,n个折线上的点都是线性的。
- 本方法的缺陷:double运算速度比int慢,被卡常TLE。
- 改进方案:换个方法。
- 备注:用回转数判断一个点是否在多边形内部时,用角度来算应该不会卡常,所以把原先的方法也留着了。
2.象限跨越(AC)
- 思路:不使用角度,在仅使用整型运算的情况下可以通过判断一个点 \(i\) 到另一点 \(j\) 跨越象限的情况判断旋转方向与旋转程度
自己定义,如第一象限-第二象限这一步为1,那么旋转程度为4(例如一-二-三-四)时转了一圈
- 问题:如果点移动到坐标轴上,难以判断旋转程度;
- 改进方案1(失败):由于点坐标都是整点,所以可以将坐标轴旋转一个极小的角度,来使得新坐标轴不经过任何一个点,即原坐标轴上的点可以分类并入四个象限。
- 方案1的缺陷:情况种类多(首先是两种旋转方向,其次在每种旋转方向下象限跨越方式都要找对应的“旋转程度”),整形运算时间优势被情况数量的常数带来的时间劣势抵消了,依然会TLE,且代码会写成屎山(几十个判断)。
- 改进方案2(可行):观察方案一,\(x、y\) 与0的关系分别出现了四种情况,导致新坐标轴判断情况增加。可以换一个更优的情况合并方式,使得\(x、y\) 与0的关系分别只有两种情况(和原坐标轴一样)。
- 这样修改可以让情况变少,并且将 \(x,y\) 的相似情况合并——可以使用一个函数判断 \(x,y\) 与新坐标轴的关系,而不是用一长串判断语句。同时设两个系数函数,将旋转方向系数和旋转程度系数分开考虑。旋转方向系数根据叉积判断,旋转程度系数判断方式:一个点 \(i\) 到另一点 \(j\) 的横纵坐标每当有一个“变号”,旋转程度系数就+1。两个系数的乘积就是点 \(i\) 到点 \(j\) 的旋转程度。
int zhou(int t) //第一象限:x>0,y>0;第二象限:x<=0,y>0;第三象限:x<=0,y<=0;第四象限:x>0,y<=0;
{
return t > 0 ? 1 : -1;
}
int coe1(VEC v1, VEC v2) //根据是否跨越象限判断系数
{
int t = 0;
if (zhou(v1.x) * zhou(v2.x) < 0)
t++;
if (zhou(v1.y) * zhou(v2.y) < 0)
t++;
return t;
}
int coe2(VEC v1, VEC v2) //根据叉积判断方向
{
if (1ll * v1.x * v2.y - 1ll * v1.y * v2.x == 0)
return 0;
else if (1ll * v1.x * v2.y - 1ll * v1.y * v2.x > 0)
return 1;
else
return -1;
}
- 特判EDGE:与角度法相同,略。
- 复杂度:\(O(nm)\).
代码
#include <bits/stdc++.h>
using namespace std;
#define ll long long
struct Seg
{
int x, y;
} seg[5005];
struct Node
{
int x, y;
} node[5005];
struct VEC
{
int x, y;
} vec[5005];
int zhou(int t) //第一象限:x>0,y>0;第二象限:x<=0,y>0;第三象限:x<=0,y<=0;第四象限:x>0,y<=0;
{
return t > 0 ? 1 : -1;
}
int coe1(VEC v1, VEC v2) //根据是否跨越象限判断系数
{
int t = 0;
if (zhou(v1.x) * zhou(v2.x) < 0)
t++;
if (zhou(v1.y) * zhou(v2.y) < 0)
t++;
return t;
}
int coe2(VEC v1, VEC v2) //根据叉积判断方向
{
if (1ll * v1.x * v2.y - 1ll * v1.y * v2.x == 0)
return 0;
else if (1ll * v1.x * v2.y - 1ll * v1.y * v2.x > 0)
return 1;
else
return -1;
}
int main()
{
int n;
scanf("%d", &n);
for (int i = 1; i <= n; i++)
{
scanf("%d%d", &seg[i].x, &seg[i].y);
}
int m;
scanf("%d", &m);
for (int i = 1; i <= m; i++)
{
scanf("%d%d", &node[i].x, &node[i].y);
int ans = 0;
int flag = 0;
for (int j = 1; j <= n + 1; j++)
{
vec[j].x = seg[j].x - node[i].x;
vec[j].y = seg[j].y - node[i].y;
vec[n + 1] = vec[1];
if (j >= 2)
{
if (1ll * vec[j].x * vec[j - 1].y - 1ll * vec[j].y * vec[j - 1].x == 0 && 1ll * vec[j].x * vec[j - 1].x <= 0) //先判定有无过“原点”的线段
{
flag = 1;
break;
}
ans += coe1(vec[j - 1], vec[j]) * coe2(vec[j - 1], vec[j]);
}
}
if (flag == 0)
{
ans = ans / 4;
printf("%d\n", ans);
}
else
printf("EDGE\n");
}
return 0;
}
E 兔儿爷(判断点在三角形内)
思路
题意转化:平面直角坐标系中给出三点 \((a_{1},b_{1}),(a_{2},b_{2}),(a_{3},b_{3})\) ,给出m个点 \((a,b)\) ,判断点 \((a,b)\) 是否在点 \((a_{1},b_{1}),(a_{2},b_{2}),(a_{3},b_{3})\) 组成的三角形内(或三角形上)。
思路1:回转数法判断点在多边形内部。(wa5不知道挂在哪没调出来,因为用了atan2函数以为是double精度问题就决定换做法)
思路2:判断点在直线某一侧。
由于数据范围小,而且已经确定了是三角形,所以才想到使用这种方法来判断。
假设点 \(x_{1}(a_{1},b_{1}),x_{2}(a_{2},b_{2}),x_{3}(a_{3},b_{3})\) 组成三角形(无共线),那么三角形就有两种情况:\(x_{1},x_{2},x_{3}\) 是逆时针顺序的(或者是顺时针顺序)。这里可以用叉积判断。
进一步假设 \(x_{1},x_{2},x_{3}\) 是逆时针顺序的(即向量 \(x_{3}-x_{2}\) 相对于向量 \(x_{2}-x_{1}\) 是逆时针的),那么考虑判断点在直线某侧的算法——在直线上选取一个点向 \(y(a,b)\) 作向量并与直线=的方向向量作叉积。此处点少并已经确定,那么以三角形的一条边、向量 \(x_{2}-x_{1}\) 为例:要使点 \(y\) 在三角形内,那么向量 \(y-x_{1}\) 相对于向量 \(x_{2}-x_{1}\) 应该是逆时针的(或者重合就在三角形上)。如果三条边都满足这样的情况那么点就在三角形内。这里也可以用叉积判断。
而如果 \(x_{1}(a_{1},b_{1}),x_{2}(a_{2},b_{2}),x_{3}(a_{3},b_{3})\) 不能够组成三角形,即向量 \(x_{3}-x_{2}\) 与向量 \(x_{2}-x_{1}\) 叉积为0。此时可能出现点的重合情况,但是不需要特判。此时问题就转化为点在线段上。将三个点按照 \(a,b\) 坐标排序,求 \(y\) 与两个点连向量的点积即可知道 \(y\) 是否在线段中间,同时判断一下 \(y\) 与两个点连向量的叉积(判断 \(y\) 是否共线)。
复杂度:\(O(m)\) 。
代码
#include <bits/stdc++.h>
using namespace std;
struct VEC
{
int a, b;
VEC(int X = 0, int Y = 0) { a = X, b = Y; }
} x[5], y[100005], vec[3], ck[3];
VEC operator-(VEC v1, VEC v2) { return VEC(v1.a - v2.a, v1.b - v2.b); }
bool operator==(VEC v1, VEC v2) { return v1.a == v2.a && v1.b == v2.b; }
bool operator!=(VEC v1, VEC v2) { return !(v1.a == v2.a && v1.b == v2.b); }
int cj(VEC v1, VEC v2)
{
if (v1.a * v2.b - v1.b * v2.a == 0)
return 0;
else if (v1.a * v2.b - v1.b * v2.a > 0)
return 1;
else
return -1;
}
int dj(VEC v1, VEC v2)
{
return v1.a * v2.a + v1.b * v2.b;
}
int cmp(VEC v1, VEC v2)
{
if (v1.a * v2.b - v1.b * v2.a == 0)
return dj(v1,v1) < dj(v2,v2);
return v1.a * v2.b - v1.b * v2.a > 0;
}
int main()
{
scanf("%d%d", &x[1].a, &x[1].b);
scanf("%d%d", &x[2].a, &x[2].b);
scanf("%d%d", &x[3].a, &x[3].b);
sort(x + 1, x + 4, cmp);
int n;
scanf("%d", &n);
for (int i = 1; i <= n; i++)
{
scanf("%d%d", &y[i].a, &y[i].b);
int flag = 0;
if (cj(x[3] - x[2], x[2] - x[1]) == -1)
{
if (cj(x[2] - x[1], y[i] - x[1]) >= 0 && cj(x[3] - x[2], y[i] - x[2]) >= 0 && cj(x[1] - x[3], y[i] - x[3]) >= 0)
flag = 1;
}
else if (cj(x[3] - x[2], x[2] - x[1]) == 1)
{
if (cj(x[2] - x[1], y[i] - x[1]) <= 0 && cj(x[3] - x[2], y[i] - x[2]) <= 0 && cj(x[1] - x[3], y[i] - x[3]) <= 0)
flag = 1;
}
else
{
if (cj(x[3] - y[i], y[i] - x[1]) == 0 && dj(x[3] - y[i], y[i] - x[1]) >= 0)
flag = 1;
if (cj(x[2] - y[i], y[i] - x[1]) == 0 && dj(x[2] - y[i], y[i] - x[1]) >= 0)
flag = 1;
if (cj(x[3] - y[i], y[i] - x[2]) == 0 && dj(x[3] - y[i], y[i] - x[2]) >= 0)
flag = 1;
}
if (flag)
printf("YES\n");
else
printf("NO\n");
}
return 0;
}
G 数三角形
思路
前情提要:由于C题被卡了double,所以G题也全用的整型运算。据lsr说他用的double运算AC了但是在网上找的一个标程对拍时,20个点以上就出现问题。说明应该是卡了double精度但没完全卡double。
暴力方法:枚举三角形三个点,复杂度 \(O(n^{3})\) ,会TLE。(一开始以为“不同的锐角三角形”要去掉全等三角形,就以为只能暴力做)
优化方法:
- 判断角的贡献。总的三角形个数是 \(C_{n}^{3}\) 。钝角与直角的贡献是-1(一个直角/钝角对应一个直角/钝角三角形),平角的贡献是-0.5(平角正着反着都会被扫到,有重复)。
- 先遍历所有点依次作为原点,然后使用叉积极角排序,分上下平面排序后合并。
- 然后使用三个指针,前两个指针用于计算直角/钝角数量,最后一个指针用于计算平角。
- 为了防止不存在直角钝角导致指针多扫一圈,将第一个指针设为<90°的最大极角序,第二个指针设为>=180°的最小极角序
最后答案:
ans = 1ll * n * (n - 1) * (n - 2) / 6 - nobt - nline / 2;
复杂度:\(O(n^{2}logn)\)
代码
#include <bits/stdc++.h>
#include <map>
using namespace std;
#define ll long long
struct VEC
{
int x, y;
} vec[2005];
struct VECC
{
int x, y;
} vecc[4010];
int cf(VECC v1, VECC v2) //根据叉积判断方向
{
if (1ll * v1.x * v2.y - 1ll * v1.y * v2.x == 0)
return 0;
else if (1ll * v1.x * v2.y - 1ll * v1.y * v2.x > 0)
return 1;
else
return -1;
}
ll dot(VECC v1, VECC v2)
{
return 1ll * v1.x * v2.x + 1ll * v1.y * v2.y;
}
ll cross(VECC v1, VECC v2)
{
return 1ll * v1.x * v2.y - 1ll * v1.y * v2.x;
}
bool cmp(VECC v1, VECC v2)
{
if (cf(v1, v2) == 0)
return v1.x < v2.x;
else
return cf(v1, v2) > 0;
}
int main()
{
// freopen("1.in", "r", stdin);
int n;
scanf("%d", &n);
ll ans = 0;
for (int i = 1; i <= n; i++)
{
scanf("%d%d", &vec[i].x, &vec[i].y);
}
ll nobt = 0, nline = 0; //数非锐角
for (int i = 1; i <= n; i++) //枚举角的顶点A
{
//叉积极角排序
int tot = 0; //总点
int up = 0;
int down = 0;
int x0, y0;
for (int j = 1; j <= n; j++)
{
if (i != j)
{
x0 = vec[j].x - vec[i].x;
y0 = vec[j].y - vec[i].y;
if ((x0 >= 0 && y0 >= 0) || (x0 < 0 && y0 > 0)) //上半平面的点
{
up++;
tot++;
vecc[up].x = x0;
vecc[up].y = y0;
}
}
}
for (int j = 1; j <= n; j++)
{
if (i != j)
{
x0 = vec[j].x - vec[i].x;
y0 = vec[j].y - vec[i].y;
if ((x0 >= 0 && y0 < 0) || (x0 < 0 && y0 <= 0)) //下半平面的点
{
down++;
tot++;
vecc[tot].x = x0;
vecc[tot].y = y0;
}
}
}
assert(tot == n - 1);
sort(vecc + 1, vecc + up + 1, cmp);
sort(vecc + up + 1, vecc + tot + 1, cmp);
for (int i = tot + 1; i <= 2 * tot; i++) //补完(第四象限转到第一象限等
{
vecc[i].x = vecc[i - tot].x;
vecc[i].y = vecc[i - tot].y;
}
//
// printf("%d %d %d::\n", up, down, tot);
//
//枚举向量
int ab = 1;
int l, r, m;
l = r = m = 1;
while (ab <= tot) // ab转一圈时停止。
{
l = max(l, ab);
// 成正向锐角l++,共线且不是转过来一圈时l++,排除转过来一圈继续共线的情况
while ((cross(vecc[ab], vecc[l + 1]) > 0 && dot(vecc[ab], vecc[l + 1]) > 0) || (cross(vecc[ab], vecc[l + 1]) == 0 && dot(vecc[ab], vecc[l + 1]) > 0)) // l:小于90最大极角序
l++;
m = max(m, l + 1);
// 在l+1基础上只可能>=90,成直角钝角就m++
while (cross(vecc[ab], vecc[m]) > 0 && m + 1 <= ab + tot) // r:大于180最小极角序
m++;
r = max(r, m);
// 在m基础上只可能>=180,排除转过来一圈继续共线的情况
while (cross(vecc[ab], vecc[r]) == 0 && dot(vecc[ab], vecc[r]) < 0 && r + 1 <= ab + tot) // r:大于180最小极角序
r++;
nobt += m - l - 1;
nline += r - m;
// printf("%d %d\n", l, r);
ab++;
}
//
//
}
assert(nline % 2 == 0);
ans = 1ll * n * (n - 1) * (n - 2) / 6 - nobt - nline / 2;
printf("%lld", ans);
return 0;
}
K 神像(闵可夫斯基和)
思路
三角形重心满足条件:\((x,y)=(\frac{x_{1}+x_{2}+x_{3}}{3},\frac{y_{1}+y_{2}+y_{3}}{3})\)
三个祭坛 \(P_{1},P_{2},P_{3}\) 可以构造出一个神像 \(Q(x,y)\) 构建位置的范围。
由于祭坛都是凸多边形,刚好都是凸包,由此想到使用闵可夫斯基和——求三个凸包的闵可夫斯基和,即是 \((3x,3y)\) 的范围。
算法
- 求凸包(Graham扫描法):将所有点按极角序排序,用栈维护上下凸包。
- 求闵可夫斯基和:求两个凸包闵可夫斯基和,将两个凸包上所有点按极角排序(由于凸包本来就按顺序输入的,所以只要在循环里用一个叉积判一下两个凸包下一个点的极角序)然后向量加起来。三个凸包加两次即可。
- 复杂度: \(O(nlogn)\) 。
代码
#include <algorithm>
#include <cmath>
#include <cstdio>
#define dbl double
#define LL long long
#define Vector Point
using namespace std;
const int N = 5e4 + 3;
const dbl eps = 1e-8, Pi = acos(-1.0);
int dcmp(dbl a) { return a < -eps ? -1 : (a > eps ? 1 : 0); }
struct Point
{
dbl x, y;
Point(dbl X = 0, dbl Y = 0) { x = X, y = Y; }
bool operator<(const Point &O) const { return dcmp(x - O.x) ? x < O.x : y < O.y; }
};
dbl Dot(Vector a, Vector b) { return a.x * b.x + a.y * b.y; }
dbl Cro(Vector a, Vector b) { return a.x * b.y - a.y * b.x; }
Vector operator+(Vector a, Vector b) { return Vector(a.x + b.x, a.y + b.y); }
Vector operator-(Vector a, Vector b) { return Vector(a.x - b.x, a.y - b.y); }
Vector operator*(Vector a, dbl b) { return Vector(a.x * b, a.y * b); }
int pan_PL(Point p, Point a, Point b)
{ //【判断点P是否在直线AB上】
return !dcmp(Cro(p - a, p - b)) && dcmp(Dot(p - a, p - b)) < 0;
}
bool cmp1(Vector a, Vector b) { return a.x == b.x ? a.y < b.y : a.x < b.x; }; //按坐标排序
int ConvexHull(Point *P, int n, Point *cp)
{ //【Graham扫描法】求凸包
sort(P + 1, P + n + 1, cmp1);
int t = 0;
for (int i = 1; i <= n; ++i)
{ //下凸包
while (t > 1 && dcmp(Cro(cp[t] - cp[t - 1], P[i] - cp[t - 1])) <= 0)
--t;
cp[++t] = P[i];
}
int St = t;
for (int i = n - 1; i >= 1; --i)
{ //上凸包
while (t > St && dcmp(Cro(cp[t] - cp[t - 1], P[i] - cp[t - 1])) <= 0)
--t;
cp[++t] = P[i];
}
return --t; //要减一
}
inline int PIP(Point *P, int n, Point a)
{ //【二分法】判断点A是否在凸多边形Poly以内
//点按逆时针给出
if (dcmp(Cro(a - P[1], P[2] - P[1])) > 0 || dcmp(Cro(P[n] - P[1], a - P[1])) > 0)
return 0; //在P[1_2]或P[1_n]外
if (pan_PL(a, P[1], P[2]) || pan_PL(a, P[1], P[n]))
return 1; //在P[1_2]或P[1_n]上
int l = 2, r = n - 1;
while (l < r)
{ //二分找到一个位置pos使得P[1]_A在P[1_pos],P[1_(pos+1)]之间
int mid = l + r + 1 >> 1;
if (dcmp(Cro(a - P[1], P[mid] - P[1])) > 0)
r = mid - 1;
else
l = mid;
}
return dcmp(Cro(a - P[r], P[r + 1] - P[r])) <= 0;
}
Vector V1[N * 3], V2[N * 3];
int Mincowski(Point *P1, int n, Point *P2, int m, Point *P)
{ //【闵可夫斯基和】求两个凸包{P1},{P2}的向量集合{V}={P1+P2}构成的凸包
for (int i = 1; i <= n; ++i)
V1[i] = P1[i < n ? i + 1 : 1] - P1[i];
for (int i = 1; i <= m; ++i)
V2[i] = P2[i < m ? i + 1 : 1] - P2[i];
int t = 0, i = 1, j = 1;
P[++t] = P1[1] + P2[1];
while (i <= n && j <= m)
++t, P[t] = P[t - 1] + (dcmp(Cro(V1[i], V2[j])) > 0 ? V1[i++] : V2[j++]);
while (i <= n)
++t, P[t] = P[t - 1] + V1[i++];
while (j <= m)
++t, P[t] = P[t - 1] + V2[j++];
return t;
}
int n, m, n1, n2, n3;
Point Q, P[N * 3], cp[N * 3], P1[N], P2[N], P3[N];
int main()
{
// freopen("123.txt","r",stdin);
scanf("%d", &n1);
for (int i = 1; i <= n1; ++i)
scanf("%lf%lf", &P1[i].x, &P1[i].y);
n1 = ConvexHull(P1, n1, cp);
for (int i = 1; i <= n1; ++i)
P1[i] = cp[i];
scanf("%d", &n2);
for (int i = 1; i <= n2; ++i)
scanf("%lf%lf", &P2[i].x, &P2[i].y);
n2 = ConvexHull(P2, n2, cp);
for (int i = 1; i <= n2; ++i)
P2[i] = cp[i];
scanf("%d", &n3);
for (int i = 1; i <= n3; ++i)
scanf("%lf%lf", &P3[i].x, &P3[i].y);
n3 = ConvexHull(P3, n3, cp);
for (int i = 1; i <= n3; ++i)
P3[i] = cp[i];
n = Mincowski(P1, n1, P2, n2, P), n = Mincowski(P, n, P3, n3, cp), n = ConvexHull(cp, n, P);
scanf("%d", &m);
while (m--)
{
scanf("%lf%lf", &Q.x, &Q.y);
puts(PIP(P, n, Q * 3.0) ? "YES" : "NO");
}
return 0;
}
M 地毯(叉积意义,半平面交,旋转卡壳)
思路
首先观察美丽度式子。
其中, \(C_{1},C_{2},C_{3}\) 为三个地毯的圆心。
显然,最终化成的式子表示的是三个圆心组成的三角形面积(的两倍)。
由于给的多边形是凸包,所以圆心的坐标范围也是在一个凸包内,原题就可以转化为:求一个凸包最大内接三角形面积(的两倍)。
于是要解决这个问题就分成了两个步骤:
- 求圆心取值范围的“新凸包”——半平面交。
- 求凸包最大内接三角形面积的两倍——旋转卡壳。
步骤细节
Step1
先根据输入进来的凸包的点,逆时针构造凸包每条边的向量(因为其他题目一般都用逆时针,所以为了套板子按逆时针顺序存储了)。
然后找出平移向量:将原向量逆时针旋转90°,化为单位向量后再成圆的半径 \(r\) 。
Step2
使用半平面交算法,求出平移后向量围成的新凸包。
注意点:不能将平移后的向量直接求交点,会出现 \(l_{i},l_{i+1}\) 的交点不在凸包内所以取 \(l_{i},l_{i+2}\) 的交点作为凸包上的点的情况。
Step3
直接遍历每个点暴力做的话复杂度是 \(O(n^{3})\) 。TLE6
使用旋转卡壳算法,求出面积最大的内接三角形——固定一条边,由于凸包上点到直线的距离存在单调性,所以可以找到一个对踵点。然后遍历边,每一次遍历判断对踵点是否更新。遍历边复杂度是 \(O(n^{2})\) ,更新对踵点是常数级的,故总体复杂度是 \(O(n^{2})\) 。
TIPS:POJ那道板题的某些题解里的旋转卡壳板子是错误的(某个板子问题应该是在更新边的时候没有更新对应的对踵点),一开始套板子wa7了两发qwq。所以POJ确实水(
代码
#include <bits/stdc++.h>
#include <map>
using namespace std;
#define dbl double
#define N 2005
int n, r;
const dbl pi = acos(-1), eps = 1e-8;
int sgn(dbl a) { return a < -eps ? -1 : (a > eps ? 1 : 0); }
struct VEC
{
dbl x, y;
VEC(dbl X = 0, dbl Y = 0) { x = X, y = Y; }
};
VEC P[N], Q[N]; //原凸包上的点,新凸包上的点
struct Line
{
VEC n, s; //起始点,方向向量
} line1[N], line2[N], fx[N]; //逆时针组成原凸包的有向线段,平移后的有向线段(起始点不是新凸包上的点),平移方向向量
Line l[N]; //组成新凸包的有向线段
VEC operator+(VEC v1, VEC v2) { return VEC(v1.x + v2.x, v1.y + v2.y); }
VEC operator-(VEC v1, VEC v2) { return VEC(v1.x - v2.x, v1.y - v2.y); }
dbl operator*(VEC v1, VEC v2) { return v1.x * v2.x + v1.y * v2.y; }
dbl operator^(VEC v1, VEC v2) { return v1.x * v2.y - v1.y * v2.x; }
VEC operator*(VEC v, dbl k) { return {v.x * k, v.y * k}; }
VEC operator/(VEC v, dbl k) { return {v.x / k, v.y / k}; }
dbl dj(VEC v1, VEC v2) { return v1.x * v2.x + v1.y * v2.y; } //点积
dbl cj(VEC v1, VEC v2) { return v1.x * v2.y - v1.y * v2.x; } //叉积
// 方位角,范围在[0,2pi)
dbl arg(VEC v)
{
dbl r = atan2(v.y, v.x);
return r < 0 ? 2 * pi + r : r;
}
//模长
dbl len(VEC v) { return hypot(v.x, v.y); }
//化为单位向量
VEC unif(VEC v) { return v / len(v); }
// 方位角为f的单位向量
VEC univ(dbl f) { return {cos(f), sin(f)}; }
VEC rot90(VEC p) { return {-p.y, p.x}; } //将线段L1绕起始点逆针旋转90°
VEC litsc(Line l1, Line l2) { return l2.n + l2.s * ((l1.s ^ (l2.n - l1.n)) / (l2.s ^ l1.s)); }
// dbl abss(dbl X) { return X > 0 ? X : -X; }
//半平面交 板子
bool judge(Line l0, Line l1, Line l2) { return sgn((litsc(l1, l2) - l0.n) ^ l0.s) == 1; }
int halfplane_intersection(Line *lv, int n, VEC *pv)
{
static pair<pair<dbl, dbl>, int> a[N];
for (int i = 1; i <= n; ++i)
a[i] = {{arg(lv[i].s), lv[i].n * univ(arg(lv[i].s) - pi / 2)}, i};
sort(a + 1, a + n + 1);
static int b[N], q[N];
int w = 0, l = 1, r = 0;
for (int i = 1; i <= n; ++i)
if (i == 1 || sgn(a[i].first.first - a[i - 1].first.first))
b[++w] = a[i].second;
for (int i = 1; i <= w; ++i)
{
while (l < r && judge(lv[b[i]], lv[q[r]], lv[q[r - 1]]))
--r;
while (l < r && judge(lv[b[i]], lv[q[l]], lv[q[l + 1]]))
++l;
q[++r] = b[i];
}
while (l < r && judge(lv[q[l]], lv[q[r]], lv[q[r - 1]]))
--r;
while (l < r && judge(lv[q[r]], lv[q[l]], lv[q[l + 1]]))
++l;
if (r <= l + 1)
return 0;
int m = 0;
q[r + 1] = q[l];
for (int i = l; i <= r; ++i)
pv[++m] = litsc(lv[q[i]], lv[q[i + 1]]);
return m;
}
//旋转卡壳 板子
dbl rotating_calipers(int tp)
{
int p = 1, q = 2;
dbl ans = 0;
Q[++tp] = Q[0];
for (int i = 0; i < tp; i++)
{
int k = (i + 2) % tp;
for (int j = (i + 1) % tp; j != i; j = (j + 1) % tp)
{
while (fabs(cj(Q[i] - Q[(k + 1) % tp], Q[i] - Q[j])) > fabs(cj(Q[i] - Q[k], Q[i] - Q[j])))
{
k = (k + 1) % tp;
}
ans = max(ans, fabs(cj(Q[i] - Q[k], Q[i] - Q[j])));
}
}
return ans;
}
int main()
{
// freopen("1.in", "r", stdin);
int T;
scanf("%d", &T);
while (T--)
{
//输入与赋值
scanf("%d%d", &n, &r);
for (int i = 1; i <= n; i++)
{
scanf("%lf%lf", &P[i].x, &P[i].y);
line1[i].n = P[i];
}
// 逆时针构造line1,第i个向量起始点就是P[i],终止点是P[i-1]
for (int i = 2; i <= n; i++)
{
line1[i].s = P[i - 1] - P[i];
}
line1[1].s = P[n] - P[1];
//寻找平移方向(将line1逆时针旋转90°,并使方向向量化为单位向量并×r)
for (int i = 1; i <= n; i++)
{
fx[i].s = rot90(line1[i].s);
fx[i].s = unif(fx[i].s) * r;
// printf("_fx_%lf %lf\n", fx[i].s.x, fx[i].s.y);
//平移有向线段
line2[i].n = line1[i].n + fx[i].s;
line2[i].s = line1[i].s;
// printf("___%lf %lf", line2[i].n.x, line2[i].n.y);
// printf("___%lf %lf\n", line2[i].s.x, line2[i].s.y);
}
line2[n + 1] = line2[1], line2[n + 2] = line2[2];
int tp = halfplane_intersection(line2, n, Q);
//求凸包最大内接三角形面积——旋转卡壳
for (int i = 0; i < tp; i++)
{
Q[i] = Q[i + 1];
}
tp--;
dbl ans = rotating_calipers(tp);
printf("%e\n", ans);
}
return 0;
}
R 兔子跳(扫描线)
思路
Step1
由于大兔子步长 \(d\) ,只能在边长 \(d\) 的网格点上移动,则可以把陷阱集中映射到 \([0,d]\) 方格内集中考虑。
映射方法(特判)与注意点:
- 首先将陷阱左下角坐标取模 \(d\) ,右上角坐标根据左下角坐标加上边长计算(直接取模可能会改变长宽值)。
- *负数直接取模不在目标范围内,需要两次取模:\(x_{1}=(x_{1}\%d+d)\%d\)
- 其次判断矩形大小情况、以及矩形覆盖网格线( \(x=d,y=d\) )的情况——特判长宽大于 \(d\) 的情况。长宽都不超过 \(d\) 时,如果矩形超出范围,则拆分出超出范围的矩形并将其映射到范围内成为一个新的小矩形。(略,见代码)
Step2
如果陷阱矩形填满了 \([0,d]\) 方格内,则找不到一个永远跳不进陷阱的点。
计算陷阱矩形覆盖面积,并与 \(d^{2}\) 进行比较,如果 \(s<d^{2}\) 则能找到点输出”YES”,否则不能找到输出“NO”。
计算矩形覆盖面积算法——扫描线:(网上找板子改)
- 首先将左下角坐标取模 \(d\) ,右上角坐标通过左下角坐标加上边长进行平移。
- *负数取模不会一次进入范围内,所以需要:\(x_{1}=(x_{1}\%d+d)\%d\) .
- 其次根据矩形大小,以及矩形对网格线的覆盖情况进行判断——先特判长宽是否超过 \(d\) 如果矩形覆盖了网格线,则需要拆分出一个或多个新的矩形来映射陷阱超出网格线的部分(略,见代码)
- 最后计算所有矩形覆盖面积的并集,并与 \(d^{2}\) 进行比较得出结论。
- \(d^{2}\) 可能会爆int,用long long。
复杂度: \(O(nlogn)\) 。
代码
#include <algorithm>
#include <cassert>
#include <cstdio>
#include <cstring>
#include <iostream>
#include <vector>
using namespace std;
typedef long long lld;
const int N = 800005;
int tot = 0, tp = 0;
lld ans = 0;
struct Tree
{
int cnt, s;
} e[N << 3];
struct node
{
int x, l, r, k;
} p[N << 1];
void update(int i, int l, int r)
{
if (e[i].cnt > 0)
e[i].s = r - l + 1; // l, r表示num[l]到num[r + 1]这一段
else
e[i].s = e[i << 1].s + e[i << 1 | 1].s;
}
void add(int i, int l, int r, int nl, int nr, int k)
{
if (l >= nl && r <= nr)
{
e[i].cnt += k;
update(i, l, r);
return;
}
int mid = (l + r) >> 1;
if (nl <= mid)
add(i << 1, l, mid, nl, nr, k);
if (nr > mid)
add(i << 1 | 1, mid + 1, r, nl, nr, k);
update(i, l, r);
}
int get()
{
return e[1].s;
}
void add1(int x1, int x2, int y1, int y2)
{
p[++tp].x = x1;
p[tp].l = y1;
p[tp].r = y2;
p[tp].k = 1;
p[++tp].x = x2 + 1;
p[tp].l = y1;
p[tp].r = y2;
p[tp].k = -1;
}
int a[N], b[N], c[N], d[N];
//
int x1[N], x2[N], y1[N], y2[N];
//
vector<int> tmp;
bool cmp(node a, node b)
{
return a.x < b.x;
}
int main()
{
// freopen("1.in", "r", stdin);
int n, dd;
scanf("%d%d", &n, &dd);
tot = n;
//
int flag = 0;
for (int i = 1, x0, y0; i <= n; i++)
{
scanf("%d%d%d%d", &x1[i], &y1[i], &x2[i], &y2[i]);
//矩形处理
//记录两点横纵坐标差值
x0 = x2[i] - x1[i], y0 = y2[i] - y1[i];
//左下角取模d
x1[i] = (x1[i] % dd + dd) % dd;
y1[i] = (y1[i] % dd + dd) % dd;
//右上角=左下角+(x0,y0)
x2[i] = x1[i] + x0 - 1;
y2[i] = y1[i] + y0 - 1;
//如果矩形长宽都不超过d右上角根据情况判断:共四种
if (x0 < dd && y0 < dd) //都不超过
{
if (x2[i] < dd && y2[i] < dd) //在区域内,不作拆分
{
}
else if (x2[i] < dd && y2[i] >= dd) //区域经过y=d
{
//拆分超出范围的矩形
tot++;
x1[tot] = x1[i];
x2[tot] = x2[i];
y1[tot] = 0;
y2[tot] = y2[i] - dd;
//修改原矩形
y2[i] = dd - 1;
}
else if (x2[i] >= dd && y2[i] < dd) //区域经过x=d
{
//拆分超出范围的矩形
tot++;
x1[tot] = 0;
x2[tot] = x2[i] - dd;
y1[tot] = y1[i];
y2[tot] = y2[i];
//修改原矩形
x2[i] = dd - 1;
}
else //(d,d)在区域内
{
//拆分左上方部分
tot++;
x1[tot] = x1[i];
x2[tot] = dd - 1;
y1[tot] = 0;
y2[tot] = y2[i] - dd;
//拆分右下方部分
tot++;
x1[tot] = 0;
x2[tot] = x2[i] - dd;
y1[tot] = y1[i];
y2[tot] = dd - 1;
//拆分右上方部分
tot++;
x1[tot] = 0;
x2[tot] = x2[i] - dd;
y1[tot] = 0;
y2[tot] = y2[i] - dd;
//修改原矩形
x2[i] = dd - 1;
y2[i] = dd - 1;
}
}
else if (x0 >= dd && y0 < dd) //如果x0超过d
{
x1[i] = 0;
x2[i] = dd - 1;
if (y2[i] < dd) //在区域内,不作拆分
{
}
else
{
//拆分超出范围的矩形
tot++;
x1[tot] = x1[i];
x2[tot] = x2[i];
y1[tot] = 0;
y2[tot] = y2[i] - dd;
//修改原矩形
y2[i] = dd - 1;
}
}
else if (x0 < dd && y0 >= dd) //如果y0超过d
{
y1[i] = 0;
y2[i] = dd - 1;
if (x2[i] < dd) //在区域内,不作拆分
{
}
else
{
//拆分超出范围的矩形
tot++;
y1[tot] = y1[i];
y2[tot] = y2[i];
x1[tot] = 0;
x2[tot] = x2[i] - dd;
//修改原矩形
x2[i] = dd - 1;
}
}
else if (x0 >= dd && y0 >= dd) //如果矩形长宽都超过d,无解
{
flag = 1;
break;
}
else
{
assert(0);
}
}
if (flag)
{
printf("NO");
}
else
{
for (int i = 1; i <= tot; i++)
{
add1(x1[i], x2[i], y1[i], y2[i]); //添加四元组
}
sort(p + 1, p + 1 + tp, cmp); //按照x排序
p[0].x = 0;
for (int i = 0, j = 1; i <= dd; i++)
{
for (; p[j].x == i && j <= tp; j++)
{
add(1, 0, dd - 1, p[j].l, p[j].r, p[j].k);
}
ans += get();
}
assert(get() == 0);
if (ans < 1ll * dd * dd)
printf("YES");
else
printf("NO");
}
//
return 0;
}