点定位:如何拆分更新梯形图和二分搜索结构 ·(一):总思路分析
1. 背景介绍
点定位(Point Location)是计算几何中非常重要的基础算法,一个最常用的应用就是GPS定位,在定位查找中,我们会用到点定位的算法进行快速查找。而且通过学习点定位算法,大家应该会见识到如何在空间内进行二分查找,以及如何创建这样一个支持在空间中进行二分查找的数据结构(即Search Structure,SS)。在此之前,我们一般接触的二分都是在一个数组中进行查找。
这里不涉及算法本身的讲解,我在这里给到点定位的两个核心算法的伪代码12,至于算法本身,大家可以参考邓俊辉老师在edX上面的计算几何课程:计算几何 | Computational Geometry。这里着重讲解一下梯形图(Trapezoidal Map)和二分搜索结构(Search Structure)的拆分和更新,以及实现层面的细节。
// 这个算法是用来建立Trapezoidal Map 和 Search Structure
Algorithm TRAPEZOIDALMAP(S)
算法 TRAPEZOIDALMAP(S)
Input. A set S of n non-crossing line segments.
输入:一组共n 条互不相交的线段
Output. The trapezoidal map T(S) and a search structure D for T(S) in a bounding box.
输出:梯形图T(S),以及与之对应的、限制于一个包围框之内的查找结构D
1. Determine a bounding box R that contains all segments of S, and initialize the trapezoidal
map structure T and search structure D for it.
1. 构造一个包围框R,其大小必须足以容纳S中的所有线段根据R,初始化相应的梯形图结构T 以及查找结构D
2. Compute a random permutation s1, s2, …, sn of the elements of S.
2. 将S 中的所有线段随意打乱,得到一个随机序列:s1, s2, …, sn
3. for i <- 1 to n
4. do Find the set D0, D1, ..., Dk of trapezoids in T properly intersected by si.
4. do 在(当前的)T中,找到与si真相交①的所有梯形Δ0, Δ1, …, Δk
5. Remove D0, D1, ..., Dk from T and replace them by the new trapezoids that appear
because of the insertion of si.
5. 将Δ0, Δ1, …, Δk 从T 中删去将它们替换为由于si 的引入而新生出来的若干梯形
6. Remove the leaves for D0, D1, ..., Dk from D, and create leaves for the new trapezoids.
Link the new leaves to the existing inner nodes by adding some new inner nodes, as explained
below.
6. 将与Δ0, Δ1, …, Δk 对应的叶子从D中删去对应于每个新生成的梯形,生成一匹新的叶子将新生出的叶子与已有的内部节
点相联接 (* 正如下面要介绍的,为此可能需要加入一些新的内部节点 *) 接
// 这个算法是在Search Structure中查找本次更新需要拆分和更新的梯形节点(叶节点)
Algorithm FOLLOWSEGMENT(T, D, si)
算法 FOLLOWSEGMENT(T, D, si)
Input. A trapezoidal map T, a search structure D for T, and a new segment si.
输入:梯形图T,与T 相对应的查找结构D,以及新近引入的一条线段si
Output. The sequence D0; : : : ;Dk of trapezoids intersected by si.
输出:由所有与si 真相交的梯形(自左向右)组成的一个序列:Δ0, …, Δk
1. Let p and q be the left and right endpoint of si.
1. 分别令p 和q 为si 的左、右端点
2. Search with p in the search structure D to find D0.
2. 在查找结构D 中对p 进行查找,最终找到梯形Δ0
3. j <- 0;
4. while q lies to the right of rightp(Dj)
4. while (q 位于rightp(Δj)的右侧)
5. do if rightp(Dj) lies above si
5. do if (rightp(Δj)位于si 的上方)
6. then Let Dj+1 be the lower right neighbor of Dj.
6. then 令Δj+1 为Δj 的右下方邻居
7. else Let Dj+1 be the upper right neighbor of Dj.
7. else 令Δj+1 为Δj 的右上方邻居
8. j <- j+1
9. return D0, D1, ..., Dj
对于edX上面的视频课程,用绿色√标注的章节为必看章节,这些视频对理解Point Location算法是至关重要的,其余未标注章节为拓展部分:
2. 梯形结构(Trapezoid)
2.1 定义和数据结构
首先,我们先来认识一下梯形的结构和它的数据结构。梯形是由两部分组成:1)自身的结构;2)邻居。梯形自身由四个部分组成:1)top; 2)bottom; 3)leftP; 4) rightP,前两者为线段,后两者为端点。那这样定义的梯形会有几个邻居呢?答案是4个,分别是:1)upperLeftNeighbor;2)lowerLeftNeighbor;3)upperRightNeighbor;4)lowerRightNeighbor。大家可以通过下面的图例来进行理解:
另外,大家可以注意到,通过左右邻居我们可以任意找到给定梯形的所有有关联的梯形,通过这样的方式,我们就把整个梯形图连接在一起,形成一个完整的subdivision,所以之后我们在实现的时候,我们不会使用在《三角拆分》中使用的DCEL(虽然也可以用,但是非常复杂和麻烦),而是使用以上图为根据的特殊数据结构。在进入到正式讲解之前,我们还需要明确一个假设,我们的输入没有下面两种退化情况:
- 有两个端点的x-坐标相同;
- 查询点与各x-节点会落在同一条垂线之上,也会正好落在与某个y-节点相对应的线段上;
这两者情况会出现一个梯形有超过4个邻居的情况,比如下图这样1:
我们可以看到梯形(ii)至多有5个左邻居。这两种特殊情况需要用到对偶数(Dual Number)来进行处理,数学浓度太高,有兴趣的童鞋可以参考教材:6.3 退化情况的处理,我在这里就不进行讨论了。
2.2 代码分析
首先我们来看看梯形(Trapezoid)的数据结构代码:
public class Trapezoid {
private static int IDStatic = 0;
// initializing ID, starting with 0
public final int ID;
// ID in the search structure as leaf node
// in pre-traversal ordering, starting with 1
public int leafID = -1;
// pointer to th leaf node in SS
public SearchVertex vertex;
public Vector leftP;
public SearchVertex leftSear;
public Vector rightP;
public SearchVertex rightSear;
public Line top;
public Line bottom;
// TODO: 10/31/2021 handle the two degenerate cases with dual number
public Trapezoid upperLeftNeighbor;
public Trapezoid lowerLeftNeighbor;
public Trapezoid upperRightNeighbor;
public Trapezoid lowerRightNeighbor;
// other code
}
数据域里面有我们之前提到的四个定义梯形的成员变量:
public Vector leftP;
public Vector rightP;
public Line top;
public Line bottom;
以及四个邻居成员变量:
// TODO: 10/31/2021 handle the two degenerate cases with dual number
public Trapezoid upperLeftNeighbor;
public Trapezoid lowerLeftNeighbor;
public Trapezoid upperRightNeighbor;
public Trapezoid lowerRightNeighbor;
这里大家需要注意这个成员变量:
// pointer to the leaf node in SS
public SearchVertex vertex;
这个成员变量是用来表示当前梯形是在SS中的哪个叶节点,这样我们能在O(1)时间内,进行拆分和更新操作。
3. 总思路分析
当我们每添加一条线段的时候,对整个梯形图和SS的拆分和更新有两种情况(教材上两种,但其他地方可能会把后一种看成两种,一共三种情况)1。
3.1 线段横跨一个梯形内部
我们可以看到,整条线段都落在某个梯形内部,所以我们可以把原来的梯形拆分左右上下四个部分。对应的,SS中我们只要把原来的梯形节点替换成相应的节点即首先是Pi(线段左端点),以及它的左孩子A(左梯形),右孩子Qi(线段右端点),然后是Qi的左孩子Si(线段),右孩子B(右梯形),最后是Si的左孩子C(上梯形),右孩子D(下梯形)。至于具体的拆分顺序,大家可以参考下面的流程图:
大家可以看到,我们先插入Pi,把原来的梯形拆分成左右两个部分(笔者称这种拆分为“左拆分(left separation)”),之后我们把右边的梯形传给处理Q事件(插入Qi)的方法,因为左边的梯形已经被固定下来。同样,这个方法会把传进来的梯形拆分左右两个部分(笔者称这种拆分为“右拆分(right separation)”),但是这次我们把左边的梯形传给处理S事件的方法,因为右边的梯形已经固定了。
最后插入Si,我们把梯形拆分成上下两个部分(笔者称这种拆分为“水平拆分(horizontal separation)”),继而完成梯形图的更新。而对于SS的更新,顺承刚才更新梯形图的思路,我们遵循“从上至下”的顺序,更新方法和上方描述的基本类似,大家可以结合图例理解一下。
3.2 线段横跨多个梯形内部
另一种情况就是线段横跨不止一个梯形的内部,比如下图的情况1:
更新的方法是类似的,只是细节上有不同:
插入类型 | 梯形图 | SS |
---|---|---|
Pi(左端点) | 原梯形拆分成3个部分,左上下,如上图中的梯形Δ0① | 原节点替换成5个节点Pi,Si, 左上下三个梯形节点 |
Qi(右端点) | 原梯形拆分成3个部分,右上下,如上图中的梯形Δ3 | 原节点替换成5个节点Qi, Si, 右上下三个梯形节点 |
Si(线段) | 原梯形拆分成2个部分,上下,如上图中的梯形Δ1 | 原节点替换成3个节点Si, 上下两个梯形节点 |
①这里左梯形不存在,因为Pi和之前某条线段的左端点重合
同样,这里我们给到具体的拆分顺序流程图:
对于横跨多个梯形内部的情况,我们的处理方法和刚才其实极其类似的,只是分为Pi,Si,Qi三种情况。这里可以告诉我们一个非常重要的信息:
横跨一个或多个梯形内部,都可以用同一套代码逻辑进行处理,无需分情况讨论。
对于这个例子,大家应该还注意到了一个细节,也就是Pi和Qi的退化情况:
对于Pi和Qi已经存在在梯形图或SS中(即它们与之前的端点重合),我们不对原梯形进行拆分,直接传递给下一步的事件处理方法。
接下来,我们将进入每个具体事件处理的讲解中,并简要分析一下相应的代码思路。
下一节:(二):处理P和Q
4. 参考资料
- Computational Geometry: Algorithms and Applications
- 计算几何 ⎯⎯ 算法与应用, 邓俊辉译,清华大学出版社
5. 免责声明
※ 本文之中如有错误和不准确的地方,欢迎大家指正哒~
※ 此项目仅用于学习交流,请不要用于任何形式的商用用途,谢谢呢;