Loading

凸包 学习笔记

前置知识

三角函数

三角函数定义

给定一个有向角度 \(\alpha\),假设一个点的旋转 \(R(x, y) = (xc - ys, xs + yc)\),则定义 \(\sin(\alpha) = s\)\(\cos(\alpha) = c\)

考虑到 \((1, 0)\) 旋转到点 \((c, s)\) 后的图像,发现 \(s\) 对应部分就在弦上,\(c\) 对应部分在剩下的弦上,因此被成为 正弦 和 余弦

我们经常在 单位圆 上讨论三角函数,所以三角函数其实跟圆关系更大。

如上图,容易注意到 \(CD = \sin(\alpha), AD = \cos(\alpha)\)

常用三角函数的图像以及性质

\(\sin(\alpha)\) 图像:

容易发现,正弦的图像周期为 \(2\pi\),为奇函数,定义域为 \((-\infty, +\infty)\),值域为 \([-\pi, \pi]\)

\(\cos(\alpha)\) 图像:

容易发现,余弦的图像周期为 \(2\pi\),为偶函数,定义域为 \((-\infty, +\infty)\),值域为 \([-\pi, \pi]\)

这里发现余弦实际上是正弦的线性变换(平移)。

\(\tan(\alpha)\) 图像:

正切函数定义域为 \((-\infty, +\infty)\),值域为 \([-\infty, \infty]\)

向量

前引

向量是一个在物理学中十分常见的概念,在数学和编程中的应用也很广泛。今天来浅谈一下向量的一些基本运算及其应用。

定义

向量是一个有方向,有长度的量,在坐标系中通常通过起点坐标和终点坐标表示。为了方便运算,七点坐标通常被设为原点。

但正如同刚才所说,向量的两个关键因素为方向和长度,所以还可以用这一种方式:

\[P = \{r, \alpha\} \]

这也被称为 极角坐标系\(r\) 也被称为向量的模长,\(\alpha\) 自然是与 \(x\) 轴成的夹角。

向量在坐标系中大致是这样:

基本运算

向量加法

对于向量加法,我们考虑将两个向量移动到原点,然后这两个向量之和的向量的终点坐标就是:

\[(x_3, y_3) = (x_1 + x_2, y_1 + y_2) \]

说白了,就是横坐标相加,纵坐标相加。

然后他的几何意义也不难。你可以把向量想成向一个方向移动一段距离。那么,他的图像如下:

其实,它也可以表示成一个平行四边形的对角线,如图:

向量减法

我们已经知道了加法运算,那么可以考虑减法。

\[a - b = a + (-b) \]

又知道相反数的定义为:向量的模不变,方向与之前相反

所以减法的图像就可以表示为:

向量点乘

向量的点乘大小等于这两个向量的模的乘积再乘以两个向量之间的夹角的余弦,就是:

\[\begin{aligned} a \cdot b & = \left\vert a \right\vert \left\vert b \right\vert \cos{<a,b>}\\ & = a_xb_x+a_yb_y \end{aligned} \]

其实,两个向量 \(a\)\(b\) 的点积的模等于 \(b\)\(a\) 上的投影的向量乘以 \(a\) 的模,如图:

点乘有如下性质:

  • \(a \cdot e = \left\vert a \right\vert \left\vert b \right\vert \cos{<a,b>} = \left\vert a \right\vert \cos{<a,b>}\)
  • \(a \perp b\) 等价于 \(a \cdot b = 0\),即 \(a_xb_x + a_yb_y + a_zb_z = 0\)
  • 自乘:\(\left\vert a \right\vert ^ 2 = a \cdot a\)
  • 结合律:\((\lambda \cdot a) \cdot b = \lambda(a \cdot b)\)
  • 交换律:\(a \cdot b = b \cdot a\)
  • 分配律:\(a \cdot (b + c) = a \cdot b + a \cdot c\)

如果有三个点 \(P, Q, R\),并且 \(\overrightarrow{PQ}\)\(\overrightarrow{PR}\) 同线,对于一个向量 \(\overrightarrow{PQ}\) 和一个点 \(R\),则:

  • \(\overrightarrow{PQ} \cdot \overrightarrow{PR} < 0\)\(R\)\(\overrightarrow{PQ}\) 的左手边。
  • \(0 \leq \overrightarrow{PQ} \cdot \overrightarrow{PR} \leq \overrightarrow{PQ} \cdot \overrightarrow{PQ}\),则 \(R\)\(\overrightarrow{PQ}\) 上。
  • \(\overrightarrow{PQ} \cdot \overrightarrow{PQ} < \overrightarrow{PQ} \cdot \overrightarrow{PR}\),则 \(R\)\(\overrightarrow{PQ}\) 的右手边。
