凸包

承上一节
继续介绍点集的凸包
(下文中所有凸包 若不做特殊说明均指点集的凸包)
这一节介绍相比更高效的算法
====================================================================
一.卷包裹算法(Gift Wrapping Algorithm)的特性
前面提到过卷包裹算法的复杂度问题
由于卷包裹算法是两重循环实现的 因此很好分析它的复杂度
1 while true do 2 begin 3 k:=0; 4 inc(m); ch[m]:=j; 5 for i:=1 to n do 6 if (i<>j)and((k=0)or 7 cmp(p[j],p[k],p[i])) 8 then k:=i; 9 if k=temp then break;10 j:=k;11 end
内部循环N次 外循环的次数决定于凸包上的点数H
所以卷包裹算法复杂度为O(HN) 这个复杂度很有特点
考虑一个随机点集的凸包上的点数往往很少 但是最坏情况下是O(N)级别的
比如所有的点都在一个圆上的时候
 
这就决定了卷包裹算法很适合随机的点集 但是如果是刻意构造的数据或是比较特殊的数据
就会达到O(N^2)的最坏复杂度 这是我们不愿意看到的
下面介绍最坏情况下复杂度更好的Graham扫描算法(Graham Scan Algorithm)
====================================================================
二.Graham扫描算法(Graham Scan Algorithm)
Graham扫描算法维护一个凸壳 通过不断在凸壳中加入新的点和去除影响凸性的点 最后形成凸包
The Graham scan is a method of computing the convex hull of a finite set of points in the plane with time complexity O(n log n). It is named after Ronald Graham, who published the original algorithm in 1972.[1] The algorithm finds all vertices of the convex hull ordered along its boundary.
http://en.wikipedia.org/wiki/Graham_sca
算法主体由两部分组成 先是排序 后是扫描 分块讲解一下
----------------------------------------------------------------------------------------------------------
1.点集排序
为了得到加入新点的顺序 Graham扫描法的第一步是对点集排序
排序是对杂乱的点集进行了梳理 这也是这种算法能够得到更高效率的根本原因
排序的方法也有两种 极角坐标排序(极角序) 和 直角坐标排序(水平序)
前者好理解一些 但是在实现的时候 后者更方便
先说极角序 为了极角排序 我们先得得到一个参考点
一般的 我们取最左边(横坐标最小)的点作为参考点 如果有多个这样的点就取最下面的(纵坐标最小)
看这样一个例子 这是一个任意给出的平面点集:
 
参考点的定义:在横坐标最小的情况下取纵坐标最小的点
所以所有的点只能在这个黄色的半平面中 而且正上方为闭(可取得) 正下方为开(不可取)
这就决定了参考点的性质:点集中任意两点和参考点所成的到角为锐角
这样我们取得参考点 然后再考虑极角排序
 
极角排序以参考点为极角坐标系原点 各个点的极角为关键字
由于上面我们得到的参考点的性质 我们可以设所有点的极角均在(-90,90]之间
排序完成后应该是这样的:
 
比较极角我们仍然可以利用向量的叉积
叉积在这里已经介绍了 http://www.cnblogs.com/Booble/archive/2011/02/28/1967179.html
同样由于参考点的性质 所有向量之间的到角都是在180度以内 不会产生错误
 
----------------------------------------------------------------------------------------------------------
2.Graham的栈扫描
Graham的扫描是一个很优美的过程 用到的数据结构也很简单 仅仅是一个栈而已
核心的思想是按照排好的序 依次加入新点得到新的边
如果和上一条边成左转关系就压栈继续 如果右转就弹栈直到和栈顶两点的边成左转关系 压栈继续
实现的时候我们不用存边 只需要含顺序在栈里存点 相邻两点就是一条边
由于我们时时刻刻都保证栈内是一个凸壳 所以最后扫描完毕 就得到了一个凸包
下面还是继续上面的那个样例 演示一下栈扫描的过程
 
 
 
这样Graham扫描算法基本完成
复杂度是排序O(Nlog2N) 扫描O(N) {每个点仅仅出入栈一次}
合起来是一个O(Nlog2N)的算法 很优秀
----------------------------------------------------------------------------------------------------------
3.双重共线点难题
和卷包裹算法一样 我们同样还要考虑共线点问题
而且Graham扫描算法的共线问题更复杂 所以需要仔细考虑
i).排序时的共线问题
 
