Graham Scan算法的流程
1、预处理
Graham Scan首先要做的是一个预处理排序操作(presorting)。即找到某个基准点,然后将其余所有的点按照相对于基准点的极坐标排序。如下图:
以点1为基准点,其余点按照相当于点1的极角依次排序为2、3、4……理论上讲任何一个点都能当第一个基准点,为了简化算法通常选择lowest-then-leftmost point(LTL)作为基准点。
将起始边 与 剩下的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])。也就下图红色标注的三个点:
举例
1、先来看一个最简单的例子,即点集S中所有的点都在凸包边界上。如下图:
现在要关心S[1], S[0]和T[0],就是点1,2和3。点3位于边12左侧,to left关系为true,S.push ( T.pop() ),向前拓展了一条暂定极边。
接下来重复上述过程。考虑点2,3和4。to left关系为true,S.push( T.pop() )……最终栈T空,算法结束,凸包由栈S自底向上得到。S和T的变化过程如下图:
2、上面列举了最简单的情况下Graham Scan的过程,接下来列举一个更有代表性的实例深入算法的细节。输入的点集S,并进行预处理排序,并初始化栈S、T,如下图:
接下来对点1,2和3进行to left测试,本质上就是判断边2→3(图中黄色边)能否被暂时采纳。测试结果为true,暂时采纳边2→3,S.push( T.pop() )。如下图所示:
接下来关注点2,3和4,来判断下一条黄色边3→4能否被接纳。to left测试为true,S.push( T.pop() ),接纳边3→4。如下图右侧所示:
-
算法经历了无效操作,进行了回溯,得到了目前来说最优的“极边”。虽然这些”极边“不一定能最终保留,但问题的规模得到了削减。
-
验证算法的正确性
思路上的正确性
Graham Scan过程就是一个个引入点的过程。每当我们得到第k个点的时候,算法所得到的就是前k个点对应的“最好的凸包”。因此当k = n时得到的是整体的凸包。
归纳的第一步就是证明k = 3时得到的是当前点集S‘ = {1,2,3}中的极边——也就是证明第1张骨牌会倒。
显然边1→2是S’的一条极边。而根据预处理的方式,3相较于1的极角一定大于2,因此点3一定在边1→2的左侧,因此边2→3会得到保留。对于这三个点来说,任意两条边一定都是极边,2→3也是一条极边。
-
预处理的方式是对2~n所有点相较于点1按极角排序,因此下一个要处理点k+1一定出现在线1→k的左侧,也就是下图蓝色区域和绿色区域(假设k = 9):
而根据目前接纳的最后一条极边( k-1)→k (例如图中8→9)来划分,点k+1可能出现的区域又分为两块,即该极边的左侧(绿色区域)和右侧(蓝色区域)。这也正对应于算法判定的两个分支。
表述方式的正确性
预处理操作的必要性
上图中从点1开始出发进行to left测试,可以发现,每次判定结果都为true,最终所有的点都被保留了下了,而显然这并不是一个凸包。因此presorting是整个算法成立的基础。
分析复杂度
上面证明了Graham Scan算法的正确性,接下来分析其复杂度是否满足O(nlogn),实现所谓的最优算法。
直观上无法断定Graham Scan是一个最优的算法,尤其是以下极端情况令人质疑其效率:
所以算法的总体复杂度:O(nlogn + n * ?) ,可见scan的复杂度决定了算法总体的复杂度。
其实上述分析并非错误,只是不够精确。O(n^2)确实是Graham Scan算法的一个上界,但是这个上界并不是紧的。
-
通过观察可以发现,从图论的角度看,所有的黄色边和紫色边连在一起构成了一张平面图,也就是它们互相是不可能内部相交的。平面图的一个重要性质:平面图中所有边的数目和顶点数目保持同阶
算法推广
Graham Scan算法不仅可以用于凸包构造问题,在其他许多场景下中也十分有效。为了推广Graham Scan算法,首先可以对其做简化,以方便利用在其他问题。
首先再来回顾一下预处理排序,这是算法成立必不可少的一步。排序算法套用成熟的方法即可,利用数学方法计算偏角不仅复杂而且引入了误差,所以要采用to left test。要做的就是两点:
1、原始点集已经有某种次序
有时候我们并不是从零开始构造凸包,例如得到的待处理点集已经是有某种次序的(比如已经按x坐标大小排序,如下图)。
这种情况也不一定非得进行persorting构造新的次序,通常改变观察的角度,换一种理解方式就能免去预处理而直接进行后面的线性的scan操作了。
注:对 “考虑y轴负方向无穷远一个点,所有的点相对于这个点的极角排序恰好就是各点的x坐标序!” 这句话的理解:
也是如此。考虑一个在y轴正方向无穷远的一个点,以此为起点进行scan,最终得到lower hull:1→4→7。最后将两个凸包合二为一即可。