https://www.luogu.com.cn/problem/P5490
题目描述
求 n n n 个四边平行于坐标轴的矩形的面积并。
输入格式
第一行一个正整数 n n n。
接下来 n n n 行每行四个非负整数 x 1 , y 1 , x 2 , y 2 x_1, y_1, x_2, y_2 x1,y1,x2,y2,表示一个矩形的四个端点坐标为 ( x 1 , y 1 ) , ( x 1 , y 2 ) , ( x 2 , y 2 ) , ( x 2 , y 1 ) (x_1, y_1),(x_1, y_2),(x_2, y_2),(x_2, y_1) (x1,y1),(x1,y2),(x2,y2),(x2,y1)。
输出格式
一行一个正整数,表示 n n n 个矩形的并集覆盖的总面积。
输入输出样例 #1
输入 #1
2
100 100 200 200
150 150 250 255
输出 #1
18000
说明/提示
对于 20 % 20\% 20% 的数据, 1 ≤ n ≤ 1000 1 \le n \le 1000 1≤n≤1000。
对于 100 % 100\% 100% 的数据, 1 ≤ n ≤ 10 5 1 \le n \le {10}^5 1≤n≤105, 0 ≤ x 1 < x 2 ≤ 10 9 0 \le x_1 < x_2 \le {10}^9 0≤x1<x2≤109, 0 ≤ y 1 < y 2 ≤ 10 9 0 \le y_1 < y_2 \le {10}^9 0≤y1<y2≤109。
🚀 C++ 代码实现
#include <iostream>
#include <vector>
#include <algorithm>using namespace std;struct Event {int x, y1, y2, type; // x 坐标, y 起点, y 终点, type=1(加入), type=-1(删除)Event(int x, int y1, int y2, int type) : x(x), y1(y1), y2(y2), type(type) {}
};struct SegmentTree {// y 轴区间被覆盖次数机覆盖长度vector<int> cnt, len;// 存储离散化后的 y 坐标值,用于映射 y 轴上的真实值到线段树的索引vector<int> y_coords;SegmentTree(int size) {cnt.resize(size * 4);len.resize(size * 4);}void build(int l, int r, int idx) {cnt[idx] = len[idx] = 0;if (l + 1 == r) return;int mid = (l + r) / 2;build(l, mid, idx * 2);build(mid, r, idx * 2 + 1);}void update(int jobl, int jobr, int val, int l, int r, int idx) {if (jobl >= r || jobr <= l) return;if (jobl <= l && r <= jobr) {cnt[idx] += val;} else {int mid = (l + r) / 2;update(jobl, jobr, val, l, mid, idx * 2);update(jobl, jobr, val, mid, r, idx * 2 + 1);}// 计算当前区间的 y 方向被覆盖的长度if (cnt[idx] > 0) {len[idx] = y_coords[r] - y_coords[l]; // 计算该段区间长度} else {len[idx] = (l + 1 == r) ? 0 : (len[idx * 2] + len[idx * 2 + 1]); // 合并子区间}}
};int main() {int n;cin >> n;vector<Event> events;vector<int> y_coords;// 读取矩形数据for (int i = 0; i < n; i++) {int x1, y1, x2, y2;cin >> x1 >> y1 >> x2 >> y2;events.emplace_back(x1, y1, y2, 1); // 左边界events.emplace_back(x2, y1, y2, -1); // 右边界y_coords.push_back(y1);y_coords.push_back(y2);}// 对 y 坐标进行离散化sort(y_coords.begin(), y_coords.end());y_coords.erase(unique(y_coords.begin(), y_coords.end()), y_coords.end());// 给事件按照 x 轴排序sort(events.begin(), events.end(), [](const Event &a, const Event &b) {return a.x < b.x;});// 初始化线段树SegmentTree segTree(y_coords.size());segTree.y_coords = y_coords;segTree.build(0, y_coords.size() - 1, 1);long long area = 0;for (size_t i = 0; i < events.size() - 1; i++) {int x = events[i].x;int y1 = lower_bound(y_coords.begin(), y_coords.end(), events[i].y1) - y_coords.begin();int y2 = lower_bound(y_coords.begin(), y_coords.end(), events[i].y2) - y_coords.begin();segTree.update(y1, y2, events[i].type, 0, y_coords.size() - 1, 1);// 计算面积增量int dx = events[i + 1].x - x;area += 1LL * dx * segTree.len[1]; // 累加当前 x 范围的面积}cout << area << endl;return 0;
}
🌟 解法思路:扫描线 + 线段树
核心思想:
-
转换为扫描线问题:
- 把每个矩形拆成两条竖直边(左边界 + 右边界)。
- 每条边记录
x
坐标、y
区间以及是左边界(加入)还是右边界(删除)。
-
按
x
坐标排序:- 依次处理
x
变化的位置,维护y
方向的覆盖情况。
- 依次处理
-
使用线段树维护
y
方向的覆盖长度:- 统计当前
x
坐标下y
方向被覆盖的总长度。 - 在
x
变化时,用dx * covered_length
计算新增的面积。
- 统计当前
📌 代码解析
-
读取输入并创建事件
- 每个矩形
(x1, y1, x2, y2)
被拆成两个事件:- 左边界:
(x1, y1, y2, +1)
- 右边界:
(x2, y1, y2, -1)
- 左边界:
- 每个矩形
-
离散化
y
轴y
轴可能范围很大,使用排序+去重将y
压缩成[0, m-1]
范围。
-
排序事件
- 按照
x
轴排序,保证扫描顺序是从左到右。
- 按照
-
扫描线遍历
- 遍历
x
方向的边界事件,更新y
方向的覆盖长度。 - 计算当前
x
范围的面积增量dx * covered_length
。
- 遍历
-
线段树维护
y
方向覆盖情况update(y1, y2, type)
更新cnt
计数。len[1]
存储y
方向被覆盖的总长度。
📊 复杂度分析
- 事件排序: O ( N log N ) O(N \log N) O(NlogN)
- 线段树更新: O ( N log N ) O(N \log N) O(NlogN)
- 总时间复杂度: O ( N log N ) O(N \log N) O(NlogN)
✅ 适用场景
- 计算多个矩形的面积并
- 计算建筑投影面积
- 计算不规则区间合并
📌 该方法适用于 N ≤ 10^5
的情况,效率极高!🚀
在线段树的构建过程中,我们通常将一个区间 [l, r]
拆分,直到无法继续拆分为止。
在很多常见的线段树应用(如单点更新、区间查询)中,叶子节点往往是 l == r
,但是在扫描线+线段树这种应用场景下,我们的离散化 y
坐标并不连续,因此叶子节点的定义有所不同,l + 1 == r
才是叶子节点,而不是 l == r
。
🌟 为什么 l + 1 == r
才是叶子节点?
离散化的线段树是基于区间
而不是单个点。
-
常规线段树(连续区间):
- 例如求区间最小值、区间和,通常是
l == r
作为叶子节点。 - 但在这里,我们是计算“区间长度”,所以叶子节点应该是最小单位的区间
[y_coords[l], y_coords[r]]
,而不是单个点。
- 例如求区间最小值、区间和,通常是
-
离散化线段树(区间合并):
- 叶子节点应该是最小的 y 轴区间,而
l == r
只表示单个坐标,不是一个“区间”。 - 当
l + 1 == r
,意味着y_coords[l]
到y_coords[r]
之间已经没有更多的离散坐标,可以认为它们形成了最小单位的“区间”,无法再继续细分,因此它是叶子节点。
- 叶子节点应该是最小的 y 轴区间,而
🔹 详细举例
1️⃣ 假设 y_coords = {2, 5, 10, 15}
离散化后的 y_coords
索引:
y 值 | 离散索引 |
---|---|
2 | 0 |
5 | 1 |
10 | 2 |
15 | 3 |
表示的区间:
区间 [0,1] 表示 y = [2, 5]
区间 [1,2] 表示 y = [5, 10]
区间 [2,3] 表示 y = [10, 15]
2️⃣ 线段树的构造
构建线段树时,我们递归拆分区间 [0,3]
:
[0,3]/ \[0,2] [2,3]/ \[0,1] [1,2]
在这棵树中:
[0,1]
表示[2,5]
,是叶子节点(因为0+1 == 1
)[1,2]
表示[5,10]
,是叶子节点(因为1+1 == 2
)[2,3]
表示[10,15]
,是叶子节点(因为2+1 == 3
)
3️⃣ 为什么 l + 1 == r
作为叶子节点,而 l == r
不行?
如果 l == r
作为叶子节点,那就会出现 l = 0, r = 0
这种情况,这样的区间没有物理意义,因为:
y_coords[0] = 2
只是一个点,并不能表示一个范围。- 但
y_coords[0]
到y_coords[1]
([2,5]
) 形成了一个“区间”,才有意义。
所以,叶子节点的最小单位应该是 [y_coords[l], y_coords[r]]
,而不是单个点。
✅ 结论
方式 | 叶子节点定义 | 适用情况 | 是否适用于扫描线+线段树 |
---|---|---|---|
l == r | 单个点 | 适用于连续值(如 RMQ、区间和) | ❌ 不适用于离散区间 |
l + 1 == r | 一个离散区间 | 适用于离散化的线段树(扫描线) | ✅ 适用 |
📌 代码示例
if (l + 1 == r) { // 叶子节点len[idx] = 0; // 初始状态下没有被覆盖
} else { // 非叶子节点,合并左右子树len[idx] = len[idx * 2] + len[idx * 2 + 1];
}
🎯 总结
- 线段树的叶子节点是最小单位的“区间”而不是单点,所以
l + 1 == r
作为叶子节点。 - 常规线段树
l == r
适用于单点查询,但扫描线 + 线段树需要处理区间,所以l + 1 == r
作为叶子节点。 - 这样,我们才能用
y_coords[r] - y_coords[l]
计算被覆盖的区间长度,否则l == r
没有意义。
🚀 这样就能理解为什么 l + 1 == r
是叶子节点,而 l == r
不行了!
扫描线相关练习题目
- 218. 天际线问题
- P1904 天际线