构造凸包-Graham Scan法

Graham Scan算法的流程

假设待处理点集S共有n个点。

1、预处理

  1. 预排序(presorting

    Graham Scan首先要做的是一个预处理排序操作(presorting)。即找到某个基准点,然后将其余所有的点按照相对于基准点的极坐标排序。如下图:

    img

    点的排序可以套用任意排序算法的框架,只是将排序对象由数值变为了平面上的点,而比较器改为to left test实现。

    如何找出第一个点:点1?

    以点1为基准点,其余点按照相当于点1的极角依次排序为2、3、4……理论上讲任何一个点都能当第一个基准点,为了简化算法通常选择lowest-then-leftmost point(LTL)作为基准点。

  2. 找出起始边

    对于与基准点1极角最小的点,也就是图中点2(假设没有三点共线的情况)。将点1和点2作为算法的起始点。

  3. 将起始边 与 剩下的n-2条边分别存储在不同的栈(stack)中:

    算法开始前先将起始点1和2入栈S,其他的n-2个点入栈T,如下图。注意S和T中元素的入栈顺序。至此预处理已经完成。

    Graham Scan用到的数据结构。整个算法非常简明,核心数据结构只有两个,分别记作栈S和栈T。便于理解我们将S和T画成开口相对的形式,如上图。

2、scan操作(此处为逆时针扫描)

完成预处理之后,就能开始算法的核心:scan操作。scan的过程主要关注三个点:栈S的栈顶(S[0])、次栈顶(S[1])和栈T的栈顶(T[0])。也就下图红色标注的三个点:

img

对这三个关注的点,进行检测,检测的框架为:

image-20200314182719216

举例

1、先来看一个最简单的例子,即点集S中所有的点都在凸包边界上。如下图:

image-20200314183056395

image-20200314211052355


2、上面列举了最简单的情况下Graham Scan的过程,接下来列举一个更有代表性的实例深入算法的细节。输入的点集S,并进行预处理排序,并初始化栈S、T,如下图:

20180207215925185

验证算法的正确性

思路上的正确性

了解了算法的整体流程之后,我们再来论证一下算法的正确性。证明一个算法正确性的方法有很多,在此选用数学归纳法。数学归纳法的思想可用多米诺骨牌类比,要做的无非是两件事:证明第1张骨牌会倒;证明如果第n张骨牌会倒则第n+1张骨牌也会倒下。

Graham Scan过程就是一个个引入点的过程。每当我们得到第k个点的时候,算法所得到的就是前k个点对应的“最好的凸包”。因此当k = n时得到的是整体的凸包。

  1. 归纳的第一步就是证明k = 3时得到的是当前点集S‘ = {1,2,3}中的极边——也就是证明第1张骨牌会倒。

    显然边1→2是S’的一条极边。而根据预处理的方式,3相较于1的极角一定大于2,因此点3一定在边1→2的左侧,因此边2→3会得到保留。对于这三个点来说,任意两条边一定都是极边,2→3也是一条极边。

  2. 然后证明:假设已经处理到第k个点,得到的是前点集S’ = {1,2,3,…,k}中所谓“最好的凸包”。根据算法处理方式,接下来从S’’ = {1,2,3,…,k,k+1}得到的结果是否也是正确的——也就是证明第n张骨牌会倒,则第n+1张骨牌也会倒下。

    预处理的方式是对2~n所有点相较于点1按极角排序,因此下一个要处理点k+1一定出现在线1→k的左侧,也就是下图蓝色区域和绿色区域(假设k = 9):

    img

    而根据目前接纳的最后一条极边( k-1)→k (例如图中8→9)来划分,点k+1可能出现的区域又分为两块,即该极边的左侧(绿色区域)和右侧(蓝色区域)。这也正对应于算法判定的两个分支。

至此,算法思路上的正确性已经证明完毕。

表述方式的正确性

接下来还要考虑算法的表述方式是否有漏洞:代码中每次to left test之前并没有判断S栈中是否有≥2个元素。这也可以由预处理的方式来论证。点1选取的是LTL,而点2是相对于点1极角最小的点,这样的做法保证了除了点1和点2之外所有的点一定是在边1→2左侧的。因此算法回溯最多到点2,永远不可能把点2丢弃,S中元素任何时候至少有两个。


Graham Scan算法的正确性论证完毕。

预处理操作的必要性

最后来思考一下预处理操作:presorting。仔细回顾上述论证过程会发现,每一步的正确性都是建立在最初的排序上的。那么这个预处理排序真的是必要的吗?可以来举极端的反例,每次选取下一个点都是随机的,例如下图的路径:

img

上图中从点1开始出发进行to left测试,可以发现,每次判定结果都为true,最终所有的点都被保留了下了,而显然这并不是一个凸包。因此presorting是整个算法成立的基础。

分析复杂度

上面证明了Graham Scan算法的正确性,接下来分析其复杂度是否满足O(nlogn),实现所谓的最优算法。

直观上无法断定Graham Scan是一个最优的算法,尤其是以下极端情况令人质疑其效率:

image-20200314220437069

Graham Scan算法复杂度由三部分决定:

所以算法的总体复杂度:O(nlogn + n * ?) ,可见scan的复杂度决定了算法总体的复杂度。

其实上述分析并非错误,只是不够精确。O(n^2)确实是Graham Scan算法的一个上界,但是这个上界并不是紧的。

问题就出在分析假定了每次都会出现回退高达O(n)个点。

算法推广

Graham Scan算法不仅可以用于凸包构造问题,在其他许多场景下中也十分有效。为了推广Graham Scan算法,首先可以对其做简化,以方便利用在其他问题。

首先再来回顾一下预处理排序,这是算法成立必不可少的一步。排序算法套用成熟的方法即可,利用数学方法计算偏角不仅复杂而且引入了误差,所以要采用to left test。要做的就是两点:

按照这样的流程就能间接地实现persorting。

1、原始点集已经有某种次序

有时候我们并不是从零开始构造凸包,例如得到的待处理点集已经是有某种次序的(比如已经按x坐标大小排序,如下图)。

img

这种情况也不一定非得进行persorting构造新的次序,通常改变观察的角度,换一种理解方式就能免去预处理而直接进行后面的线性的scan操作了。

上半部分凸包:

考虑y轴负方向无穷远一个点,所有的点相对于这个点的极角排序恰好就是各点的x坐标序!也就是将无穷远的点看作起始点①,最右侧点(图中点8)看作点②,进行scan过程直到最左边的点(图中点1)结束,就得到了凸包的上半部分(upper hull),也就是下图的8→7→2→1:

注:对 “考虑y轴负方向无穷远一个点,所有的点相对于这个点的极角排序恰好就是各点的x坐标序!” 这句话的理解:

img

下半部分凸包(lower hull)的构造

也是如此。考虑一个在y轴正方向无穷远的一个点,以此为起点进行scan,最终得到lower hull:1→4→7。最后将两个凸包合二为一即可。

代码实现


发布了34 篇原创文章 · 获赞 10 · 访问量 1万+
展开阅读全文

没有更多推荐了,返回首页

©️2019 CSDN 皮肤主题: 游动-白 设计师: 上身试试

分享到微信朋友圈

×

扫一扫,手机浏览