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

这篇具有很好参考价值的文章主要介绍了[计算几何] 2 二维凸包/笨蛋(我)也能看懂的二维凸包算法。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

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

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

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

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

那我们开始吧。

目录
  • Andrew算法
      • 算法思想
      • 代码思路
      • 代码实现
      • 小小的改动

Andrew算法

算法思想

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

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

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

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

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

bool operator < (Point& B) {
        if (!sgn(x - B.x)) return y < B.y;
        return x < B.x;
    }
[计算几何] 2 二维凸包/笨蛋(我)也能看懂的二维凸包算法

初始点集\(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\}\)就是这三个点的上边界。

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

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

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

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

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

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

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

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

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

合起来就是凸包:

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

代码思路

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

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

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

[计算几何] 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)

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

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

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\)弹出,就像下面这样:

[计算几何] 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的左端点和右端点;

好!水完了!有什么表达不清的地方请联系我哦!文章来源地址https://www.toymoban.com/news/detail-618062.html

到了这里,关于[计算几何] 2 二维凸包/笨蛋(我)也能看懂的二维凸包算法的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 【外行也能看懂的RabbitMQ系列(三)】—— RabbitMQ进阶篇之死信队列(内含视频演示业务和业务代码)

    准备篇 RabbitMQ安装文档 第一章 RabbitMQ快速入门篇 第二章 RabbitMQ的Web管理界面详解 第三章 RabbitMQ进阶篇之死信队列 第四章 RabbitMQ进阶篇之通过插件实现延迟队列 恭喜所有看到本篇文章的小伙伴,成功解锁了RabbitMQ系列之高级特性 死信队列 的内容🎁通过本文,你将清楚的了解

    2024年02月07日
    浏览(44)
  • 【计算机网络复习之路】数据链路层(谢希仁第8版)0基础也能看懂 !!!

    专栏 :计算机网络复习之路 好了,复习完了上面两章【第一章概述 | 第二章物理层】,我们接着复习数据链路层。 目录 1  数据链路层概述  数据链路和帧 2  三个基本问题 封装成帧  透明传输 差错检测(重点) 3  点对点协议PPP PPP协议的组成 PPP协议的帧格式(重点) p

    2023年04月24日
    浏览(57)
  • 【计算机视觉】YOLOv8参数详解(全面详细、重点突出、大白话阐述小白也能看懂)

    comments description keywords true Master YOLOv8 settings and hyperparameters for improved model performance. Learn to use YOLO CLI commands, adjust training settings, and optimize YOLO tasks modes. YOLOv8, settings, hyperparameters, YOLO CLI commands, YOLO tasks, YOLO modes, Ultralytics documentation, model optimization, YOLOv8 training YOLO 设置和超参数

    2024年02月05日
    浏览(47)
  • Stable Diffusion进阶!姥姥都能看懂的ControlNet超全教程

    前言 Hello,大家好,言川又来写教程啦!!这是一篇继《外婆都能看懂的 Stable Diffusion 入门教程》教程之后的一篇文章,如果你还没有安装并了解 Stable diffusion 这个软件,那么你一定要先去看看入门教程的文章,然后安装 Stable Diffusion。 一、Controlnet(图像精准控制)是什么

    2024年02月09日
    浏览(44)
  • GoogleTest从入门到入门,小白都能看懂的gtest详细教程

    单元测试 项目管理和技术管理中做单元测试,衡量一个软件是否正常的标准,良好的单元测试以及足够多的覆盖率,至少保证关键功能,关键业务的覆盖率接近100%。 gtest是谷歌公司发布的一个跨平台(Linux、Mac OS、Windows等)的C++单元测试框架,它提供了丰富的断言、致命和

    2024年02月07日
    浏览(51)
  • 手把手教你入门Vue,猴子都能看懂的教程

    概述 : 动态构建 用户界面 的 渐进式 框架 动态构建 :虚拟DOM 用户界面 渐进式 作者 特点 声明式 编码、遵循 MVVM 原则 编码简单、体积小 组件化、复用率高、代码维护容易 vue2官网资源 :https://v2.cn.vuejs.org/ 2.1 插值语法 概述 :解析 标签体 内容、可以解析表达式( 可以返

    2024年01月17日
    浏览(47)
  • Java 零基础入门学习(小白也能看懂!)

    📚博客主页:爱敲代码的小杨. ✨专栏:《Java SE语法》 ❤️感谢大家点赞👍🏻收藏⭐评论✍🏻,您的三连就是我持续更新的动力❤️ 1.1.1什么是 Java Java是一种优秀的程序设计语言 ,它具有令人赏心悦目的语法和易于理解的语义。 不仅如此, Java还是一个有一系列计算机软

    2024年01月19日
    浏览(56)
  • STP生成树协议(超详细小白也能看懂)

    目录 一、为什么要用STP 二、STP的作用 三、STP操作 四、STP名词解释: 五、生成树选举办法 六、生成树选举因素 七、根桥选举: 八、根端口的选举 九、端口状态 十、定时器 十一、故障恢复时间         十二、广播风暴 十三、广播风暴危害 十四、BPDU组成 十五、STP的一

    2024年02月09日
    浏览(44)
  • 零基础也能看得懂的AI绘画教程!灵魂画师MJ咒语案例展示!

    当谈到人工智能技术应用场景时,除了ChatGPT,另外让人着迷的应用就是AI绘画,随着图像生成器的不断升级和更迭,图片生成的质量也得到了显著提升。 目前画图支持Midjourney和Stable Diffusion,风景、人物、动物、建筑、梦境等等,都可以用MJ和SD生成,同聊天一样,只有掌握正

    2024年04月23日
    浏览(57)
  • 【概念】区块链中账本是什么?通用区块链平台账本概念介绍,一个谁都能看懂的账本概念

    目录 前言 举个例子 账本在不同链中担任什么角色 联盟链 公有链 私有链 随着区块链的发展,目前国内也掀起了一阵区块链的热潮,无论是金融、信任、交易、溯源等领域都是非常受欢迎,慢慢的我们也将成为第一个吃螃蟹的人,本篇文章主要是与大家一起聊聊什么是区块链

    2023年04月10日
    浏览(38)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包