向量叉乘

向量的叉乘大小等于这两个向量的模的乘积再乘以两个向量之间的夹角的正弦,就是:

\[\begin{aligned} a \cdot b & = \left\vert a \right\vert \left\vert b \right\vert \sin{<a,b>}\\ & = b_xa_y-a_xb_y \end{aligned} \]

它也可以被写成行列式的形式:

\[p_1 \times p_2 = \det{\begin{vmatrix}x_1&x_2\\y_1&y_2\end{vmatrix}}=x_1y_2-x_2y_1=-p_2\times p_1 \]

他的几何意义如下:

这时,如果 \(p_1 \times p_2 > 0\), 则相对于原点来说,\(p_1\)\(p_2\) 的顺时针方向;如果 \(p_1 \times p_2 < 0\) 时,\(p_2\)\(p_1\) 的顺时针方向;如果 \(p_1 \times p_2 = 0\),则 \(p_1\)\(p_2\) 共线。

凸包

凸包简介

凸包定义

粗略地对凸包进行一个理解,我们其实容易知道凸包就是把一个平面上的点集最外面全部连起来形成的一个凸多边形,这个凸多边形能包含点集内所有点。

但是我们要对凸包进行一个较严格的定义:

\(S\) 为欧几里得空间 \(R^n\) 的任意子集,包含 \(S\) 的最小凸集便是凸包。

相关概念

我们观察一张图。

如果我们把这个凸包给分成上下两个部分,那么每个部分的边的斜率是单调的。如下图:

于是就有了两个概念:上凸壳和下凸壳。

Graham 扫描法

Graham 扫描法是一种十分优秀的求凸壳算法。我们发现其实求凸壳的算法还有 Jarvis 步进法,但是因为复杂度高且代码难写被 ban 了。

排序

为了方便,我们先选择一个 \(y\) 坐标最小的点作为整个坐标系的原点。

接下来,我们把剩余的每个点 \(P_i\) 与原点 \(O\) 连一个向量。

排序时我们使用极角表示法。我们剩余的点按照极角排序,这样所有点就会按照逆时针顺序排列起来。

如下图。假设 \(P\) 数组已经排好序,则应该如下:

单调栈

单调栈,起爆!

我们按照逆时针顺序进行一个凸包的求。

假设我们当前正在插入 \(P_i\)

  • \(P_i\) 与栈顶元素和栈的第二个元素连成了一个凸壳,则把 \(P_i\) 扔进去。

  • 否则,把栈弹掉。

Graham 图解

接下来进行一个图的解。

如上图。假设我们现在要插入 \(P_{10}\)

  • 首先,我们发现,\(P_{10}, P_9, P_8\) 连成了一个凹壳。于是将 \(P_9\) 弹出。

  • 然后,我们发现,\(P_{10}, P_8, P_7\) 连成了一个凹壳。于是将 \(P_8\) 弹出。

  • 最后,我们发现,\(P_{10}, P_7, P_5\) 连成了一个凸壳。于是插入 \(P_{10}\),结束。

Graham 实现细节

I 如何计算极角?

我们使用一个黑科技:\(\operatorname{atan2}\)

具体的,如果我们调用 \(\operatorname{atan2(y, x)}\),则他会帮助我们计算出 \(\frac{y}{x}\) 的反正切函数值。

所以从某种意义上,它帮助我们快速地求出了极角。

II 如何判断凸壳 / 凹壳?

假设我们要判断 \(X, Y, Z\) 三个点。这个问题就等价于问 \(Y\)\(\overrightarrow{XZ}\) 的哪一边。

叉乘有性质:

  • 如果 \(p_1 \times p_2 > 0\), 则相对于原点来说,\(p_1\)\(p_2\) 的顺时针方向;
  • 如果 \(p_1 \times p_2 < 0\) 时,\(p_2\)\(p_1\) 的顺时针方向;
  • 如果 \(p_1 \times p_2 = 0\),则 \(p_1\)\(p_2\) 共线。

