[计算几何] 2 二维凸包/笨蛋(我)也能看懂的二维凸包算法

二维凸包,这篇博客已经说得够好了,介绍了斜率逼近法、Jarvis算法,Graham算法,还有Andrew算法。我这篇博客只会非常详细的介绍Andrew算法

数论小白都能看懂的平面凸包详解 - ShineEternal的笔记小屋 - 洛谷博客 (luogu.com.cn)

我相信凭借着我6个粉丝其中5个都是老熟人的传播量,应该不会因为乱贴别人链接导致啥问题()。也许会有朋友要问了,人家都写的这么好了,你博客的创新点在哪里呢?(谁问你了)我主要解决的是这三个问题

  • 关于三点共线等等的特殊情况,网上的有些代码会被hack掉,我在这里给出一个相对比较靠谱的代码。
  • 每个人对Andrew算法的理解可能都有点点不一样,也许我的博客会更适合你。
  • 我会尽量探讨,在不同的情况中,板子应该怎么做一些小改动。

那我们开始吧。

Andrew算法

算法思想

要对一个点集求凸包,就是用一个凸多边形把这些点集全部围起来,这些点最多只能在凸多边形上,不能超出这个凸多边形,并且这个凸多边形是所有可以满足上述条件的凸多边形中,面积、周长均是最小的那一个。并且,这样的一个凸多边形是唯一的。我们从感性上就可以感觉到,这个凸多边形的顶点一定是从点集中选取的。

image-20230724162353819

假如黑色的点是原本的点集,点P是形成的凸包中的某一个顶点。P不属于初始,并且凸包把所有的点围了起来。那么,一定存在初始点集中两个点,把它们相连之后,围成的凸多边形仍然是凸包,并且面积比原来的凸包更小。有了这样一个认知,我们要做的事情就变成了:从点集中选出若干点构成凸包。也可以说,这些点肯定是最“外面”的凸包框住的,对吧?

首先,我们把所有点从左到右的排个序,找到点集的最左边的点A和最右边的点B,点A和点B肯定是最“外面”的点,也就是说,肯定是构成凸包的点。我们想象现在有一块柔软的布从上到下垂下来,盖住了这些点,形成了凸包的上边界:

image-20230724170600804

这个上边界很明显,起始于A,终于B。我们先不想一次找完整个凸包,我们先找上边界。

把点按照这个方法排序,可以得到\(P=\{p_1,...,p_7\}\)

bool operator < (Point& B) {
        if (!sgn(x - B.x)) return y < B.y;
        return x < B.x;
    }

初始点集\(U=\{A\}\),然后,A先试探性的和\(p_1\)相连,然后把\(p_1\)插入点集U。嗯,很显然\(U=\{A,p_1\}\)就是\(A,p_1\)的上边界。再看\(p_2\),如果把\(p_1p_2\)相连,那\(U=\{A,p_1,p_2\}\)就是这三个点的上边界。

现在压力来到\(p_3\),把\(p_2p_3\)相连后,情况开始不对了,为了维护美丽的上边界形状,我们删除\(\overrightarrow{p_1p_2},\overrightarrow{Ap_1}\),然后把\(p_3\)加进来,现在\(U=\{A, p_3\}\)

重复上述过程,最终我们得到了\(U=\{A, p_3, p_7, B\}\)

image-20230724175342449

纵观整个寻找上边界的过程,其实可以描述为:按照排序从小到大依次遍历节点,当遍历到第\(i\)个节点时,看节点\(i\)是否当前的上边界\(U\)的末尾两点构成的向量\(\overrightarrow{v}\)的右侧,如果是,则将点\(i\)加入\(U\),否则,从\(U\)中不断弹出最后一个节点,直到满足条件或者弹到没点可弹为止。

维护下边界,就是反着再来一遍:按照排序从大到小依次遍历节点,当遍历到第\(i\)个节点时,……(不要指望着我再告诉你怎么做了!自己推一下)就像这样:

嗯嗯,最后我们可以得到,\(U=\{A, p_3, p_7, B\}; D=\{ B, p_5, p_2, A\}\)

合起来就是凸包:

代码思路

问题已经解决了,现在我们只要把我们的思想用代码表示出来即可。

回过头来看凸包的形成过程,我们为什么能判断\(p_7\)是新的上边界,而把\(p_6\)弹出了呢?通过仔细的观察,我们发现是因为\(p_7\)这个点在向量\(\overrightarrow{p_3p_6}\)的左侧。如果在维护上边界的时候,点在向量左侧,那就说明\(p_7\)在“更上面”,不是吗?维护下边界也是一样:如果当前的点\(p_1\)在下边界末尾两点构成向量\(\overrightarrow{p_5p_2}\)的右侧,就把当前点加入下边界;如果当前的点在向量左侧,就把下边界末尾的点弹出,直到没点可弹或满足点在向量右侧为止。

判断点和向量(其实这里是有方向的直线)的位置关系,如果你已经看过我的[计算几何] 1 二维基础运算与概念 - 跳岩 - 博客园 (cnblogs.com)的话,应该可以已经非常轻松的写出来代码了罢()

// 判断直线a和点b的关系
// 1: a在直线左边; 0: a在直线上; -1: a在直线右边 
int relation(Line a, Point b){
    return sgn(cross(a.v, b - a.p));
}

代码实现

[P2742 USACO5.1] 圈奶牛Fencing the Cows /【模板】二维凸包 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

现在我们就以这道题为例,看看怎么做一个二维凸包。

