在国外某知名网站上浏览信息时发现了一篇非常好的论文,因为是英文的,自己翻译、整理了一下,如果想看原始的可以去以下链接:https://www.codeproject.com/Articles/1210225/Fast-and-improved-D-Convex-Hull-algorithm-and-its
- 介绍
这篇文章是关于一个相对较新和未知的凸包算法及其实现。这个新的算法有很好的性能,本文给出了很多实现的变化和/或优化。本文包含详细的解释,代码和基准,以便读者容易理解和比较结果与最流行的实际凸包算法及其实现。你会在这里找到真正的工作和测试代码。本文结合:代码,算法和数学。这是一个有点专业化,但我希望你会在这里找到一些有趣的东西。
不断优化的过程
最初的原因是展示了用C ++完成的Ouellet算法的新开发实现。根据提供的性能测试,新实现比C(也提供)中的Chan实现快了约4倍。 使用AVL树作为Convex Hull点而不是基于数组的容器来添加和显示Ouellet算法的另一个实现。有2个实现基于AVL树:一个简单的和更有效的一个与许多增加的优化。 解释AVL版本中添加了哪些优化以获得更好的性能(Ouellet AVL v2 vs Ouellet AVL)。 要共享我的工作台工具,以轻松比较二维算法,例如使用Convex Hull算法或任何其他以点为输入并生成点作为输出的算法。例如,工作台也可以用来测试最小的封闭圆算法(提供的代码)。 为了显示和记录我的算法的第一次迭代的一些改进,使得我的C#Convex Hull实现比用C编写的Chan算法的Pat Morin实现(至少用于我用于测试的所有“常规”随机点生成器)更快。 为了展示许多现实生活中的凸包算法实现并比较它们的性能。 为了显示一组点(提供的代码)的所有排列可能性的生成器的最快实现。 分享其他的想法/发现(多线程,维基百科,最小的围圈,...)。
什么是凸面船体
定义
根据维基百科:“在数学中,凸包或凸信封一组X点在欧氏平面或欧几里得空间是最小的凸集包含X,例如,当X是一个有界平面的子集,该凸形船体可以被形象化为围绕X延伸的橡皮筋所包围的形状。
- 凸壳在2D和3D中的应用
凸Hull算法是计算几何中的一个基本算法,在计算几何的基础上提出了许多算法。还有很多应用程序使用Convex Hull算法。
在许多地方使用凸面船体,所有点所占据的空间周围的路径成为一个有价值的信息。有一些例子:
确定他们的网络(IEEE)的电力公用事业仿真的阻抗区域。这就是为什么我几年前遇到凸包的原因。
它可以用于输入其他算法,以提高其性能。例如,通过快速凸包算法实现预处理点,通用的最小封闭圆算法可以大大提高其性能。在提供的基准中可以显示性能差异。
凸壳可用于图像处理。
在GIS系统中用于创建一个包含多边形的要素类,该多边形表示包含每个输入要素或每组输入要素的指定最小边界几何。
还有其他许多... 在Quora上的11个应用程序
凸壳的发现也在大学中被广泛使用和研究。
- 凸包算法的性能和复杂性
有许多不同的复杂度的凸包算法。维基百科 - 凸浮体算法列出了很多。Ouellet Convex Hull(或Liu and Chen)算法不在维基百科中。有关更多信息,请参阅本文末尾的附录B - 维基百科。算法性能的一个很好的指标通常与其复杂性有关。建议选择一个复杂度最快的算法。在凸包的情况下, 速度测试结果 证实了这个经验法则。目前,最有名的凸包算法的复杂性顺序是O(n log h)其中:
“n”是所有源点的数量
“h”是所有源点中的结果点的集合,其限定了凸包的路径。
根据测试和个人经验,在一般用例中,“h”比“n”小很多。例如,在我测试一个圆圈内的200 000 000个随机点的情况下,凸形船体通常由200到600点的普通随机发生器(圆圈或者扔掉)组成。考虑到它存在复杂度为O(n^2)的算法),O(n log n)和O(n log h)。很容易认识到,基于“log h”而不是“log n”的复杂性会大大提高性能。但是也有可能有两种算法在相同的复杂度下比另一种算法快得多。这是这篇文章的主要原因。但是,正如您将会看到的那样,您可以使用提供的代码来测试自己,一般情况下的结果表明Ouellet算法是最快的算法,并且有相当好的余量。它也有其他的优点,取决于你的需求。比较时,还应该考虑算法实现的语言。在实际的测试结果中,你必须考虑到C#的声誉比C慢。我个人的测试似乎是根据C#比C ++慢的事实。
- Ouellet凸包算法
算法的主要思想
这是一个快速的总结。更多的信息可以在一个凸的Hull算法及其在O(n log h)中的实现中找到。
该算法有三个主要步骤按顺序发生:
第1步 - 确定虚拟象限边界
我们找到了边界(左,上,右,下)
或所有输入点(所有输入点的第一遍)。
我们定义了4个基于边界点的虚拟象限(Q1是基于顶点和最右边的点,其根基于顶点的x和最右点的y)。同样的事情适用于每个象限。
每个船体点将保持每个象限(虚拟象限),并将始终订购。注意:在所有提供的实现中,对于每个象限,所有的点都是逆时针存储的。
第2步 - 找到船体点
对于每个输入点,我们会发现它是否是象限的一部分,如果应用则插入为凸面点。
使用二分法,我们搜索输入点应该插入的地方,如果适用。
如果它是凸包的一部分,我们插入输入点。如果是这种情况,我们也删除新的凸面点使其失效的任何邻居。
第3步 - 合并象限结果
合并四象限船体点的结果,消除在象限边界的任何重复
- Ouellet VS.的区别 Liu和Chen凸包Hull算法
在进入凸包Hull优化之前,应该清楚的是,Liu和Chen凸包Hull算法和Ouellet算法是基于相同的原理:虚拟象限,至少根据我所理解的刘和陈的文章:https://rd.springer.com/article/10.1631/jzus.2007.A1210。刘和陈的文章没有代码或实现细节和/或链接到任何这些。因此,我决定重新命名我的第一个执行代码:Liu和Chen。自那以来,增加了许多优化,提高了性能。包含的基准结果显示了这些差异。
- 优化
优化之后是可以应用于一个或多个Ouellet凸包算法的可选优化。有些仅在Ouellet CPP版本中使用,而其中大部分都应用在Ouellet AVL v2中。刘和陈的执行都没有包括在内。
Slope VS Cross Product
结果
Cross Product在性能上的使用结果略微有所增加。只是在某些情况下击败Chan算法。但它也有简化潜在凸包候选人的选择的效果。对于所有象限来说,交叉乘积可以完全相同,从而实现三件事情:
通过提高每个象限基类的点选择,简化和更好的代码架构(更通用)。
通过将斜坡的缓存管理移除为点结构来简化
通过从点结构中去除缓存的坡度来减小占地面积。可以使用已经定义的框架点结构。
基准测试结果:
根据包括的基准测试,乘法比分数快1.14。要了解更多关于测试条件的信息,请参阅用于测试的硬件。2之间的速度差异部分解释了为什么跨产品有点快。许多其他变量应该被认为是CPU流水线,内存邻近等等。
- 快速丢弃(QD)
快速丢弃是“Liu和Chen”算法和Ouellet算法之间存在的第一个区别。这是一个简单的检查,添加到Ouellet算法,这是在二分法搜索的每一次迭代完成的,在大多数情况下,这将快速停止搜索,因为该点将被拒绝,而不必进一步进入二分法搜索,并且不必做任何计算。这个优点来自于我们使用象限的事实,而且因为象限凸壳点总是被排序的。一般来说,做这个简单测试的时间将会节省执行二分法步骤的时间,因为可以在第一步中快速丢弃一个点。快速丢弃是可能的,因为二分法合并到插入点。如果我们考虑到“h”(凸面点数)一般来说,比“n”(输入点数)要少很多,大多数情况下应该会节省时间。换句话说,应该抛弃很多的观点,而且很快就有抛弃的机会。
刘和陈和Ouellet之间已经做了两个最有价值(更接近现实生活)的随机生成器的测试:扔掉和圈。刘和陈没有QD而Ouellet有QD。在不好的情况下(扔掉点随机发生器),QD只是慢一点点(可以忽略不计),对于任意数量的输入点都相当稳定。在好的情况下(圆点随机发生器),QD在10万点上快了1.2倍,性能随着点数的增加而缓慢下降。如果没有QD,这可能会更糟糕,但如此轻微,这是一个很好的补充,并应改善一般用法的性能。提供的基准测试可以轻松完成。
- 相对象限检查(OQC)
在Ouellet算法中,由于象限定义的方式,两个相邻象限不能有任何交集区。奇数和偶数是互斥的,而对面的两个象限可以共享一个区域。
在算法的第一个版本中,点象限归属是基于象限根点计算的。相反的象限可以有一个相交区域(见下面的例子)。因此,该算法应该对交叉点中的任何点执行相反的象限检查(OQC)。OQC来源于检查其对象象限点的必要性,尽管它已被确定为特定象限的一部分。这种情况只发生在一些具体的点分布,但应该考虑在内。
以下示例显示了分发强制算法执行相反象限检查的情况。在这个例子中,根据每个象限根点,P1同时是2个象限(Q1和Q3)的一部分。这种情况主要发生在点分布狭窄的对角线上时:
在这里,我们有粉红色的虚拟Q1和绿色的Q3,每个象限的根点为菱形,与象限的颜色相同。正如你所看到的那样,P1(黄色区域)和Q3(绿色区域)包含了黄色的点,它应该被评估,看它是否是凸面船体的一部分。但是如果我们要对P1进行P1评估,并且在没有对Q3进行验证的情况下将其丢弃,那么我们就会抛弃一个好的候选者。为了不错过任何象限检查,懒惰的方法是检查每个象限的每个点。但是,只有当一个点被确定为象限的一部分时,才能通过仅在相反的象限上进行验证来轻松地进行优化。在大多数的算法实现中,每个点都要针对所有可能的象限进行检查,以确保不会错过任何象限中的任何点。但是在Ouellet CPP和Ouellet AVL v2中,关于Ouellet Convex Hull的每个实现的具体信息)。我们可以很容易地看到奇数和偶数象限是相互排斥的。换句话说,如果一个点是Q1或Q3的一部分,它不能成为Q2或Q4的一部分,反之亦然。
- 相对象限检查旁路(OQCB)
另一个优化是在任何二分步骤中一个点被估计为凸凸点(不仅是象限的一部分),而是不需要对任何其他象限进行检查,或者是相反的。它不能同时作为2个象限的凸包点的一部分。对一个相反象限的检查只对一个被确定为一个象限的一部分而没有被确定为潜在的船体点的点是强制性的。只有象限极限点可在其中将在3个合并成只有一个点,同时2个象限RD算法的步骤。该优化只在Ouellet CPP和Ouellet AVL v2中完成。
- 不相交象限检查(DQC)
PCZ
第一个和最后一个点总是按照每个象限逆时针定义的。PCZ是“潜在的候选区域”,由第一个和最后一个象限点确定。这是一个象限的第一个和最后一个点右边的区域。该区域每个象限都是唯一的,并且定义了可能添加的候选区域。该区域外的任何其他地方都不能包含此象限的任何有效船体点。
DQC使用PCZ
有一个很好的优化,也可以做,只有在Ouellet AVL v2中使用。开始时要验证所有象限是否不相交。为了验证,我们只需要检查一个象限的根点是否在它的相对象限PCZ之外。如果是所有象限都是这种情况,那么如果一个输入点位于一个象限内,我们可以跳过任何其他象限检查,或者是相反的象限检查。
这个优化需要2件事情:
一种验证做前2次的算法的步骤,以确定是否象限是不相交的。
为2个不同的代码第二取决于如果象限是不相交的或不是算法的步骤:
如果不相交。使用DQZ
如果不相交,则使用原始算法代码
- PCZ点属(PCZB)
PCZ是在不相交的象限检查开始时解释的。使用象限PCZ来确定当前点象限归属。
为了快速保留/丢弃潜在的候选点,我们可以通过使用交叉乘积而不是使用象限根点来检查它是否是目标象限PCZ的一部分。做交叉产品有一个计算成本,但使用它可以防止我们做很多二分迭代。它也有一个优点,就是在一个象限的PCZ中,一个点不能是任何其他象限的一个点。Ouellet AVL v2使用这个优化。
- 前象限(PQ)
另一个只在“Ouellet AVL v2”中进行的优化,就是总是从最后一个添加点开始象限检查,然后按顺序循环剩下的象限。在某些情况下,这种优化可能是有用的,在这种情况下,连续的评估输入点具有很高的邻居风险,因此成为同一象限的一部分。这样做也可以使我们跳过不必要的其他象限检查,提高性能。虽然这种优化在随机点放置方面没有任何优势,但也不会增加任何缺点。这是一种优化,在某些情况下可能会更好,对其他所有方面都没有影响。
- 算法比较
- 时间复杂度分析