这样可以直接套一下,就可以了。

III 排序函数的细节

由上面的性质,我们发现 \(x \times y > 0\) 时,\(x\)\(y\) 的顺时针方向。

\(x \times y = 0\),那么 \(x\)\(y\) 共线。此时我们可以判断 \(x\)\(y\) 到原点的距离,以便处理一些情况。(比如链)

2.3 相关例题

I Fencing the cows

一道二维凸包模板题。注意特判一行 / 一列的情况。

/*******************************
| Author:  DE_aemmprty
| Problem: P2742 [USACO5.1] 圈奶牛Fencing the Cows /【模板】二维凸包
| Contest: Luogu
| URL:     https://www.luogu.com.cn/problem/P2742
| When:    2024-04-16 12:19:19
| 
| Memory:  128 MB
| Time:    1000 ms
*******************************/
#include <bits/stdc++.h>
using namespace std;

long long read() {
    char c = getchar();
    long long x = 0, p = 1;
    while ((c < '0' || c > '9') && c != '-') c = getchar();
    if (c == '-') p = -1, c = getchar();
    while (c >= '0' && c <= '9')
        x = (x << 1) + (x << 3) + (c ^ 48), c = getchar();
    return x * p;
}

const int N = 1e5 + 7;

int n;
int st[N], tot;

struct vec {
    long double x, y;
    long double angle() { return atan2(y, x);}
    long double len() { return sqrt(x * x + y * y);}
    vec operator + (const vec &p) const {
        return (vec) {x + p.x, y + p.y};
    }
    vec operator - (const vec &p) const {
        return (vec) {x - p.x, y - p.y};
    }
    long double operator * (const vec &p) const {
        // x1y2 - x2y1
        // outer product
        return x * p.y - p.x * y;
    }
} p[N];

bool operator < (vec x, vec y) {
	return x * y == 0 ? x.len2() < y.len2() : x * y > 0;
}

void solve() {
    // freopen("P2742_1.in", "r", stdin);
    cin >> n;
    long long col = -1;
    long double ymn = 2e18;
    bool row = 1, ccc = 1;
    for (int i = 1; i <= n; i ++) {
        cin >> p[i].x >> p[i].y;
        row &= (p[i].x == p[1].x);
        ccc &= (p[i].y == p[1].y);
        if (p[i].y < ymn) {
            ymn = p[i].y;
            col = i;
        }
    }
    if (ccc) {
        swap(row, ccc);
        for (int i = 1; i <= n; i ++) swap(p[i].x, p[i].y);
    } if (row) {
        long double xmn = 2e18, xmx = -2e18;
        for (int i = 1; i <= n; i ++) {
            xmn = min(xmn, p[i].y);
            xmx = max(xmx, p[i].y);
        }
        printf("%.2Lf\n", (long double) (xmx - xmn) * 2.0);
        return ;
    }
    swap(p[1], p[col]);
    vec rt = p[1];
    for (int i = 2; i <= n; i ++)
        p[i] = p[i] - rt;
    sort(p + 2, p + n + 1);
    p[1] = {0, 0};
    st[1] = 1, st[2] = 2; tot = 2;
    for (int i = 3; i <= n; i ++) {
        while (tot >= 2 && (p[st[tot - 1]] - p[i]) * (p[st[tot - 1]] - p[st[tot]]) >= 0)
            tot --;
        st[++ tot] = i;
    }
    long double ans = (p[st[1]] - p[st[tot]]).len();
    for (int i = 2; i <= tot; i ++)
        ans += (p[st[i]] - p[st[i - 1]]).len();
    printf("%.2Lf\n", ans);
}

signed main() {
    int t = 1;
    while (t --) solve();
    return 0;
}

II 信用卡凸包

我们考虑把每一张信用卡的四个点给拿出来算凸包。

容易发现,外面那一圈直的线总周长和这个凸包的周长相等。

于是只需要算圆弧状的周长。

设扇形的周角之和为 \(S\),凸壳的点数为 \(n\)

则不难发现一个等价关系:\(S + 180^{\circ}n + 180^{\circ}(n - 2) = 360^{\circ}n\)

于是解得 \(S = 360^{\circ}\),故扇形的周角和为 \(360^{\circ}\),即一个圆的周长。

故得解。

一些细节:

