6.线段求交(学习笔记)

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;	//不允许重复
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值