把矩形拆成竖边按 x 扫描,用线段树维护 y 轴上被覆盖的总长度(cover_cnt 不下传、cover_len 自底向上拼接),每条竖边 O(log n) 累加面积,总复杂度 O(n log n)。
OJ: roj
题目 ID: 3114
难度:提高+/省选-
标签:python扫描线线段树离散化计算几何
创建: 2026-10-01 17:52
更新: 2026-10-10 11:30
形式化题目
给定平面上的
即被至少一个矩形覆盖的区域的面积,重复覆盖只计算一次。坐标可以不是整数,需要按浮点处理。
坐标系方向(题面
数据范围:
样例:两个矩形 180.00。
暴力解法
思路
面积并的原始定义是「被覆盖点集的测度」,最直接的切入点是把平面切碎成小块, 逐块判断是否被覆盖。切分的关键依据是:
任何矩形的
边界只会出现在 或 处, 所以把全部 坐标排序去重得到 后, 每个竖条 内部不包含任何矩形的竖直边。
由此得到一个竖条内的覆盖情况是恒定的:矩形
括号内是一个一维问题:把
样例的两个矩形产生
25.5 | +-------------------------+ <- R2 上边
| | R2 |
| | |
20 | +------------+------------+ | <- R1 上边 (与 R2 相交)
| | R1 | | |
| | | | |
15 | | +------------+------------+ <- R2 下边 (与 R1 相交)
| | |
| | |
10 | +-------------------------+ <- R1 下边
+----+------------+------------+------------+
10 15 20 25
| | | |
└─ 竖条1 ─────┴─ 竖条2 ────┴─ 竖条3 ────┘| 竖条 | 完整覆盖它的矩形 | 覆盖长度 | 贡献面积 | ||
|---|---|---|---|---|---|
| 1 | 10 | ||||
| 2 | 15.5 | ||||
| 3 | 10.5 |
三个竖条贡献相加
代码
复杂度
- 时间
:竖条数 ;每个竖条要 筛选矩形, 再对至多 个区间排序合并 。 时约 量级,Python 必然超时。 - 空间
。
瓶颈
瓶颈不在「离散化」这一步——竖条的划分是必要的,问题出在每个竖条都要从头重算:
- 筛选
需要扫一遍全部 个矩形; - 求出
后还要重新排序、重新合并一遍 区间。
但相邻竖条的
正解
关键观察
把「关注竖条」换成「关注竖边」,瓶颈立刻消失:
矩形
的两条竖直边 、 携带增量 、 。 按 升序依次处理(同 的边连续处理),则处理完 处的全部竖边后, 轴上被覆盖的总长度,就是区间 上的覆盖长度。
展开就是:进入竖条
剩下的问题变成:维护一个
思路
第一步,离散化
第二步,确定每个结点存什么。 每个结点维护两个值:
cover_cnt:完整覆盖该结点段区间的竖边条数。关键性质是它不下传;cover_len:该结点段区间内被覆盖的总长度。
回填(pushup)规则:
为什么 cover_cnt 不下传也正确?因为一个结点只有两种状态:
被整段覆盖时长度由自己决定(哪怕子结点什么都没记),
否则长度必然等于左右儿子之和。被部分覆盖的信息已经记在更深层的结点上,
父结点不需要重复计数,也不会漏算。
第三步,扫描竖边。 把
area = 0; prev_x = edges[0].x
for (x, l, r, d) in edges:
area += cover_len[根] * (x - prev_x) # 结算 [prev_x, x),此时树里只有 x' <= prev_x 的边
cover(1, 0, m-1, l, r, d) # 再应用本条边
prev_x = x样例的 4 条竖边(
| 顺序 | 类型 | 覆盖段 | 结算前 cover_len |
本段贡献 | 更新后 cover_len |
||
|---|---|---|---|---|---|---|---|
| 1 | 10 | 0 | 0 | 0 | 10 | ||
| 2 | 15 | 5 | 10 | 50 | 15.5 | ||
| 3 | 20 | 5 | 15.5 | 77.5 | 10.5 | ||
| 4 | 25 | 5 | 10.5 | 52.5 | 0 |
四段贡献 cover_len」正是第
代码
/**
* Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
* rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
* rainboy的学习导航网站: https://idx.roj.ac.cn
* create_at: 2026-10-10 11:30
* update_at: 2026-10-10 11:30
*/
// 亚特兰蒂斯:矩形面积并,按 x 扫描竖边,线段树维护被覆盖的 y 总长度。
#include <cstdio>
#include <vector>
#include <algorithm>
typedef long long ll;
struct Rect {
double x1, y1, x2, y2;
};
struct Edge {
double x;
ll ql, qr; // 基本段区间 [ql, qr]
ll delta; // 左边界 +1、右边界 -1
};
std::vector<double> ys; // 离散化后的 y 坐标
std::vector<ll> cover_cnt; // 结点被整段覆盖的次数
std::vector<double> cover_len; // 结点内被覆盖的总长度
bool edge_less(const Edge& a, const Edge& b) {
if (a.x != b.x) return a.x < b.x;
if (a.ql != b.ql) return a.ql < b.ql;
if (a.qr != b.qr) return a.qr < b.qr;
return a.delta < b.delta;
}
// 把基本段区间 [ql, qr] 的覆盖次数加 delta,再回填本结点的覆盖长度
void cover(ll node, ll nl, ll nr, ll ql, ll qr, ll delta) {
if (ql <= nl && nr <= qr) { // 整段落在同一条竖边的跨度里
cover_cnt[node] += delta;
} else {
ll mid = (nl + nr) >> 1;
if (ql <= mid) cover(node << 1, nl, mid, ql, qr, delta);
if (qr > mid) cover(node << 1 | 1, mid + 1, nr, ql, qr, delta);
}
if (cover_cnt[node]) { // 被完整覆盖:整段都算
cover_len[node] = ys[nr + 1] - ys[nl];
} else if (nl == nr) {
cover_len[node] = 0.0;
} else {
cover_len[node] = cover_len[node << 1] + cover_len[node << 1 | 1];
}
}
double union_area(std::vector<Rect>& rects) {
ys.clear();
for (size_t i = 0; i < rects.size(); i++) {
ys.push_back(rects[i].y1);
ys.push_back(rects[i].y2);
}
std::sort(ys.begin(), ys.end());
ys.erase(std::unique(ys.begin(), ys.end()), ys.end());
ll seg_cnt = (ll)ys.size() - 1; // 基本段个数:第 i 段是 [ys[i], ys[i+1]]
cover_cnt.assign(4 * seg_cnt + 4, 0);
cover_len.assign(4 * seg_cnt + 4, 0.0);
std::vector<Edge> edges;
for (size_t i = 0; i < rects.size(); i++) {
ll r1 = std::lower_bound(ys.begin(), ys.end(), rects[i].y1) - ys.begin();
ll r2 = std::lower_bound(ys.begin(), ys.end(), rects[i].y2) - ys.begin();
Edge e;
e.x = rects[i].x1; e.ql = r1; e.qr = r2 - 1; e.delta = 1; edges.push_back(e);
e.x = rects[i].x2; e.ql = r1; e.qr = r2 - 1; e.delta = -1; edges.push_back(e);
}
std::sort(edges.begin(), edges.end(), edge_less);
double area = 0.0;
double prev_x = edges[0].x;
for (size_t i = 0; i < edges.size(); i++) {
area += cover_len[1] * (edges[i].x - prev_x); // 区间 [prev_x, x) 用移动前的覆盖长度
cover(1, 0, seg_cnt - 1, edges[i].ql, edges[i].qr, edges[i].delta);
prev_x = edges[i].x;
}
return area;
}
int main() {
ll n;
ll case_id = 0;
while (scanf("%lld", &n) == 1 && n != 0) {
case_id++;
std::vector<Rect> rects(n);
for (ll i = 0; i < n; i++) {
scanf("%lf %lf %lf %lf", &rects[i].x1, &rects[i].y1, &rects[i].x2, &rects[i].y2);
}
double area = union_area(rects);
printf("Test case #%lld\nTotal explored area: %.2f\n\n", case_id, area);
}
return 0;
}#!/usr/bin/env python3
# Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
# rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
# rainboy的学习导航网站: https://idx.roj.ac.cn
# create_at: 2026-10-01 17:54
# update_at: 2026-10-01 17:54
import sys
def union_area(rects: list[tuple[float, float, float, float]]) -> float:
"""所有矩形的面积并:按 x 扫竖边,线段树维护当前竖线上被覆盖的 y 总长度。"""
ys = sorted({y for _x1, y1, _x2, y2 in rects for y in (y1, y2)}) # 离散化后的 y 坐标
y_rank = {y: i for i, y in enumerate(ys)} # y 坐标 → 下标
seg_cnt = len(ys) - 1 # 基本段个数:第 i 段是 [ys[i], ys[i+1]]
cover_cnt = [0] * (4 * seg_cnt) # 结点被整段覆盖的次数
cover_len = [0.0] * (4 * seg_cnt) # 结点内被覆盖的总长度
def cover(node: int, nl: int, nr: int, ql: int, qr: int, delta: int) -> None:
"""把基本段区间 [ql, qr] 的覆盖次数加 delta,再回填本结点的覆盖长度。"""
if ql <= nl and nr <= qr: # 整段落在同一条竖边的跨度里
cover_cnt[node] += delta
else:
mid = (nl + nr) >> 1
if ql <= mid:
cover(node << 1, nl, mid, ql, qr, delta)
if qr > mid:
cover(node << 1 | 1, mid + 1, nr, ql, qr, delta)
if cover_cnt[node]: # 被完整覆盖:整段都算
cover_len[node] = ys[nr + 1] - ys[nl]
elif nl == nr: # 叶子没被覆盖,长度为 0
cover_len[node] = 0.0
else: # 否则由两个子结点拼接
cover_len[node] = cover_len[node << 1] + cover_len[node << 1 | 1]
edges = sorted( # 竖边:左边界 +1、右边界 -1
(x, y_rank[y1], y_rank[y2] - 1, delta)
for x1, y1, x2, y2 in rects
for x, delta in ((x1, 1), (x2, -1))
)
area = 0.0
prev_x = edges[0][0]
for x, ql, qr, delta in edges:
area += cover_len[1] * (x - prev_x) # 区间 [prev_x, x) 用移动前的覆盖长度
cover(1, 0, seg_cnt - 1, ql, qr, delta)
prev_x = x
return area
def solve() -> None:
data = iter(sys.stdin.buffer.read().split())
out: list[str] = []
case = 0
while (n := int(next(data))) != 0:
case += 1
rects = [
(float(next(data)), float(next(data)), float(next(data)), float(next(data)))
for _ in range(n)
]
out += [f"Test case #{case}", "Total explored area: %.2f" % union_area(rects), ""]
sys.stdout.write('\n'.join(out) + '\n')
if __name__ == "__main__":
solve()实现说明:
union_area的前三行是离散化:ys排序去重、y_rank做坐标到下标的映射、seg_cnt = len(ys) - 1是基本段数。cover是内嵌的线段树区间加:ql <= nl and nr <= qr时才改动cover_cnt[node], 否则递归到左右儿子,最后统一pushup。递归深度,不会爆栈。 cover_len/cover_cnt用长度的扁平列表存放, 下标 1为根,node << 1、node << 1 | 1为左右儿子; Python 里没有pushdown,正好对应「cover_cnt不下传」的设计。edges生成式把每个矩形同时产出两条竖边, 右端点写成y_rank[y2] - 1,见上文「第一步」的解释。solve按空白切分整份输入并用指针pos顺序读,n = 0时while立即结束,恰好对应题面「该用例无需处理」。
复杂度
- 时间
:竖边排序 ; 条竖边各做一次 的区间加,每次 pushup是常数次算术;面积累加。 离散化排序同样 。 - 空间
: ys、y_rank、edges与两棵四倍数组均为线性。
总结
- 破题点是竖条内部覆盖状态恒定:所有矩形的竖直边只落在有限个
坐标上, 于是二维面积并可拆成有限个竖条,竖条内的覆盖是一维区间并。 - 暴力做法每个竖条重算一次区间并,瓶颈是
; 观察到相邻竖条之间只差一小部分竖边,把「竖条」换成「竖边事件」, 覆盖长度就能增量维护。 - 增量维护的工具是线段树:
cover_cnt记录整段被覆盖次数且不下传,cover_len按「整段覆盖→段长 / 叶子→0 / 否则左右相加」回填, 区间加与询问都是。 - 扫描线上两个容易写错的细节:先结算
再更新; 以及 y区间是段不是点,右端点要减 1。 - 同一套模型可以直接推广到矩形周长并:把面积换成「相邻竖条覆盖长度之差」 与「竖条内被覆盖段数」,只需要在线段树上多维护一个段数与左右端点是否被覆盖。
