1. 项目概述
P1884 [USACO12FEB] Overplanting S是美国计算机奥林匹克竞赛(USACO)2012年2月月赛银组的一道经典题目。这道题考察的是计算几何中的矩形面积并问题,属于平面扫描算法的典型应用场景。题目要求计算多个矩形在平面上覆盖的总面积,需要考虑重叠区域的去重。
在实际应用中,这类算法可以用于农业规划中的种植面积计算、城市规划中的土地利用分析,以及图形学中的区域填充计算等场景。这道题作为USACO银组题目,适合已经掌握基础数据结构和算法思想的选手进行练习。
2. 问题分析与建模
2.1 问题描述
给定N个矩形的坐标(每个矩形用左下角和右上角坐标表示),计算这些矩形覆盖的总面积。矩形可能重叠,重叠区域只计算一次。
输入格式:
- 第一行:整数N(1≤N≤1000)
- 接下来N行:每行四个整数x1 y1 x2 y2,表示一个矩形的左下角(x1,y1)和右上角(x2,y2)坐标
- 坐标范围:-10^8 ≤ x1,y1,x2,y2 ≤ 10^8
输出格式:
- 一个整数,表示覆盖的总面积
2.2 问题转化
这个问题可以转化为计算平面上被至少一个矩形覆盖的点的总数。直接枚举所有点显然不可行,因为坐标范围很大。我们需要找到一种高效的计算方法。
关键观察点:
- 矩形的边都是平行于坐标轴的
- 面积计算可以分解为x方向和y方向的投影处理
- 重叠区域需要特殊处理以避免重复计算
3. 算法设计与选择
3.1 暴力法分析
最直观的方法是使用二维差分数组标记被覆盖的区域,最后统计被标记的面积。但是这种方法在坐标范围很大时(本题可达2×10^8)会消耗过多内存和时间,不可行。
3.2 平面扫描算法
更高效的解决方案是使用平面扫描(Plane Sweep)算法,结合离散化和线段树。这是计算几何中处理矩形覆盖问题的标准方法。
算法步骤:
- 离散化:将所有x坐标和y坐标分别排序去重
- 扫描线:沿x轴方向扫描,处理矩形的左右边界事件
- 线段树维护:在y轴上使用线段树维护当前被覆盖的区间长度
3.3 离散化处理
由于坐标范围很大但矩形数量较少,我们需要先对坐标进行离散化:
cpp复制vector<int> xs, ys; // 存储所有x和y坐标
// 读取所有矩形坐标并添加到xs和ys中
sort(xs.begin(), xs.end());
xs.erase(unique(xs.begin(), xs.end()), xs.end());
// 对ys做同样处理
离散化后,我们将原始的大坐标范围映射到小的索引范围,大大减少了需要处理的数据量。
4. 详细实现步骤
4.1 数据结构设计
我们需要定义几个关键数据结构:
- 事件结构体:记录矩形的左右边界
cpp复制struct Event {
int x, y1, y2;
int type; // 1表示矩形开始,-1表示矩形结束
bool operator<(const Event& e) const {
return x < e.x;
}
};
- 线段树节点:用于维护y轴上的覆盖情况
cpp复制struct Node {
int l, r;
int cover; // 当前区间被完全覆盖的次数
int len; // 当前区间被覆盖的长度
} tr[N*8];
4.2 线段树实现
线段树需要支持区间更新和覆盖长度查询:
cpp复制void build(int u, int l, int r) {
tr[u] = {l, r, 0, 0};
if (l == r) return;
int mid = (l + r) >> 1;
build(u<<1, l, mid);
build(u<<1|1, mid+1, r);
}
void pushup(int u) {
if (tr[u].cover > 0) {
tr[u].len = ys[tr[u].r+1] - ys[tr[u].l];
} else if (tr[u].l != tr[u].r) {
tr[u].len = tr[u<<1].len + tr[u<<1|1].len;
} else {
tr[u].len = 0;
}
}
void modify(int u, int l, int r, int k) {
if (tr[u].l >= l && tr[u].r <= r) {
tr[u].cover += k;
pushup(u);
return;
}
int mid = (tr[u].l + tr[u].r) >> 1;
if (l <= mid) modify(u<<1, l, r, k);
if (r > mid) modify(u<<1|1, l, r, k);
pushup(u);
}
4.3 主算法流程
cpp复制int main() {
// 1. 读取输入并离散化坐标
// 2. 构建事件列表
vector<Event> events;
for (auto& rect : rectangles) {
int x1 = rect.x1, y1 = rect.y1, x2 = rect.x2, y2 = rect.y2;
int y1_idx = lower_bound(ys.begin(), ys.end(), y1) - ys.begin();
int y2_idx = lower_bound(ys.begin(), ys.end(), y2) - ys.begin() - 1;
events.push_back({x1, y1_idx, y2_idx, 1});
events.push_back({x2, y1_idx, y2_idx, -1});
}
sort(events.begin(), events.end());
// 3. 构建线段树
build(1, 0, ys.size()-2);
// 4. 扫描处理事件
long long res = 0;
for (int i = 0; i < events.size(); i++) {
if (i > 0) {
res += (long long)tr[1].len * (events[i].x - events[i-1].x);
}
modify(1, events[i].y1, events[i].y2, events[i].type);
}
cout << res << endl;
return 0;
}
5. 算法复杂度分析
- 离散化:O(N log N)
- 事件排序:O(N log N)
- 线段树构建:O(N)
- 扫描处理:每个事件处理时间为O(log N),共O(N log N)
- 总时间复杂度:O(N log N)
- 空间复杂度:O(N)
6. 优化与注意事项
6.1 实现细节
- 离散化时要注意去重,避免重复坐标影响线段树操作
- 线段树的叶子节点实际上代表的是相邻两个离散化坐标之间的区间
- 在事件处理时,y2_idx需要减1,因为线段树维护的是区间而非点
6.2 常见错误
- 整数溢出:面积可能很大,需要使用long long类型存储结果
- 坐标映射错误:离散化后的索引处理不当会导致线段树操作错误
- 事件排序不完整:必须确保所有事件按x坐标严格排序
6.3 替代方案
对于N较小的情况(如N≤100),可以使用容斥原理计算面积并,但时间复杂度为O(2^N),不适用于本题。
7. 扩展与应用
7.1 三维情况
这个问题可以扩展到三维空间,计算长方体的体积并。此时需要使用平面扫描结合二维线段树,时间复杂度为O(N^2 log N)。
7.2 实际应用场景
- 农业规划:计算多块农田的总种植面积
- 城市规划:计算多个建筑地块的总占地面积
- 图形学:计算多个图形的覆盖区域
- VLSI设计:计算芯片上多个元件的覆盖区域
8. 总结与个人体会
这道题是计算几何中的经典问题,考察了离散化、扫描线和线段树等多个重要算法思想的综合应用。在实际编码中,我发现以下几点特别重要:
- 离散化处理要小心坐标映射关系,建议在纸上画出示例验证
- 线段树的cover标记和len维护需要仔细处理各种情况
- 事件处理时要注意边界条件,特别是第一个和最后一个事件
通过这道题,我对扫描线算法有了更深的理解。它不仅适用于矩形面积问题,还可以解决许多其他几何问题,如求交点、计算轮廓等。掌握这种算法思想对解决复杂的计算几何问题非常有帮助。
