做图形编辑器的人,几乎都会碰到这种尴尬:你画了一个不规则的凹多边形,要填充它的内部,最简单的办法是逐行扫描判断点是否在多边形内,数据量一上来就卡成PPT;做GIS或者芯片版图的人更熟悉这种痛,几千个矩形叠在一起,要算它们的并集面积,两两求交复杂度直接爆炸。这些问题看起来彼此无关,但底层都能统一到一个思路——扫描线算法。
扫描线算法的核心就一句话:用一条假想的直线按顺序“扫”过整个问题空间,在任一时刻只维护“当前线与哪些对象相交”这个状态,把二维问题降成一维问题处理。它没有花哨的数学定理,本质是“排序 + 动态维护 + 增量更新”,但用它来解决多边形填充、矩形面积合并、线段求交、天际线等问题时,能把暴力解法动辄 O(N²) 的复杂度降到 O(N log N)。这篇东西适合所有写过一点算法题、或者工作中需要处理几何计算的人,我会从最朴素的思路开始拆,给出可以直接抄走的代码,再分享一些真正调通这些算法后才知道的坑。
1. 扫描线算法到底在解决什么问题
1.1 核心思想:把二维问题降成一维
先忘掉复杂的公式,想象你要统计一栋楼里哪些房间亮着灯。你不会一层一层把所有房间都看一遍,而是坐在电梯里从下往上走,每到一个楼层只记录“谁进来、谁出去”,最终拼出整个楼的亮灯分布。扫描线算法就是这样:一条水平线从 y 最小值开始,沿着 y 轴方向向上移动,在任何时刻,它只关心“当前水平线和哪些图形的边相交”。这些交点落在一个一维的 x 轴上,于是你可以在这一维区间上做填充、计数、求覆盖长度,再让扫描线往上走一步,重复这个过程。
为什么这个方法快?因为整个二维平面的无穷多个点,被压缩成了有限个“关键事件”。想象一下,扫描线在两条相邻的边之间移动时,交点的数量和顺序其实不会发生任何变化,真正改变状态只发生在扫描线恰好穿过某个顶点或边界的时候。所以算法只需要做两件事:第一,把所有关键位置(顶点、边的上下端点、矩形的上下边)收集起来排序;第二,在两个关键位置之间,只做增量更新,不重头计算。前者负责“离散化”,后者负责“状态维护”,两个动作加起来,整体复杂度就由原来的 O(面积 × 边数) 变成了 O(N log N),N 是几何对象的数量。
我还是拿一个特别典型的场景举例。比如有 N 个矩形,要求它们覆盖的总面积(重叠部分只算一次)。暴力做法是两两求交,对每一对小矩形算交集面积再加加减减,N=5000 时就已经会跑到你怀疑人生。而扫描线的做法是:把每个矩形拆成“下边界”和“上边界”两个事件,扫描线从下往上推进,维护 x 方向被覆盖的区间总长度,当前覆盖长度乘以扫描线走过的垂直距离,就是这一小条的面积,把所有小条面积加起来就是答案。整个过程没有做一次矩形之间的两两求交,靠的是“排序 + 线段树维护覆盖区间”,这就是扫描线算法最迷人的地方。
1.2 三个经典分支:填充、度量、求交
很多人提到扫描线,第一反应是图形学里的多边形填充,但这只是它的一个应用而已。我在实际工程和算法竞赛里,更多是把它分成三个分支来用:
| 分支 | 要解决的问题 | 核心数据结构 | 典型复杂度 |
|---|---|---|---|
| 多边形扫描线填充 | 给任意多边形内部像素上色 | 边表(ET) + 活动边表(AET) | O(Y × N),Y为扫描行数 |
| 扫描线求面积/周长 | 矩形并集面积、区间覆盖长度 | 事件 + 离散化 + 线段树 | O(N log N) |
| 平面扫描求交 | 线段集合的交点检测 | 平衡树 + 优先队列(Bentley-Ottmann) | O((N+K) log N),K为交点个数 |
第一个分支是“逐行”刷,服务器端图形学和软件渲染用得多;第二个分支是我个人认为性价比最高的,学好了一套线段树模板,矩形面积并、周长并、天际线全都能解;第三个分支是计算几何里平面扫描的经典代表,用来找 N 条线段的所有交点,K 是交点数量,在很多碰撞检测、地图叠加分析场景里很实用。你会发现它们都有一个共同的内核:排序决定顺序,一个动态结构维护当前状态,每次只处理“变化点”。
我这篇文章会把前两个分支的完整细节都写到位,第三个分支给一个思路性讲解,因为 Bentley-Ottmann 的代码细节比较多,全铺开会偏题,但思想和前两个一脉相承。下面我们从最经典的多边形填充开始。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 多边形填充:扫描线最经典的图形学应用
2.1 边表(ET)与活动边表(AET)是怎么设计的
多边形填充的目标很简单:给定一个由 N 条线段围成的多边形,把多边形内部的像素点全部标记出来。最朴素的做法是“逐点判断”,对每个像素发一条射线,数它与多边形的交点数,奇数就在内部,这就是射线法,复杂度是 O(像素数 × 边数)。扫描线填充的聪明之处在于,它不逐点判断,而是逐行计算“这个 y 高度上,多边形覆盖了哪些 x 区间”。
假设扫描线当前在 y 位置,它穿过了多边形的若干条边,得到若干个交点。由于多边形是封闭的,这些交点按 x 排序后,必然是“入点、出点、入点、出点……”交替出现,所以第一个交点和第二个交点之间的部分在多边形内部,第三个和第四个之间也在内部。问题就变成了两件事:第一,维护“当前 y 下有哪些边可能被穿过”;第二,高效计算这些边与扫描线的交点 x,并且保证排序正确。这就引出了两个结构。
- 边表(Edge Table, ET):以每条边的下端点 ymin 为键,把边挂到对应高度上。它回答的问题是“扫描线走到 y 时,有哪些边新加入了游戏”。
