摘要
文章通过快递分拣的比喻,详细解释了多边形扫描线填充算法中边表(Edge Table)和活性边表(Active Edge Table, AET)的数据结构及其工作流程。边表类似于快递分拣柜,按楼层(y坐标)存放所有边,每条边包含起点x、最高点ymax和斜率倒数dx。AET则类似于快递员的派送清单,记录当前层需要处理的边。算法逐层扫描,更新AET中的边,并根据x值成对填充多边形区域。整个过程通过分层处理和简单计算,实现了高效的多边形填充。文章还通过伪代码和具体示例,进一步说明了边表和AET的配合方式,形象地展示了算法的执行过程。
1. 生活比喻:快递分拣柜
想象你是快递站的分拣员,每天有很多快递包裹(多边形的边),
你要把它们按楼层(y坐标)分好,放进不同的分拣柜(边表的每一层)。
每个包裹上都贴着详细的标签(边的详细信息),
这样快递员每到一层楼(扫描线),
只需要打开对应的分拣柜,
就能拿到本层要派送的所有包裹(边)。
2. 边表的每一项都包含什么?
每个分拣柜(y=k的边表项)里,
都放着从这一层开始的所有包裹(边),
每个包裹的标签上写着:
- x:包裹在这一层的起始位置(边的下端点x坐标)
- ymax:包裹能送到的最高楼层(边的上端点y坐标)
- dx:每上一层,包裹要往哪边挪(边的斜率倒数,Δx/Δy)
- (有时还会记录ymin,但通常用分拣柜的编号就知道了)
形象点说:
- x:快递员在本层的起点
- ymax:快递员能送到的最高楼层
- dx:快递员每上一层要往左还是往右走多少
3. 数据结构长什么样?
代码版(伪代码)
# 边表是一个“楼层号”到“包裹列表”的映射
EdgeTable = {
y0: [Edge1, Edge2, ...], # y0楼层的所有包裹
y1: [Edge3, ...],
...
}
# 每个包裹(边)的标签
class Edge:
x # 当前x坐标(下端点)
ymax # 最高楼层(上端点y)
dx # 每上一层x要加多少(斜率倒数)
表格版
楼层(y) | x(起点) | ymax(终点) | dx(斜率倒数) |
---|---|---|---|
2 | 3 | 7 | 0.5 |
2 | 5 | 6 | -0.25 |
3 | 4 | 8 | 1.0 |
… | … | … | … |
4. 这些数据怎么用?
- x:告诉你本层从哪里开始处理这条边
- ymax:告诉你这条边什么时候“送完”不用再管
- dx:每到下一层,x要加上dx,表示边的延伸方向
举个例子
假设有一条边从(2, 3)到(6, 7):
- 起点y=3,x=2
- 终点y=7,ymax=7
- dx = (6-2)/(7-3) = 1.0
这条边会被放进y=3的分拣柜,
快递员每上一层,x就加1,
一直送到y=7为止。
5. 为什么要这样设计?
- 分层存放:每层只处理本层的边,查找快,效率高
- x和dx:方便计算每层的交点,填充时只需简单加法
- ymax:自动判断边何时“过期”,不用多余判断
6. 总结
边表的数据细节就像快递分拣柜里的包裹标签,
每个标签都写明了起点、终点和每层的移动方向,
让快递员(算法)每到一层都能高效、准确地派送(处理边),
整个过程井井有条,省时省力!
我们继续深入,用生活化的比喻和具体的数据细节,让你彻底明白边表的数据结构,以及它和“活性边表”(AET)如何配合工作的全过程!
1. 边表和活性边表的配合——“快递分拣与派送”
边表(Edge Table)——“分拣柜”
- 你提前把所有快递(边)按楼层(y坐标)分好,放进不同的分拣柜(边表的每一层)。
- 每个快递包裹(边)上都贴着标签(x、ymax、dx)。
活性边表(Active Edge Table, AET)——“派送清单”
- 快递员每到一层楼(扫描线y),就把本层分拣柜里的新快递加入到自己的派送清单(AET)。
- 派送清单里只保留还没送完的快递(y < ymax)。
2. 具体流程(以多边形扫描线填充为例)
步骤1:初始化边表
- 先把所有边按起点y分组,放进边表的各个“分拣柜”。
- 每个分拣柜(y=k)里,存放所有下端点y=k的边。
步骤2:逐层处理
- 从最低的y开始(比如y=2),每到一层:
- 把本层分拣柜里的边加入AET(派送清单)。
- 从AET中删除ymax==当前y的边(送完的快递)。
- 按x从小到大排序AET(方便后续处理)。
- 成对处理AET中的边,每两条边之间的区间就是要填充的区域(比如要扫雪的路段)。
- 更新AET中每条边的x:x = x + dx(快递员每上一层,位置要变)。
- y加1,进入下一层,重复上述步骤。
3. 具体数据细节举例
假设有如下多边形边:
边编号 | 下端点(x, y) | 上端点(x, y) | dx(斜率倒数) |
---|---|---|---|
1 | (2, 2) | (6, 5) | 1.33 |
2 | (6, 5) | (4, 8) | -0.67 |
3 | (4, 8) | (2, 2) | -0.33 |
边表结构:
EdgeTable = {
2: [Edge(x=2, ymax=5, dx=1.33), Edge(x=2, ymax=8, dx=-0.33)],
5: [Edge(x=6, ymax=8, dx=-0.67)]
}
AET在每一层的变化:
- y=2:AET加入Edge1和Edge3
- y=3:AET更新x,继续处理
- y=5:AET加入Edge2,Edge1到ymax=5被移除
- y=6~7:AET只剩Edge2和Edge3
- y=8:Edge2和Edge3到ymax=8被移除,AET清空
4. 形象小结
- 边表就像是快递分拣柜,提前把所有任务分好类,等着快递员来取。
- AET就像是快递员手里的派送清单,每到一层楼就更新一次,只处理当前需要派送的快递。
- 每个快递(边)都有详细的标签(x、ymax、dx),快递员只需按标签操作,省时省力。
5. 代码片段(伪代码)
# 初始化边表
for edge in polygon_edges:
y_min = min(edge.y1, edge.y2)
y_max = max(edge.y1, edge.y2)
x = edge.x1 if edge.y1 == y_min else edge.x2
dx = (edge.x2 - edge.x1) / (edge.y2 - edge.y1)
EdgeTable[y_min].append(Edge(x, y_max, dx))
# 扫描线填充
AET = []
for y in range(y_min, y_max):
# 1. 加入新边
AET.extend(EdgeTable.get(y, []))
# 2. 移除到达ymax的边
AET = [e for e in AET if e.ymax > y]
# 3. 按x排序
AET.sort(key=lambda e: e.x)
# 4. 成对填充
for i in range(0, len(AET), 2):
fill_between(AET[i].x, AET[i+1].x, y)
# 5. 更新x
for e in AET:
e.x += e.dx
6. 生活化总结
边表的数据细节和配合流程,
就像快递分拣和派送一样,
每个包裹(边)有详细标签,
每一层(y)只处理本层的任务,
快递员(算法)高效、准确地完成派送(多边形填充)!