每颗星拆成 +c/-c 两个 x 方向事件按竖线扫描,y 方向降成「窗底区间 [y-H+1, y] 带权覆盖最大值」,用区间加+懒标记线段树维护全局最大覆盖,O(n log n)。
OJ: roj
题目 ID: 3115
难度:提高+/省选-
标签:python扫描线线段树离散化
创建: 2026-10-01 17:53
更新: 2026-10-10 11:30
形式化题目
平面上给定
数据范围:
样例:
暴力解法
思路
最朴素的想法是枚举窗口左下角
每颗星在窗内的条件是
共
代码
复杂度
- 时间
(候选位置 × 每位置统计 )。 - 空间
。
瓶颈
瓶颈在「每个候选位置都从头统计一遍」。但相邻候选之间窗内的星几乎不变:
- 横方向:
在两个相邻事件坐标之间滑动时,窗内星集完全不变—— 横位置其实只有 条"竖线",答案只在竖线上结算。 - 纵方向:固定竖线后,「选窗底
最大化权值和」是一维问题, 每条竖线重新排序滑窗太浪费,它也应该随扫描增量变化。
两个方向合起来,就是扫描线 + 线段树的正解。
正解
关键观察
观察一(竖线扫描):每颗星拆成两个事件——左边界到达
观察二(y 方向降维):固定窗内星集后,剩下的问题是选窗底
维护每个窗底位置的覆盖值
, 支持区间加、询问全局最大值。
观察三(线段树登场):事件只有"区间
思路
第一步,离散化窗底。 把每颗星的
第二步,事件化。 每颗星
:左边界贴上 ,星进入; :左边界越过 ,星离开。
事件按
第三步,扫描与结算。 流程骨架:
ans = 0
for 每条竖线 x(把同 x 的事件全部处理完):
区间加 ±c
ans = max(ans, 根的全局最大覆盖)第四步,线段树细节。 结点维护两个值:
MX[node]:结点区间内所有候选窗底的覆盖值最大值;LAZY[node]:还没下传给儿子的区间加标记。
区间加完全落在结点区间内时只改 MX 与 LAZY;
否则下传 LAZY(儿子的 MX 与 LAZY 都加上去),递归两侧,
最后 MX[node] = max(MX[左], MX[右]) + LAZY[node]。
"父结点回填时加上自己的 LAZY"是懒标记不必每次下传的关键——
读根 MX[1] 时,沿途未下传的加值已经体现在根自己身上。
样例(
| 竖线 | 事件 | 变化 | 星 |
含义 |
|---|---|---|---|---|
| 1 | 2 | 星 |
||
| 2 | 3 | 星 |
||
| 3 | 3 | 星 |
||
| 3 | 2 | 星 |
||
| 4 | 3 | 星 |
||
| 5 | 3 | 星 |
观察点:竖线 3 上"先
建模主线(从星星到答案):
flowchart LR
A["每颗星 x, y, c"] --> B["x 方向:拆成 +c 进 / -c 出 两个事件"]
A --> C["y 方向:贡献区间 [y-H+1, y]"]
B --> D["按 x 扫描竖线(同 x 先加后减)"]
C --> E["端点离散化成候选窗底"]
D --> F["线段树区间 加减c"]
E --> F
F --> G["每条竖线后读根的全局最大"]
G --> H["ans = max(ans, 根最大值)"]
代码
/**
* 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,线段树按「窗口底位置」维护覆盖亮度的最大值。
#include <cstdio>
#include <vector>
#include <algorithm>
typedef long long ll;
struct Star {
ll x, y, c;
};
struct Event {
ll x;
ll sign; // 进入窗口 +1、离开 -1
ll c;
ll l, r; // 窗口底区间 [l, r]
};
std::vector<ll> MX; // 线段树节点:该区间内窗口底的覆盖亮度最大值
std::vector<ll> LAZY; // 线段树节点:还没下传的区间加标记
bool event_less(const Event& a, const Event& b) {
if (a.x != b.x) return a.x < b.x;
return a.sign > b.sign; // x 相同先加后减
}
// 把窗口底区间 [l, r] 上每个位置的覆盖亮度加 delta
void range_add(ll node, ll lo, ll hi, ll l, ll r, ll delta) {
if (r < lo || hi < l) return;
if (l <= lo && hi <= r) {
MX[node] += delta;
LAZY[node] += delta;
return;
}
if (LAZY[node]) { // 下传标记
MX[node * 2] += LAZY[node];
LAZY[node * 2] += LAZY[node];
MX[node * 2 + 1] += LAZY[node];
LAZY[node * 2 + 1] += LAZY[node];
LAZY[node] = 0;
}
ll mid = (lo + hi) >> 1;
range_add(node * 2, lo, mid, l, r, delta);
range_add(node * 2 + 1, mid + 1, hi, l, r, delta);
MX[node] = (MX[node * 2] > MX[node * 2 + 1] ? MX[node * 2] : MX[node * 2 + 1]) + LAZY[node];
}
int main() {
ll n;
while (scanf("%lld", &n) == 1) {
ll width, height;
scanf("%lld %lld", &width, &height);
std::vector<Star> stars(n);
std::vector<ll> points;
for (ll i = 0; i < n; i++) {
scanf("%lld %lld %lld", &stars[i].x, &stars[i].y, &stars[i].c);
// 覆盖亮度只在区间端点 y-height+1 和 y 处变化
points.push_back(stars[i].y - height + 1);
points.push_back(stars[i].y);
}
std::sort(points.begin(), points.end());
points.erase(std::unique(points.begin(), points.end()), points.end());
ll m = (ll)points.size();
if (m == 0) {
printf("0\n");
continue;
}
std::vector<Event> events;
for (ll i = 0; i < n; i++) {
ll l = std::lower_bound(points.begin(), points.end(), stars[i].y - height + 1) - points.begin();
ll r = std::lower_bound(points.begin(), points.end(), stars[i].y) - points.begin();
Event e;
e.x = stars[i].x; e.sign = 1; e.c = stars[i].c; e.l = l; e.r = r; events.push_back(e);
e.x = stars[i].x + width; e.sign = -1; e.c = stars[i].c; e.l = l; e.r = r; events.push_back(e);
}
std::sort(events.begin(), events.end(), event_less);
MX.assign(4 * m + 4, 0);
LAZY.assign(4 * m + 4, 0);
ll ans = 0;
ll i = 0;
ll total = 2 * n;
while (i < total) {
ll x = events[i].x;
while (i < total && events[i].x == x) { // 同一条竖线:整列一起扫进/扫出
range_add(1, 0, m - 1, events[i].l, events[i].r, events[i].sign * events[i].c);
i++;
}
// 扫到下一条竖线前,窗口左右可停在任意位置
if (MX[1] > ans) ans = MX[1];
}
printf("%lld\n", ans);
}
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:53
# update_at: 2026-10-01 17:53
import sys
import bisect
MX: list[int] = [] # 线段树节点:该区间内所有窗口底位置的覆盖亮度最大值
LAZY: list[int] = [] # 线段树节点:还没下传给儿子的区间加标记
def range_add(node: int, lo: int, hi: int, l: int, r: int, delta: int) -> None:
"""把窗口底区间 [l, r] 上每个位置的覆盖亮度加 delta。"""
if r < lo or hi < l:
return
if l <= lo and hi <= r:
MX[node] += delta
LAZY[node] += delta
return
if LAZY[node]: # 下传标记:儿子的最大值同样被整段抬高
for child in (node * 2, node * 2 + 1):
MX[child] += LAZY[node]
LAZY[child] += LAZY[node]
LAZY[node] = 0
mid = (lo + hi) >> 1
range_add(node * 2, lo, mid, l, r, delta)
range_add(node * 2 + 1, mid + 1, hi, l, r, delta)
MX[node] = max(MX[node * 2], MX[node * 2 + 1]) + LAZY[node]
def solve() -> None:
global MX, LAZY
data = iter(map(int, sys.stdin.buffer.read().split()))
out: list[str] = []
while True:
try:
n = next(data)
except StopIteration:
break
width, height = next(data), next(data)
stars = [(next(data), next(data), next(data)) for _ in range(n)]
# 离散化窗口底位置:覆盖亮度只在区间端点 y-height+1 和 y 处变化,
# 每颗星对应底边区间 [y-height+1, y](边界上的星不算,所以左端取 +1)。
points = sorted({v for _, y, _ in stars for v in (y - height + 1, y)})
m = len(points)
# 每颗星拆成两个事件:x 进入窗口左边界时 +c,右边界越过它时 -c。
events: list[tuple[int, int, int, int, int]] = []
for x, y, c in stars:
l = bisect.bisect_left(points, y - height + 1)
r = bisect.bisect_left(points, y)
events += [(x, 1, c, l, r), (x + width, -1, c, l, r)]
# x 相同先加后减:左边界贴上某颗星时它已在窗内,右边界压住它时已移出。
events.sort(key=lambda e: (e[0], -e[1]))
MX = [0] * (4 * m)
LAZY = [0] * (4 * m)
ans = 0
i = 0
while i < 2 * n:
x = events[i][0]
while i < 2 * n and events[i][0] == x: # 同一条竖线:整列一起扫进/扫出
_, sign, c, l, r = events[i]
range_add(1, 0, m - 1, l, r, sign * c)
i += 1
# 扫到下一条竖线前,窗口左右可停在任意位置,覆盖亮度只由窗底决定。
ans = max(ans, MX[1])
out += [str(ans)]
print('\n'.join(out))
if __name__ == "__main__":
solve()实现说明:
range_add是裸的递归线段树区间加:完全覆盖时改MX/LAZY; 否则先下传LAZY,递归两侧,回填时max(左右) + LAZY[node]。- 全局最大值永远是
MX[1],所以不需要单独的查询函数; 每条竖线结算一次,。 events生成式同时产出一条星的进入与离开事件;排序键(e[0], -e[1])中第二维取-e[1](sign 的相反数)实现"先加后减"。points是与 的排序去重; bisect_left把区间两端 映射成线段树叶子下标。- 多组用例:
MX/LAZY用全局列表,每组重建;输出按组拼接。
复杂度
- 时间
:事件排序 ; 次区间加各 ; 每条竖线读根 。离散化排序同为 。 - 空间
:事件 条,线段树 。
总结
- 破题点是两层降维:横方向把窗口位置压成
条竖线(事件扫描), 纵方向把"选窗底"变成"带权区间覆盖的最大值"。 - 每颗星拆成
/ 两个事件,配上线段树区间加, "窗内星集"与"每个窗底的覆盖值"都能增量维护,总复杂度 。 - 「边界上的星不算」在本题有三处落点:横方向同竖线先加后减、
纵方向贡献区间左端取
、以及结算时机在"整条竖线处理完之后"。 这类语义细节是本题最容易出错的地方。 - 线段树实现上,懒标记采用"下传 + 父结点回填加自身 LAZY"的标准写法; 由于只询问全局最大值,查询退化成读根,非常干净。
- 同一套模型还能解决矩形面积并(ROJ 3114「Atlantis」)与 POJ 2482 同型题: 区别只在线段树维护的是"覆盖长度"还是"覆盖最大权"。
图示解析
本题从题意到答案的主线:
窗口圈星(边界不算)
`- 每颗星拆成两个 x 事件:+c 进窗 / -c 出窗
`- 按竖线扫描 x,窗内星集只随事件变化
`- y 方向降维:星对窗底 t 的贡献是区间 [y-H+1, y]
`- 线段树区间 ±c 维护「每个候选窗底的覆盖值」
`- 每条竖线后取根的全局最大值 → 答案顺着分支看:第一层把二维的窗口选择降成对竖线的扫描;
第二层把"窗内星集"变成增量维护的状态;
第三层把剩下的一维问题(选窗底最大化带权覆盖)交给线段树;
最后一层利用"全局最大值只读根"做到每次结算