扫描线算法的论文《The Intersections for a Set of 2D Segments, and Testing Simple Polygons》的部分翻译

原文地址:http://www.cp.eng.chula.ac.th/~attawith/class/linint


本特利-奥特曼算法的输入是一个线段的集合Ω,输出是它们的交点集合。这个算法可以直观地理解成令一条直线扫过所有Ω中的线段并在经过每条线段时收集其信息,因此又被称为“扫描线算法”。被收集的信息本质上是由现在和扫描线相交的线段组成的一个有序表。用来维护这些信息的数据结构通常也被称为“扫描线”。这个算法会在发现交点的时候检查并输出它们。所以,“扫描线算法”发现这些交点的过程就是算法和其效率的关键。


为了实现扫描线的理论,我们必须先线性排序这些线段来确定扫描线扫过它们的顺序。实际上,我们需要排序的是每条线段的端点,这让我们可以知道扫描线什么时候开始与某条线段相交,以及什么时候停止。一般来讲,这些端点可以以X为第一关键字,以Y为第二关键字排序。在这样的排序下,扫描线将会是竖直的,从左到右扫过每条线段,如图所示。




可以看出,无论何时,与扫描线相交的线段的两个端点一定分别在扫描线左右。因此,维护“扫描线”的方法是:当遇到一个左端点时,加入这条线段,当遇到一个右端点时,删除这条线段。与此同时,“扫描线”还需要维护一个通过上下关系排序的包含与扫描线相交线段的有序表。所以,想要添加或者删除一条线段,它在“扫描线”中的位置必须被确定,而这可以通过二分这个表中的线段来在O(log n)的时间内完成。但是,除了添加和删除线段,还有另一种情况会改变“扫描线”的结构:当经过两条线段的交点时,他们在排序列表中的位置会交换。鉴于需要交换的两条线段在列表中一定是相邻的,所以交换仍是一个O(log n)的操作。


为了管理这些,算法还需要有一个有序的“事件队列”,里面包含了会让“扫描线”变化的事件。一开始,Q包含的是所有线段的两个端点。每当有线段间的交点被找到时,他们就会按照和端点一样的排序规则加入“扫描线”,这时,你必须进行一个测试,来保证不会将点重复插入“扫描线”中--从上面的那幅图中就可以发现是存在这种可能性的。在事件2中,S1和S2两条线段的存在使它们的交点I12被计算并加入“事件队列”。接着,在事件3中,S3加入并将S1和S2分开。接着,在事件4中,因为经过了S1和S3的交点I13,S1和S3在“扫描线”中交换了位置,这时,S1再次回到S2旁边,这就导致了它们的交点I12被第二次计算。但是,每个交点只能有一个事件,所以I12不可以被第二次加入“事件队列”。所以,当一个交点即将被放入“事件队列”时,我们必须先检查它可能存在的位置,确保它不在“事件队列”中。因为两条线段最多只有一个交点,所以可以用线段来唯一标记一个交点。所以,最终“事件队列”的大小是2n+k,这是小于2n+n^2的,因此,一个插入操作可以做到最坏情况下O(log(2n+n^2))也就是O(log n)级别的效率。


但是,这些和有效找到线段交点的全集有什么关系吗?嗯,当所有线段在“扫描线”上被依次更新,它们与其他合法线段的可能存在的交点也被确定了下来。每当一个合法的交点被发现,它就会被插入“事件队列”。然后,当一个交点在“事件队列”中被处理,就会产生一次对“扫描线”的重新排序,同时交点也会被加入输出列表中。最后,当所有的事件都被处理,这个输出列表就会包含线段交点的全集。


但是,还有一个关键的细节需要解决,如何计算一个有效的交点?这个细节是整个算法的核心之一。显然,两条线段只有在同时存在于“扫描线”上的时候才可能相交。但这样处理的话算法的效率是不够高的。观察到一个非常重要的事实,两个相交的线段在扫描线中一定是相邻的。也就是说,只有在有限的几种情况下需要计算可能存在的交点:
1、当一条线段被加入“扫描线”,需要确定它是否与上下两条线段相交;
2、当一条线段从“扫描线”中删除,它上下的两条线段将会相邻,因此需要确定它们是否存在交点;
3、在一个交点事件中,有两条线段会在“扫描线”中交换位置,所以需要确定它们和新相邻的线段是否存在交点。
这就意味着,对于每一个事件的发生,最多只需要再确定两个交点的存在性和位置。还有另一个细节,也就是在“扫描线”中插入、查询、互换、删除线段的时间效率。为了保证这一点,我们使用平衡二叉树(例如AVL、红黑树、2-3树)来实现“扫描线”,这样可以保证所有操作在O(log n)的效率解决。也就是说,共计有2n+k个事件,每个事件在最坏O(log n)的效率下处理,整个算法的总效率是O((n+k)log n)。

猜你喜欢

转载自blog.csdn.net/cccccwb/article/details/51130474