如何求一个旋转矩形的四个点?
考虑使用三角函数。如果一个点在半径为 \(R\) 上的原点圆上,有一个在圆上的点与极轴的夹角为 \(\alpha\),则他的坐标为 \((r\times \cos(\alpha), r \times \sin(\alpha))\)。如下图:

/*******************************
| Author:  DE_aemmprty
| Problem: P3829 [SHOI2012] 信用卡凸包
| Contest: Luogu
| URL:     https://www.luogu.com.cn/problem/P3829
| When:    2024-04-16 20:01:29
| 
| Memory:  125 MB
| Time:    1000 ms
*******************************/
#include <bits/stdc++.h>
using namespace std;

long long read() {
    char c = getchar();
    long long x = 0, p = 1;
    while ((c < '0' || c > '9') && c != '-') c = getchar();
    if (c == '-') p = -1, c = getchar();
    while (c >= '0' && c <= '9')
        x = (x << 1) + (x << 3) + (c ^ 48), c = getchar();
    return x * p;
}

const int N = 1e4 + 7;
const long double pi = acos(-1);

int n;
long double a, b, r;
long double x[N], y[N], d[N];
int st[N << 2], tot;

struct vec {
    long double x, y;
    long double angle() { return atan2(y, x);}
    long double len() { return sqrt(x * x + y * y);}
    bool operator < (const vec &p) const {
        return atan2(y, x) < atan2(p.y, p.x);
    }
    vec operator + (const vec &p) const {
        return (vec) {x + p.x, y + p.y};
    }
    vec operator - (const vec &p) const {
        return (vec) {x - p.x, y - p.y};
    }
    long double operator * (const vec &p) const {
        return x * p.y - p.x * y;
    }
} P[N << 2];

vec rotate(vec t, vec c, long double d) {
    long double std = (t - c).angle() + d;
    long double dis = (t - c).len();
    return (vec) {dis * cos(std), dis * sin(std)} + c;
}

int getRt() {
    long double mn = 2e18; int res = -1;
    for (int i = 1; i <= n * 4; i ++)
        if (mn > P[i].y) {
            mn = P[i].y;
            res = i;
        }
    return res;
}

void solve() {
    n = read();
    cin >> a >> b >> r;
    b = b / 2.0 - r;
    a = a / 2.0 - r;
    for (int i = 1; i <= n; i ++) {
        cin >> x[i] >> y[i] >> d[i];
        P[i * 4 - 3] = rotate((vec) {x[i] - b, y[i] + a}, (vec) {x[i], y[i]}, d[i]);
        P[i * 4 - 2] = rotate((vec) {x[i] + b, y[i] + a}, (vec) {x[i], y[i]}, d[i]);
        P[i * 4 - 1] = rotate((vec) {x[i] + b, y[i] - a}, (vec) {x[i], y[i]}, d[i]);
        P[i * 4 - 0] = rotate((vec) {x[i] - b, y[i] - a}, (vec) {x[i], y[i]}, d[i]);
    }
    swap(P[1], P[getRt()]);
    // for (int i = 1; i <= n * 4; i ++)
    //     cout << P[i].x << ' ' << P[i].y << '\n';
    for (int i = 2; i <= n * 4; i ++) P[i] = P[i] - P[1];
    P[1] = {0, 0};
    sort(P + 2, P + n * 4 + 1);
    st[1] = 1, st[2] = 2, tot = 2;
    for (int i = 3; i <= n * 4; i ++) {
        while (tot >= 2 && (P[st[tot - 1]] - P[i]) * (P[st[tot - 1]] - P[st[tot]]) >= 0)
            tot --;
        st[++ tot] = i;
    }
    // for (int i = 1; i <= tot; i ++)
        // cout << P[st[i]].x << ' ' << P[st[i]].y << '\n';
    long double sum = (P[st[1]] - P[st[tot]]).len() + 2.0 * r * pi;
    for (int i = 2; i <= tot; i ++) sum += (P[st[i]] - P[st[i - 1]]).len();
    printf("%.2Lf\n", sum);
}

signed main() {
    int t = 1;
    while (t --) solve();
    return 0;
}

III 防线修建

先咕着。

3 凸包直径

咕咕咕。

posted @ 2024-04-14 12:01  DE_aemmprty  阅读(50)  评论(0)    收藏  举报