上一篇文章大致的介绍了一下三角剖分领域的经典,从这一篇开始我将把新提出的算法慢慢的展现在各位面前,用一种不同以往论文式的口吻为大家讲述。
也许你没有接触过这个领域,也许你现在的工作与此领域风马牛,也许你认为这个领域与你注定没有交集。告诉你,一个月前,如果我看到这篇文章也会有同样的想法。我相信,这篇貌似很专业的随笔会让每一位看客都知道我在说什么,并且会给各位一些启迪,更重要的是我将在后续的文章中向大家展示如何将自己的想法用面向对象的方式实现出来,让它实实在在在你面前,让它实实在在为你工作。
下面是一段利用本算法对带洞多边形三角剖分的视频录像,大家先留个印象,等看完本文后再看这个视频会有助于理解。
任意平面多边形,最怕就是任意二字。对于某种特殊的多边形,例如凸多边形,算法往往很容易想出,选出任意一个点分别连接与其不相邻的点不就得了。很遗憾,在我遇到的问题中,凸多边形确实是有的,但是更多的是千奇百怪的多边形,因为这些多边形是用户输入的,我无法预期。在进行了如上篇随笔的分析后,发明一种新算法的要求迫在眉睫。
这种新的算法被我有幸找到,继而才有了这些文字。新的算法由寻找辅助线、寻找相交点、寻找三角形三个大块组成。
一、寻找辅助线
在网上进行了很长时间的搜索,终于在一个论坛的回复中我找到了灵感,决定从这里切入问题,这个切入点正是扫描线(下图的水平绿线)。
在这里我先描述一下扫描线:扫描线是水平的,其纵坐标取多边形某个顶点的纵坐标,因而在上图的多边形中会有八条扫描线,图中显示的是第一条。假定一个多边形顶点数为N,那么扫描线的条数是小于等于N的,在最坏情况下达到N,如果两个或者更多个顶点的纵坐标相同,那么扫描线的数量就会减少一些。寻找这些扫描线的时间复杂度为O(N),遍历一遍顶点就可以了嘛。
细心的你一定发现了左边这条竖直的绿线,我称之为左辅助线,它可有大作用!该线的选定方法是找出所有顶点中的最小横坐标,并将其向左移动一些(这个值任意),它的作用是什么呢,想想看,一会儿再告诉你答案。寻找这条左辅助线的时间复杂度也是O(N)。
二、寻找相交点
上面所作的一切虽然很重要,但只是一个前奏,现在就让我们缓步进入主旋律吧。
这里揭晓左辅助线的作用:左辅助线一定在多边形外,所以让各行扫描线从左辅助线出发,起点一定在多边形外,这样如果扫描线和多边形相交,第一次一定是进入多边形,第二次一定是走出多边形,第三次一定又是进入多边形,第四次……,总之就是第奇数个相交点为进入多边形点,而偶数个相交点为走出多边形点,相交点的个数一定是偶数个(如下图,第四条扫描线共有四个交点)。
各行的交点要保留下来以备下一步使用。建立一张二维表,为每一条扫描线建立一个新行,而每一行中保存该扫描线与多边形的各个交点。这里有一个问题需要注意,由于扫描线是在遍历各个顶点的时候找到的,而各个顶点的纵坐标不可能会降序或升序排列,所以我们有必要将扫描线数组进行一次排序,让他们自上而下排列于二维表中,这步排序时间复杂度为O(N*logN),至此我们的算法时间复杂度已经上升为O(N*logN)。
细心的你又发现了,不对不对,这第一条扫描线不就只有一个交点吗,没错,对它有特殊的处理。
每条扫描线一定会穿过至少一个多边形顶点,原因在于扫描线的选取方法。穿过的这个顶点,实际上是扫描线和两条边线的交点,这是显而易见的,也就是说在求两条连续边与扫描线的交点时得到的是同一个点,当遇到这样的情况后,需要有一些特殊的处理。你会发现,当出现这种情况时,会有两种可能:一种是扫描线经过该点后并没有进入多边形,而是打了个擦边球,又从多边形出去了,上图中最上面的一个点正是这种;另一种是扫描线通过该顶点后从多边形外进入了多边形,上图中第二行最左边的一个点正是这种。这当然要区别对待,如果是第一种情况,我们把它想象为扫描线进入了多边形并同时走出了多边形,我们将该点加入到二维表中两次,而第二种情况我们认为它就是个普通的点,只加入一次即可。至于他们是怎样通过计算区别开来的我们放到《第六回:寻找交点,离胜利就剩一步 之 开找》中再详细讲述。
值得注意的是,上文说的打擦边球同样会有第三行中间那个点的情况,它打了个擦边球没有走出多边形,所以更准确的:第一种情况是扫描线通过多边形某顶点后没有改变与多边形的位置关系(内外);第二种情况是扫描线通过多边形某顶点后改变了与多边形的位置关系。
每条扫描线都要与多边形的所有边求一次交,用得到的交点填充二维表,由于边的条数与多边形顶点数相同,所以该步所需时间复杂度O(N^2)。完成该步后我们得到了一张填充了所有交点的二维表,并且时间复杂度达到了O(N^2)。
二维表的情况我们会在《第六回:寻找交点,离胜利就剩一步 之 纽带》中详细介绍。
三、寻找三角形
终于,我们到达了高潮部分,我们之前的一切努力在这里将化为期望的结果。
在构建上面提到的二维表时,你想到它的作用了么,为什么要是这样的一种结构,每行偶数个交点有什么隐含的意义。再往前想,我们为什么要建立扫描线,它的引入又有什么意义。
二维表中,每行的各个交点都是无序的存放于数组中的,为了我们的分析,我们需要将其每一行的交点都按照横坐标从左到右进行一次排序,每一行中交点数在最坏情况下会达到N,因为可能某条扫描线跟所有边都交个遍,对这一行排序时间复杂度为O(N*logN),又由于有N行,所以这一步时间复杂度为O(N^2 * logN),至此该算法时间复杂度上升为O(N^2*logN)。
二维表中每一行都有偶数个交点,而所有位于偶数索引的交点都是进入点,所有位于奇数索引的交点都是走出点,这在上文已经交代过了(第一个交点索引为0),现在我们再提出一个点对儿的概念,也即相邻的两个点构成这样的一个点对儿,点对儿中的这段扫描线一定在多边形内。这样相邻的两条扫描线相对应的两个点对儿就会包围出一部分多边形区域,这个区域的形状一定是梯形,把所有这样的区域全部找出来就会填充整个多边形,梯形再转变为三角形那就是易如反掌了。在寻找梯形的过程中,需要遍历所有的扫描线,在遍历每条扫描线时需要遍历该扫描线中的每个交点,所以这步处理需要的时间复杂度为O(n^2)。
当然在寻找梯形的过程中还会有一些需要特殊处理的情况,我们将在《第七回:寻找三角形,夺取红旗》中详细讲述。
该算法简单易懂,O(N^2*logN)的时间复杂度尚可接受,核心代码大概为300行,在实际应用中的表现还是可圈可点的。下面的几篇文章会就本篇的各个方面深入展开,实现方式运用了很多面向对象的技巧,下一篇文章将详细讲解录像中各个步骤显示的实现方法。