6.1 线段求交
首先对地图叠合问题的最简单形式做一讨论。也就是说,(相互叠合的)两个地图层,分别都是由一组线段表示的某个网络。以图为例,可能有若干小比例图层分别存放道路、铁路和河流信息。
这些线段将导出的一些子区域,不过在此我们对这些子区域并不感兴趣。后面将会考虑更为复杂的情况——(相互叠合的)地图不是网络,而是平面经过子区域划分之后导出的、具有明确含义的一些子区域。
6.1.1 为解决网络叠合的问题,首先需要用几何的概念来描述这一问题。
就两个网络的叠合而言,对应的几何条件是这样的:给定由线段组成的两个集合,计算出来自于其中一个集合的所有线段,与来自于另一个集合的所有线段之间的交点。
为了做进一步简化,我们还将把分别来自两个集合的线段合到一起,构成一个集合,然后来考虑其中所有线段之间的交点。
进而将问题定义为:给定由平面上n条闭线段构成的一个集合S,报告出S中各线段之间的所有交点。
乍看起来,这个问题并没有什么挑战性——可以依次检查每一对线段,看看它们是否相交;如果的确相交,就将其交点报告出来。显然,这种直截了当式的算法需要O(n2)时间。就某种意义而言,这个结果甚至是最优的:若如图所示,每两条线段都相交,那么无论采用什么算法,至少都需要Ω(n2)时间。
在实际的环境中,大多数的线段要么根本不与其他线段相交,要么只与少数的线段相交,因此,交点的总数远远达不到平方量级。因此,我们希望得到的算法,其运行时间不仅取决于输入中线段的数目,还取决于(实际的)交点数目。这样的算法,被称为"输出敏感的"算法(output-sensitive-algorithm)。
6.1.2 交点敏感的算法
在找出所有交点的过程中,如何才能避免对所有的线段对进行测试呢?这里必须利用这种情况的几何特性——只有那些相互靠近的线段,才可能会相交;而相距甚远的线段则不可能相交。
设需要对其进行求交的线段构成集合S:={s1,s2,…,sn},首先排除一种简单的情况,如图所示,将一条线段在y轴上的正交投影,定义为它的y区间(y-interval)。
任何两条线段,只要其y区间没有重叠部分——此时,也可以说,它们在y方向上相距很远——它们就一定不会相交。这样,只需要对那些y区间相互有所重叠(即与同一条垂线相交)的线段对进行测试。
平面扫描算法(plane sweep algorithm)
为找出这些线段对,可以想象着用一条直线l,从一个高于所有线段的位置起,自上而下地扫过整个平面。在这条假象的直线扫过平面的过程中,跟踪记录所有与之相交的线段——以找出所需的所有线段对。
在算法中,使用的直线l被称为扫描线(sweep line)。与当前扫描线相交的所有线段构成的集合,被称为扫描线的状态(status)。
只有在某些特定的位置,才需要对扫描线的状态进行更新。我们称这些位置为平面扫描算法的事件点(event point)。就本算法而言,这里的事件点就是各线段的端点。
事件点
只有在扫描线触及某个事件点的时候,算法才会进行实质的处理——更新扫描线的状态,并进行一些相交测试。具体而言:
-
上端点
若事件点为某个线段的上端点,则意味着这条直线段将开始与扫描线相交,因此需要将该线段插入到状态结构(status structure)中。然后,需要将这条线段,和那些与当前扫描线相交的其他线段分别进行测试,确定是否相交。
-
下端点
若事件点为某个线段的下断点,则意味着这条线段将不再与扫描线相交,因此需要将该线段从状态结构中删去。
局限性
按照这样的方式,只需要对那些可能与某条水平直线同时相交的线段进行测试。不幸的是,这还不够——因为,在某些(特殊的)情况下,尽管实际的交点数目很少,却依然需要对平方量级的线段进行测试。
一个简单例子就是,所有线段都是垂直的,而且都与x坐标轴相交。因此,目前的这个算法还算不上是输出敏感的。
问题在于,与同一扫描线相交的两条线段,在水平方向上仍然有可能相距很远。
临近性考虑
当考虑到水平方向的临近性后,可以沿着扫描线,将与之相交的所有线段自左向右排序。
这样,只有当其中的某两条线段沿水平方向相邻时,才需要对其进行测试。这就意味着,每引入一条线段,只需要将其与另外的两条线段(具体地讲,就是与新线段上端点左、右紧邻的那两条线段)进行测试。此后,当扫描线向下推进到某个新的位置时,与某条线段紧邻的邻居有可能会发生变化,此时,也需将它与新的邻居进行测试。
在算法的状态结构中,这一新策略应该有所反映——状态结构不仅要记录与当前扫描线相交的所有线段,而且还要对这些线段排序。
在将这些构思落实为高效的算法之前,需要确定,这种方法正确。验证之前,首先忽略掉一些"棘手"的情况——假设:没有水平线段;任何两条线段最多相交于一点(也就是说,任何两条线段都不会有局部的相互重叠);任何三条线段不会相较于一点。虽然上述假设情况,很容易处理,但是为了更好的论证,目前先置之不理的好。
对于,某线段端点落在另一条线段上的情况,等到扫描线触及相应端点时,这类交点也会被检测。
因此,目前唯一剩下的问题:线段之间的每一交点,是否能被进行检测?
引理
设两条非水平的线段si和sj只相交于其内部的一点p,而且,任何第三条线段都不经过p。则在(扫描线到达)高于p的某个事件点处(时),si和sj必然会彼此紧邻,并因此接受相交测试(于是对应的交点将被发现)。
证明
如图所示,令l为比p略高的一条水平线。只要l与p相距足够近,则沿着l、si和sj必然是紧邻的(更准确地说,没有任何事件点落在我们所取的l上,而且也没有任何事件点夹在l与通过p的水平线之间)。
总而言之,必然存在某个位置,当扫描线到达这个位置时,si和sj是紧邻的。另一方面,在算法开始的时刻,si和sj并不是紧邻的——此时,扫描线的位置比所有的线段都高,故状态结构还是空的。因此,必然存在某个事件点q,在q的位置,si和sj开始变为紧邻的,从而接受相交测试。
事件点添加新成员
就目前而言,事件点既包括(事先就可以确定的)各线段端点,也包括(在算法运行过程中逐步发现的)交点。
在扫描线移动的过程中,要维护一个有序序列,该序列由所有与当前扫描线相交的线段组成。每遇到一个事件点,都会通过一些动作,对状态结构进行更新,并检测出新的交点——具体的处置方法,取决于事件点类型。
-
上端点
事件点为上端点,意味着将有一条新的线段开始与扫描线相交,如下图所示。
新引入的线段,需测试判断它是否与沿扫描线与之紧邻的另外两条线段相交。
只有位于当前扫描线下方的交点,才需要加以考虑;高于扫描线的交点,必然已经被检测过了。
例如,线段si和sk原本是紧邻的,而(在某个时刻)第三条线段sj的上端点出现在它们之间,则此时就必须分别将sj与si和sk进行测试。在检测出来的(最多两个)交点中,只要位于当前扫描线的下方,就是一个新的事件点。在处理完该上端点之后,将继续考虑下一个事件点。
-
交点
只有位于当前扫描线下方的交点,才是待处理的事件点;位于上方的交点,必然是已经处理完成的事件点。
如图所示,有关的两条相关线段就会交换其(沿扫描线)次序。它们各自可能(最多)有一条新的紧邻线段,因此,必须分别将它们与其各自的新邻居进行测试,以找出可能得交点。
假设在扫描线触及sk和sl的交点那一时刻,有四条线段sj、sk、sl和sm依次出现在扫描线上。此后,sk和sl将交换次序,于是需要分别对sl和sj、sk和sm进行测试,以找出它们可能位于扫描线下方的交点,并添加为事件点(注意,该事件点可能已经被发现——例如,两条线段在此前的一段时间内曾经是紧邻的,后来变得不再紧邻,最终又再次变成是相互紧邻的)。
-
下端点
若事件点为某条线段的下端点,则它此前的(一左一右)两个邻居现在就会变成是相互紧邻的,因此需要对它们进行相交测试。若有交点,且交点位于扫描线的下方,则该交点应添加为事件点(注意,事件点可能已被添加)。
如图,在某一时刻,沿着扫描线,有依次相邻的三条线段sk、sl和sm,扫描线继续前移后遇到了sl的下端点。于是,sk和sm将变成是相互紧邻的。
在平面扫描过程中,如下不变性始终成立:(在任何时刻)处于扫描线上方的所有交点都已经被正确地检测出来。
算法涉及数据结构
-
平衡二分查找树(balanced binary search tree)
平衡二分查找树是一种特殊的二叉搜索树,它在插入或删除节点时通过自动调整结构,保持左右子树高度差不超过一定阈值(通常为1)。这种平衡性保证了树的高度始终为O(log n),从而确保查找、插入、删除等操作的时间复杂度稳定在O(log n)。
常见实现包括:AVL树(由苏联科学家Adelson-Velsky和Landis于1962年发明)、红黑树、Treap等。
以下是AVL树的一般性实现:
#include <iostream> using namespace std; class AVLNode{ public: int key; AVLNode* left; AVLNOde* right; int height; AVLNode(int k):key(k),left(nullptr),right(nullptr),height(1){} }; class AVLTree{ private: AVLNode* root; //获取节点高度 int getHeight(AVLNode* node){ return node ? node->height : 0; } //计算平衡因子 int getBalanceFactor(AVLNode* node){ return node ? getHeight(node->left) - getHeight(node->right) : 0; } //右旋操作 /* Y(失衡节点) X(新根节点) / \ 右旋后 / \ X C ========> A Y / \ / \ A B B C 1. 将X的右子树B挂到Y的左子树位置 2. 将Y挂到X的右子树位置 */ AVLNode* rightRotate(AVLNode* y){ AVLNode* x = y->left; AVLNode* T3 = x->right; x->right = y; y->left = T3; y->height = max(getHeight(y->left),getHeight(y->right)) + 1; x->height = max(getHeight(x->left),getHeight(x->right)) + 1; return x; } //左旋操作 /* X(失衡节点) Y(新根节点) / \ 左旋后 / \ A Y ========> X C / \ / \ B C A B 1.将Y的左子树B挂到X的右子树位置 2.将X挂到Y的左子树位置 */ AVLNode* leftRotate(AVLNode* x){ AVLNode* y = x->right; AVLNode* T2 = y->left; y->left = x; x->right = T2; x->height = max(getHeight(x->left),getHeight(x->right)) + 1; y->height = max(getHeight(y->left),getHeight(y->right)) + 1; return y; } //递归插入节点 AVLNode* insertHelper(AVLNode* node,int key){ if(!node) return new AVLNode(key); if(key < node->key) node->left = insertHelper(node->left,key); else if(key > node->key) node->right = insertHelper(node->right,key); else return node; //不允许重复