二维凸包小结

前言

博客内所有的图很多不是我画的,来自网络。
我并没有系统学习过计算几何,写的不规范勿喷。写错了欢迎留言。
图片来源:

https://www.cnblogs.com/czaoth/p/6912073.html


前置芝士

你首先需要知道什么是向量什么是叉积。(详细内容可以到高中数学必修4中学习,我还没学过)
看过这个博客,没学过向量的应该也会向量了吧(我学的时候根本不知道有向量这个东西)

看一下上图A、B和C都是平面直角坐标系中的一个点,那么\(\vec{AB}\)就是一个向量。
同理\(\vec{AC} \ \vec{AD}\)都是向量。
如果要判断两个线段之间的夹角有没有超过180°,需要了解一下什么是叉积?这里讲的叉积和物理学中的叉积不大一样。
假设我们需要求二维向量\(\vec{a}={\lbrace x1,y1 \rbrace}\)\(\vec{b}={\lbrace x2,y2 \rbrace}\)的叉积

这是我自己画的图,把自己丑到了
\[\vec{a}\times \vec{b}=x1\times y2 - x2\times y1\]
叉积的几何意义:\(sin(a,b)\times |\vec{a}|\times |\vec{b}|\)
以上式子的\(sin(a,b)\)是逆时针中向量\(a\)\(b\)之间的夹角。
我们都知道:如果\(sin(\alpha)\)\(0<=\alpha<=180\),那么\(0<sin(\alpha)<1\),如果\(180<\alpha<360\),那么\(sin(\alpha)<0\),但等于180时,函数值为0。
那么在这里我们就可以由这个式子来判断原先的线段和现在新加进来的线段会不会凹进去。
如果实在不明白是怎么回事,那么只需要背下来就好了,我们只需要这个性质来判断是不是会有拐出去的情况。


关于二维凸包

他死了
这个东西就是一个凸包

凸包可以想象成一个木板上有很多个钉子。
你现在用了一根橡皮筋将所有的钉子围在了一个圈子里。


暴力\(O(n^3)\)

这个应该是一个人都会的吧,不做赘述了。


分治法\(O(nlogn)\)

把所有的点都放在二维坐标系里面。那么横坐标最小和最大的两个点 P1 和 Pn 一定是凸包上的点(为什么呢?用反证法很容易证明,这里不详讲)。直线 P1Pn 把点集分成了两部分,即 X 轴上面和下面两部分,分别叫做上包和下包。
对上包:求距离直线 P1Pn 最远的点,即下图中的点 Pmax 。
作直线 P1Pmax 、PnPmax,把直线 P1Pmax 左侧的点当成是上包,把直线 PnPmax 右侧的点也当成是上包。
重复步骤 2、3。
对下包也作类似操作。


Jarvis步进法\(O(NH)\)

纵坐标最小的那个点一定是凸包上的点,例如图上的 P0。
从 P0 开始,按逆时针的方向,逐个找凸包上的点,每前进一步找到一个点,所以叫作步进法。
怎么找下一个点呢?利用夹角。假设现在已经找到 {P0,P1,P2} 了,要找下一个点:剩下的点分别和 P2 组成向量,设这个向量与向量P1P2的夹角为 β 。当 β 最小时就是所要求的下一个点了,此处为 P3 。


Graham Scan

求解二维凸包有很多的解法,比较常见的有Graham Scan,音译过来就是刮汉死干算法,皮这一下真sufu,反正蒟蒻我就只会这一种,听说好像有5种吧,以后都学掉。qwq
代码量非常的短,而且时间复杂度也就只有\(O(nlogn)\)
我写的扫描法是分别求上下凸包的,然后再合在一起,还有一种是极角排序。


上下凸包求法:
首先我们需要确定一个起点,起点就是x坐标最左边,如果x坐标相同,那么就取y坐标最小的。

求下凸包
非常明显A点一定是在凸包上的。
那么我们需要往下连边,第一个遇到的点是C,因为当前只有一个点,那么就继续连下去。
但是接下来一步,我们C点和G点连到了,我们会发现向量\(\vec{AC}\)\(\vec{CG}\)之间的夹角\(>180\),这就说明了C点一定不是凸包上的点。
这个时候我们之前讲的叉积就派上用场了,可以判断两个线段的夹角是否大于180°。
那么我们就舍去C这个节点,G点就变成了凸包上的点。
待会?好像出锅了?从图中看向量\(\vec{AC}\)\(\vec{CG}\)的角度逆时针旋转好像并没有\(>180\)耶?难道是我们的算式错误了。
并没有错误,因为我们需要将向量的起点放到一起,然后由\(\vec{AC}\)转到\(\vec{CG}\)就会发现夹角远比180大。
那么就变成了以下这个样子

一直扩展到D号节点
同理可以一直推出上凸包

上下凸包合并就成了答案的凸包了。

