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(失败):由于点坐标都是整点,所以可以将坐标轴旋转一个极小的角度,来使得新坐标轴不经过任何一个点,即原坐标轴上的点可以分类并入四个象限。

\[一个示例:\\ 理解为将坐标轴顺时针旋转一个极小的角度\\ 第一象限:x>=0,y>0\\ 第二象限:x<0,y>=0\\ 第三象限:x<=0,y<0\\ 第四象限:x>0,y<=0 \]

  • 方案1的缺陷:情况种类多(首先是两种旋转方向,其次在每种旋转方向下象限跨越方式都要找对应的“旋转程度”),整形运算时间优势被情况数量的常数带来的时间劣势抵消了,依然会TLE,且代码会写成屎山(几十个判断)。
  • 改进方案2(可行):观察方案一,\(x、y\) 与0的关系分别出现了四种情况,导致新坐标轴判断情况增加。可以换一个更优的情况合并方式,使得\(x、y\) 与0的关系分别只有两种情况(和原坐标轴一样)。

\[一个示例:\\ 理解为在第三象限的位置对着第一象限的方向砸了一下坐标轴\\导致了坐标轴的极小偏移\\ 第一象限:x>0,y>0\\ 第二象限:x<=0,y>0\\ 第三象限:x<=0,y<=0\\ 第四象限:x>0,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 地毯(叉积意义,半平面交,旋转卡壳)

思路

首先观察美丽度式子。

\[\begin{align*} &|w_{1}+w_{2}+w_{3}|\\ =&|(x_{1}y_{2}-x_{1}y_{3})+(x_{2}y_{3}-x_{2}y_{1})+(x_{3}y_{1}-x_{3}y_{2})|\\ =&|(x_{1}y_{2}-x_{2}y_{1})+(x_{2}y_{3}-x_{3}y_{2})+(x_{3}y_{1}-x_{1}y_{3})|\\ =&|OC_{1}×OC_{2}+OC_{2}×OC_{3}+OC_{3}×OC_{1}| \end{align*} \]

其中, \(C_{1},C_{2},C_{3}\) 为三个地毯的圆心。

显然,最终化成的式子表示的是三个圆心组成的三角形面积(的两倍)。

由于给的多边形是凸包,所以圆心的坐标范围也是在一个凸包内,原题就可以转化为:求一个凸包最大内接三角形面积(的两倍)。

于是要解决这个问题就分成了两个步骤:

  1. 求圆心取值范围的“新凸包”——半平面交
  2. 求凸包最大内接三角形面积的两倍——旋转卡壳

步骤细节

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;
}
posted @ 2022-10-21 19:46  IrisHyaline  阅读(71)  评论(0编辑  收藏  举报