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 个矩形的并集覆盖的总面积。
2
100 100 200 200
150 150 250 255
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。
#include
#include
#include
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
方向被覆盖的总长度。 该方法适用于 N ≤ 10^5
的情况,效率极高!
在线段树的构建过程中,我们通常将一个区间 [l, r]
拆分,直到无法继续拆分为止。
在很多常见的线段树应用(如单点更新、区间查询)中,叶子节点往往是 l == r
,但是在扫描线+线段树这种应用场景下,我们的离散化 y
坐标并不连续,因此叶子节点的定义有所不同,l + 1 == r
才是叶子节点,而不是 l == r
。
l + 1 == r
才是叶子节点?离散化的线段树是基于区间
而不是单个点。
常规线段树(连续区间):
l == r
作为叶子节点。[y_coords[l], y_coords[r]]
,而不是单个点。离散化线段树(区间合并):
l == r
只表示单个坐标,不是一个“区间”。l + 1 == r
,意味着 y_coords[l]
到 y_coords[r]
之间已经没有更多的离散坐标,可以认为它们形成了最小单位的“区间”,无法再继续细分,因此它是叶子节点。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]
构建线段树时,我们递归拆分区间 [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
)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
不行了!