#include <bits/stdc++.h>
#define db double 
#define N 10005
using namespace std;
template <typename T> T sqr(T x) {return x * x;}
struct Point {
    db x, y;
}p[N], s[N];
db dist(Point p1, Point p2) {//计算距离 
    return sqrt(sqr(p1.x - p2.x) + sqr(p1.y - p2.y));
}
bool cmp(Point p1, Point p2) {//排序 
    return (p1.x == p2.x)? (p1.y < p2.y): (p1.x < p2.x);
}
db cross(Point p1, Point p2) {//叉积 
    return p1.x * p2.y - p1.y * p2.x;
}
db check(Point p1, Point p2, Point p3, Point p4) {//查看两个线段的向量的夹角(逆时针) 
    return cross((Point){p2.x - p1.x, p2.y - p1.y},(Point){p4.x - p3.x, p4.y - p3.y}) <= 0;
}
int n, tot;
db ans = 0;
int main() {
    scanf("%d", &n);
    for (int i = 1; i <= n; i ++) scanf("%lf%lf", &p[i].x, &p[i].y);
    sort(p + 1, p + 1 + n, cmp);
    tot = 0;
    for (int i = 1; i <= n; i ++) {//计算上凸包 
        while (tot > 1 && check(s[tot], s[tot - 1], s[tot], p[i])) -- tot;
        s[++ tot] = p[i];
    }
    int tmp = tot;
    for (int i = n - 1; i >= 1; i --) {//下凸包 
        while (tot > tmp && check(s[tot], s[tot - 1], s[tot], p[i])) -- tot;
        s[++ tot] = p[i];
    }
    for (int i = 2; i <= tot; i ++) ans += dist(s[i], s[i - 1]);
    printf("%.2lf\n", ans);
    return 0;
}

极角排序求法
步骤:

  • 找到最下面的一个点,如果存在纵坐标相同,取最左面的点。取这个点作为原点。
  • 把p0同点集中其他各点用线段连接,并计算这些线段与水平线的夹角,然后按夹角从小到大排序(夹角范围为 [0, 180)度),如果存在夹角相同的点,取最远点其余点直接删除。

  • 创建一个栈,将p0,p1,p2三点放入栈中,之后依次遍历p3~pn各点,若栈顶的两个点和当前的点pi这三点连线的方向向顺时针方向偏转,表明栈顶的点是一个凹陷处,应删除,则栈顶元素出栈。并将pi压入栈中。如果向逆时针方向偏转,则直接压入栈中。
  • 重复上述过程,直到遍历完成,在栈中的点就是凸包上的顶点。(结果图是最上边的图)
  • 不要忘记加最后一个起点。

    动态图

贡献蒟蒻代码

#include <bits/stdc++.h>
#define ll long long
#define db double 
#define ms(a, b) memset(a, b, sizeof(a))
#define inf 0x3f3f3f3f
#define N 10005
using namespace std;
template <typename T>
inline void read(T &x) {
    x = 0; T fl = 1; char ch = 0;
    for (; ch < '0' || ch > '9'; ch = getchar())
        if (ch == '-') fl = -1;
    for (; ch >= '0' && ch <= '9'; ch = getchar())
        x = (x << 1) + (x << 3) + (ch ^ 48);
    x *= fl;
}
template <typename T> T sqr(T x) {return x * x;}
struct node {
    db x, y;
}p[N], S[N];
int n, top;
db ans = 0.0;
db X(node a1, node a2, node b1, node b2) {
    return (a2.x - a1.x) * (b2.y - b1.y) - (b2.x - b1.x) * (a2.y - a1.y);
}
bool cmp(node a, node b) {
    return (a.x == b.x) ? (a.y < b.y): (a.x < b.x);
}
db dis(node a, node b) {
    return sqrt(sqr(a.x - b.x) + sqr(a.y - b.y));
}
bool cmp2(node p1, node p2) {
    db tmp = X(p[1], p1, p[1], p2);
    if (tmp > 0)  return 1;
    if (tmp == 0 && dis(p[0], p1) < dis(p[0], p2)) return 1;
    return 0;
}
int main() {
    read(n);
    for (int i = 1; i <= n; i ++) 
        scanf("%lf%lf", &p[i].x, &p[i].y);
    sort(p + 1, p + 1 + n, cmp);
    S[++ top] = p[1];
    sort(p + 2, p + n + 1, cmp2);
    for (int i = 2; i <= n; i ++) {
        while (top > 1 && X(S[top - 1], S[top], S[top], p[i]) <= 0) top --;
        S[++ top] = p[i]; 
    }
    ans = dis(S[1], S[top]);
    for (int i = 2; i <= top; i ++) ans += dis(S[i - 1], S[i]);
    printf("%.2lf\n", ans);
    return 0;
}

猜你喜欢

转载自www.cnblogs.com/chhokmah/p/10633215.html