andrewScan的伪代码如下,真的代码其实就多了一点点边界的判断。注意看,我们给点排序的时间是\(O(n\log n)\),找上下边界的时间是\(O(n)\),故总的时间复杂度为\(O(n\log n)\)

Polygon andrewScan(Polygon p){
    sort(p.begin(), p.end()); // 给点排序
    Polygon u, d;			  // u: 上边界; d: 下边界
    
    u.push_back(p[0], p[1]); 			// 把最左边的两个点push到上边界
    d.push_back(p[end], p[end - 1]);    // 把最右边的两个点push到下边界
    
    // 找上边界
    for (int i = 2; i < p.size(); ++i){
        while (u.size() >= 2 && p[i]不在u末尾向量的右边) 弹出u末尾元素;
        u.push_back(p[i]);
    }
    // 找下边界
    for (int i = end - 2; i >= 0; --i){
        while (d.size() >= 2 && p[i]不在u末尾向量的右边) 弹出d末尾元素;
        d.push_back(p[i]);
    }
    
    // 现在找到了上下边界, 我们把结果合并起来
    // 本人喜欢逆时针存储
    result = merge(d, u, anticlockwise);
    return result;
}

AC的代码如下:

#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define ZERO 1e-8
#define xx first
#define yy second
#define RIGHT -1
#define ON 0
#define LEFT 1

int sgn(double x){
    if (fabs(x) < ZERO) return 0;
    return x > 0 ? 1 : -1;
}

struct Point{
    double x, y;
    Point (double _x = 0, double _y = 0) : x(_x), y(_y) {}
    Point operator + (Point& B) { return Point(x + B.x, y + B.y); }
    Point operator - (Point& B) { return Point(x - B.x, y - B.y); }
    bool operator < (Point& B) {
        if (!sgn(x - B.x)) return y < B.y;
        return x < B.x;
    }
    friend ostream& operator <<(ostream& out, Point& p) { out << "(" << p.x << ", " << p.y << ")"; return out; }
    friend void operator >>(istream& in, Point& p) { in >> p.x >> p.y; }
};
typedef Point Vector;
typedef pair<Point, Point> Line;
typedef vector<Point> Polygon;

double cross(Vector v1, Vector v2){
    return v1.x * v2.y - v1.y * v2.x;
}

int relation(Line l, Point p){
    return sgn(cross(l.yy - l.xx, p - l.xx));
}

double get_dist(Point p1, Point p2){
    return sqrt((p1.x - p2.x) * (p1.x - p2.x) + (p1.y - p2.y) * (p1.y - p2.y));
}

Polygon andrewScan(Polygon p){
    Polygon u, d;
    // 如果p的点少于3, 可以直接返回了
    if (p.size() < 3) return p;
    sort(p.begin(), p.end());

    u.push_back(p[0]), u.push_back(p[1]);
    d.push_back(p.back()), d.push_back(p[p.size() - 2]);

    for (int i = 2; i < p.size(); ++i){
        int k = u.size();
        while (k >= 2 && relation(Line(u[k - 2], u[k - 1]), p[i]) != RIGHT){
            k--; u.pop_back();
        }
        u.push_back(p[i]);
    }
    for (int i = p.size() - 3; i >= 0; --i){
        int k = d.size();
        while (k >= 2 && relation(Line(d[k - 2], d[k - 1]), p[i]) != RIGHT){
            k--; d.pop_back();
        }
        d.push_back(p[i]);
    }

    // 这里就是做了逆时针合并, 记得u.st=A, u.ed=B, d.st=B, d.ed=A
    // 所以合并的时候还要注意不要重复存储两遍A和B
    reverse(d.begin(), d.end());
    for (int i = u.size() - 2; i > 0; --i){
        d.push_back(u[i]);
    }

    return d;
}

int n;
const int N = 1e5 + 5;

int main(void){
    cin >> n;
    Polygon pg;
    Point p;
    for (int i = 0; i < n; ++i){
        cin >> p.x >> p.y;
        pg.push_back(p);
    }
    Polygon res = andrewScan(pg);
    double dist = 0;
    for (int i = 0; i < res.size(); ++i){
        dist += get_dist(res[i], res[(i + 1) % res.size()]);
    }

    cout << fixed << setprecision(2) << dist << endl;

    return 0;
}

小小的改动

  • 注意这段代码:
for (int i = 2; i < p.size(); ++i) {
	int k = u.size();
	while (k >= 2 && relation(Line(u[k - 2], u[k - 1]), p[i]) != RIGHT) {
		k--; u.pop_back();
	}
	u.push_back(p[i]);
}

我们的代码中,认为如果当前点不在向量的右边,就要把上边界末尾的点弹出。也就是说,如果\(p_1,p_2,p_3\)三点共线,我们最终会把\(p_2\)弹出,就像下面这样:

如果我们遇到需要保留\(p_2\)的情况,那只要把!=RIGHT改为== LEFT就好啦。

for (int i = 2; i < p.size(); ++i) {
	int k = u.size();
	while (k >= 2 && relation(Line(u[k - 2], u[k - 1]), p[i]) == LEFT) {
		k--; u.pop_back();
	}
	u.push_back(p[i]);
}
  • 而且,我们的代码也是经得起一些hack的:
if (p.size() < 3) : 返回的是p;
if (p中所有点都是三点共线): 返回p的左端点和右端点;

好!水完了!有什么表达不清的地方请联系我哦!

posted @ 2023-07-31 16:15  跳岩  阅读(343)  评论(2编辑  收藏  举报