如果极角相同 我们应该怎么定先后呢?
我们得加上第二关键字距离 比如极角相同 距离参考点近的先
不过不管是近的先还是 远的先 开始和结束的两条边总是矛盾的
我们必须对其中一条特殊处理 除了结束边外距离近的先 结束边上距离远的先
这就是为什么极角排序不是很好实现的原因了 下面会介绍一下水平序
ii).扫描时的共线问题
这个和卷包裹算法的处理放法如出一辙
如果需要保留共线的点就在到角相同时取距离最近的
如果仅仅需要凸包极点就取距离最远的
----------------------------------------------------------------------------------------------------------
4.直角坐标排序(水平序)
直角坐标排序方法没有了极角排序的不足
以横坐标为第一关键字 纵坐标为第二关键字 排序点集
然后从第一个点开始 分别利用Graham扫描生成左链和右链
需要注意以下两点:
i).左链和右链的旋转方向是相反的
ii).注意生成第二条链要忽略第一条链上的点
由于水平序的CMP函数比较简单 代码也更短
还有一件有意思的事 Graham扫描算法是1972年提出的 卷包裹算法是1973年提出的
其实不奇怪 这两个算法本来也没有优劣 所用的思想不同 各自善于处理不同的情况
Graham扫描所用时间在点集随机时 还不如卷包裹算法快
正如第一节所说 卷包裹算法已经可以处理大部分情况 也是一个可取的算法
实际应用中点集更趋向于均匀 而不是集中在凸包上
----------------------------------------------------------------------------------------------------------
最后给一下比较难实现的极角排序代码
 
 
GrahamSca
====================================================================
三.快速凸包算法(Quickhull Algorithm)
对比Graham扫描算法和卷包裹算法
我们发现 Graham扫描算法在凸包上的点很密集时仍然适用
卷包裹算法在凸包上点集随机分布时是很高效的
那么有没有两个优点都具备的算法呢?
是有的! 快速凸包算法(Quickhull Algorithm)就是这样的一个算法
快速凸包算法是一个和快速排序(Quicksort Algorithm)神似的算法
尽管快速排序的最坏复杂度可以达到O(N^2)
但是有着极小的常数 实现方便 思路优美 绝大多数情况特别高效的快速排序 还是赢得了更多人的青睐
快速凸包算法也是这样 尽管可以构造一个数据使之达到O(N^2)的复杂度
但这需要刻意针对程序经过分析 才能做到 是实际应用中根本不会碰到的情况
在点集均匀分布时 快速凸包的复杂度更是达到了O(N) 是上面两种算法难以企及的
在绝大多数情况下 平均复杂度是O(Nlog2N) 也很高效
快速凸包继承了快速排序分治的思想 这是一个递归的过程
 
 
 
------------------------------------------------------------------------------------
 
 
 
伪代码如下:
1 void 快速凸包(P:点集 , S:向量 /*S.p,S.q:点)*/ ){ 2   /* P 在 S 左侧*/ 3   选取 P 中距离 S 最远的 点 Y ; 4   向量 A <- { S.p , Y } ; 向量 B <- { Y , S.q } ; 5   点集 Q <- 在 P 中 且在 A 左侧的点 ; 6   点集 R <- 在 P 中 且在 B 左侧的点 ; /* 划分 */ 7   快速凸包 ( Q , A ) ; /* 分治 */ 8   输出 (点 Y) ; /* 按中序输出 保证顺序*/ 9   快速凸包 ( P , B ) ; /* 分治 */10 }
初始化就选取 最左下和最右上的点 划分好 然后调用两次快速凸包函数分别求上下凸包
其中 选取和划分都需要用到向量的叉乘 注意方向
另外 存储点集可以用数组里的连续一段 传参的时候就传左右下标
划分的时候就是给数组交换元素 使新的点集 变成连续的一段
这里有一个很好的演示
http://www.cs.princeton.edu/courses/archive/spr10/cos226/demo/ah/QuickHull.html
还要补充说明一下
快速凸包在所有点都在圆周上的时候还是O(Nlog2N) 不过会比Graham扫描算法慢一些
可以说 这种数据是Graham扫描法的最好情况 一遍走完就行了
构造快速凸包的最坏情况就是使划分不均等 和构造快速排序最坏情况一样
贴以下我很辛苦写出来的代码吧 为了好看些 还心血来潮用cpp写了...
 
 
QuickHull
====================================================================
四.凸包算法复杂度下界
(引自<算法艺术与信息学竞赛>)
 
====================================================================
posted @ 2017-12-07 09:22  zhangenming  阅读(613)  评论(0编辑  收藏  举报