扫描线
扫描线是计算几何中一种非常经典且高效的算法思想,它的核心逻辑是将多维空间中的几何问题,“降维”处理。想象一根无限长的直线,例如垂直线,从左向右在平面上扫过。随着直线的移动,只关注与扫描线相交的那些对象,并实时维护它们的状态。
例题:P5490 【模板】扫描线
求 \(n \ (1 \le n \le 10^5)\) 个坐标轴平行的矩形(用坐标点 \((x_1,y_1)\) 和 \((x_2,y_2) \ (0 \le x_1 \lt x_2 \le 10^9, \ 0 \le y_1 \lt y_2 \le 10^9)\) 给出)覆盖的总面积(面积并)。
由于坐标范围极大,不能使用二维数组模拟。
想象一根垂直于 \(x\) 轴的直线从左向右扫描:当遇到矩形的左边界时,该矩形开始覆盖一段 \(y\) 轴区间;当遇到矩形的右边界时,该矩形停止覆盖该段区间。
将矩形的左右边界称为“边”,每条边包含:\(x\) 坐标;\(y\) 轴范围 \([y_1,y_2]\);类型(左边界为入边,右边界为出边)。
将所有边按 \(x\) 坐标从小到大排序,则扫描过程中,两根相邻扫描线之间的面积等于 \(\Delta x\) 乘上此时覆盖 \(y\) 轴的长度。
由于 \(y\) 坐标分布稀疏且范围大,收集所有 \(y\) 坐标并去重排序。设去重后的 \(y\) 坐标序列为 \(y_1, y_2, \dots, y_m\),这些点将 \(y\) 轴切分为 \(m-1\) 个基本区间 \([y_1,y_2], [y_2,y_3], \dots, [y_{m-1},y_m]\)。
用线段树维护 \(y\) 轴的覆盖情况,每个叶子节点代表其中的一个基本区间,树上的每个节点存储当前节点对应的区间被完整覆盖了多少次以及实际被覆盖的 \(y\) 轴长度。
合并逻辑:如果当前节点对应的区间的完整覆盖次数大于 0,说明该区间被完全覆盖,其实际被覆盖的 \(y\) 轴长度为 \(y\) 坐标序列上对应的两个点相减。如果完整覆盖次数等于 0,若其为叶子节点,长度为 0,若非叶子节点,长度为左右孩子的长度相加。
关键点:由于扫描线中的覆盖操作是成对出现的(入边加一,出边减一),且总是查询根节点的全局信息,因此该线段树不需要下传标记。
参考代码
#include <cstdio>
#include <algorithm>
using namespace std;
using ll = long long;
const int N = 1e5 + 5;
struct Edge {
int x, y1, y2, k; // k=1 表示入边(左边),-1 表示出边(右边)
bool operator<(const Edge &e) const {
return x < e.x;
}
};
struct Node {
int cnt, len;
};
Edge e[N * 2]; // 2n 条边
int y[N * 2]; // 2n 个 y 坐标
Node tr[N * 8]; // 线段树空间通常开 4 倍,离散化后区间数为 2n,故开 8 倍
// 更新线段树节点信息
void pushup(int p, int l, int r) {
if (tr[p].cnt > 0) {
// 如果该区间被完全覆盖,长度为离散化后的实际长度
tr[p].len = y[r + 1] - y[l];
} else if (l == r) {
// 叶子节点且未被完全覆盖
tr[p].len = 0;
} else {
// 非叶子节点,长度由子节点合并
tr[p].len = tr[p * 2].len + tr[p * 2 + 1].len;
}
}
// 区间修改
void update(int p, int l, int r, int x, int y, int v) {
// 这里的区间 [x, y] 对应离散化 y 坐标的索引范围
if (x <= l && r <= y) {
tr[p].cnt += v;
pushup(p, l, r);
return;
}
int mid = (l + r) >> 1;
if (x <= mid) update(p * 2, l, mid, x, y, v);
if (y > mid) update(p * 2 + 1, mid + 1, r, x, y, v);
pushup(p, l, r);
}
int main()
{
int n; scanf("%d", &n);
for (int i = 0; i < n; i++) {
int x1, y1, x2, y2;
scanf("%d%d%d%d", &x1, &y1, &x2, &y2);
// 存储扫描线
e[i * 2] = {x1, y1, y2, 1};
e[i * 2 + 1] = {x2, y1, y2, -1};
// 收集 y 坐标
y[i * 2 + 1] = y1;
y[i * 2 + 2] = y2;
}
// 1. 扫描线按 x 排序
sort(e, e + 2 * n);
// 2. 离散化 y 坐标
sort(y + 1, y + 2 * n + 1);
int m = unique(y + 1, y + 2 * n + 1) - (y + 1);
// 3. 构建线段树并计算面积
// 线段树维护的是离散化后的区间 [y[i], y[i+1]]
// 共有 m 个 y 坐标,则有 m-1 个基本区间
ll ans = 0;
for (int i = 0; i < 2 * n - 1; i++) {
// 找到当前扫描线在离散化数组中的位置
int l = lower_bound(y + 1, y + m + 1, e[i].y1) - y;
int r = lower_bound(y + 1, y + m + 1, e[i].y2) - y;
// 线段树区间对应:第 j 个叶子节点代表 [y[j], y[j+1]]
// 所以 y2 对应的 r 需要减 1
if (l < r) update(1, 1, m - 1, l, r - 1, e[i].k);
ans += 1ll * tr[1].len * (e[i + 1].x - e[i].x);
}
printf("%lld\n", ans);
return 0;
}
排序的时间复杂度为 \(O(n \log n)\),离散化 \(O(n \log n)\),扫描线处理 \(O(n \log n)\),总时间复杂度为 \(O(n \log n)\)。
例题:P1502 窗口的星星
在一个二维平面上有 \(n \ (1 \le n \le 10^4)\) 颗星星,每颗星星有坐标 \((x_i,y_i) \ (0 \le x_i,y_i \lt 2^{31})\) 和亮度 \(l_i \ (0 \le l_i \le 1000)\)。给定一个固定大小为 \(W \times H \ (1 \le W,H \le 10^6)\) 的窗户,求窗户能够包含的星星亮度总和的最大值。注意:落在窗户边界上的星星不计入总和。
设窗户的左下角坐标为 \((X,Y)\),则其覆盖的区域为 \((X,W+W) \times (Y,Y+H)\)。一颗星星 \((x,y)\) 能被包含在该窗户内,必须满足 \(X \lt x \lt X+W \iff x-W \lt X \lt x\) 和 \(Y \lt y \lt Y+H \iff y-H \lt Y \lt y\)。
这意味着,对于每颗星星 \((x, y)\),它对窗户左下角坐标 \((X, Y)\) 的贡献是一个形状为 \((x-W, x) \times (y-H, y)\) 的开矩形。要找一个点 \((X,Y)\),使得被覆盖的矩形权重之和最大。
由于题目规定边界上的星星不计入,且坐标为整数,可以将开区间转化为闭区间处理。对于 \(x\) 轴,可以认为星星在 \(x\) 处“进入”窗口,在 \(x+W\) 处“离开”窗口。为了处理边界,当 \(x\) 坐标相同时,先处理离开事件(减去亮度),再处理进入事件(加上亮度)。对于 \(y\) 轴,开区间 \((y-H, y)\) 对应整数范围 \([y-H+1, y-1]\)。在实现中,更简便的方法是令每个星星对应 \(y\) 轴上的区间 \([y, y+H-1]\)。
由于 \(y\) 坐标范围极大,需要对所有出现的 \(y\) 和 \(y+H-1\) 进行离散化。
将每个矩形的左右边界转化为事件,左边界 \((x, y, y+H-1, l)\),右边界 \((x+W, y, y+H-1, -l)\),按 \(x\) 从小到大排序。用线段树维护 \(y\) 轴方向上的区间最大值,每处理一个事件,相当于将离散化后的 \(y\) 轴区间 \([L, R]\) 加上权重 \(v\),更新完后,线段树根节点的值即为当前 \(x\) 坐标下窗户能获得的最大亮度。
参考代码
#include <cstdio>
#include <algorithm>
using namespace std;
using ll = long long;
const int N = 1e4 + 5;
struct Edge {
ll x, y1, y2;
int l;
bool operator<(const Edge& e) const {
if (x != e.x) return x < e.x;
return l < e.l; // 负亮度(离开)排在前面,确保边界不计入
}
};
Edge e[N * 2];
ll ys[N * 2];
int tr[N * 8], lazy[N * 8];
void pushdown(int p) {
if (lazy[p] != 0) {
tr[p * 2] += lazy[p];
lazy[p * 2] += lazy[p];
tr[p * 2 + 1] += lazy[p];
lazy[p * 2 + 1] += lazy[p];
lazy[p] = 0;
}
}
void pushup(int p) {
tr[p] = max(tr[p * 2], tr[p * 2 + 1]);
}
void update(int p, int l, int r, int x, int y, int v) {
if (x <= l && r <= y) {
tr[p] += v; lazy[p] += v;
return;
}
pushdown(p);
int mid = (l + r) >> 1;
if (x <= mid) update(p * 2, l, mid, x, y, v);
if (y > mid) update(p * 2 + 1, mid + 1, r, x, y, v);
pushup(p);
}
void solve() {
int n, w, h;
scanf("%d%d%d", &n, &w, &h);
for (int i = 0; i < n; i++) {
ll x, y; int l; scanf("%lld%lld%d", &x, &y, &l);
// 这里的区间设计确保了:如果两星间距正好是 w 或 h,它们不会被同时计入
e[i * 2] = {x, y, y + h - 1, l};
e[i * 2 + 1] = {x + w, y, y + h - 1, -l};
ys[i * 2 + 1] = y;
ys[i * 2 + 2] = y + h - 1;
}
sort(e, e + 2 * n);
sort(ys + 1, ys + 2 * n + 1);
// 清空线段树,注意范围
int m = unique(ys + 1, ys + 2 * n + 1) - (ys + 1);
for (int i = 0; i <= m * 4; i++) tr[i] = lazy[i] = 0;
int ans = 0;
for (int i = 0; i < n * 2; i++) {
int l = lower_bound(ys + 1, ys + m + 1, e[i].y1) - ys;
int r = lower_bound(ys + 1, ys + m + 1, e[i].y2) - ys;
update(1, 1, m, l, r, e[i].l);
ans = max(ans, tr[1]);
}
printf("%d\n", ans);
}
int main()
{
int t; scanf("%d", &t);
while (t--) solve();
return 0;
}
排序的时间复杂度为 \(O(n \log n)\),离散化 \(O(n \log n)\),扫描线处理过程包含 \(2n\) 次线段树操作,每次 \(O(\log n)\),总时间复杂度为 \(O(T \cdot n \log n)\)。
习题:P3875 [TJOI2010] 被污染的河流
参考代码
#include <cstdio>
#include <algorithm>
using namespace std;
const int MAXN = 10005;
struct Line {
int x, y1, y2, flag;
bool operator<(const Line& other) const {
return x < other.x;
}
} line[MAXN * 2];
int y[MAXN * 2], cnt;
struct Node {
int l, r, len, cnt;
} tree[MAXN * 8];
void pushup(int cur) {
if (tree[cur].cnt) tree[cur].len = y[tree[cur].r] - y[tree[cur].l];
else if (tree[cur].l + 1 == tree[cur].r) tree[cur].len = 0;
else tree[cur].len = tree[cur * 2].len + tree[cur * 2 + 1].len;
}
void build(int cur, int l, int r) {
tree[cur] = {l, r, 0, 0};
if (l + 1 == r) return;
int mid = (l + r) / 2;
build(cur * 2, l, mid);
build(cur * 2 + 1, mid, r);
}
void update(int cur, int l, int r, int d) {
if (tree[cur].l >= r || tree[cur].r <= l) return;
if (tree[cur].l >= l && tree[cur].r <= r) {
tree[cur].cnt += d;
pushup(cur); return;
}
update(cur * 2, l, r, d); update(cur * 2 + 1, l, r, d);
pushup(cur);
}
int main()
{
int n;
scanf("%d", &n);
for (int i = 1; i <= n; i++) {
int x1, y1, x2, y2;
scanf("%d%d%d%d", &x1, &y1, &x2, &y2);
if (x1 == x2) {
line[i] = {x1 - 1, min(y1, y2), max(y1, y2), 1};
line[i + n] = {x1 + 1, min(y1, y2), max(y1, y2), -1};
y[i] = min(y1, y2); y[i + n] = max(y1, y2);
} else {
line[i] = {min(x1, x2), y1 - 1, y1 + 1, 1};
line[i + n] = {max(x1, x2), y1 - 1, y1 + 1, -1};
y[i] = y1 - 1; y[i + n] = y1 + 1;
}
}
sort(line + 1, line + 2 * n + 1);
sort(y + 1, y + 2 * n + 1);
cnt = unique(y + 1, y + 2 * n + 1) - y - 1;
build(1, 1, cnt);
int ans = 0;
for (int i = 1; i < n * 2; i++) {
int y1 = lower_bound(y + 1, y + cnt + 1, line[i].y1) - y;
int y2 = lower_bound(y + 1, y + cnt + 1, line[i].y2) - y;
update(1, y1, y2, line[i].flag);
ans += (line[i + 1].x - line[i].x) * tree[1].len;
}
printf("%d\n", ans);
return 0;
}
习题:P1856 [IOI1998] [USACO5.5] 矩形周长Picture
参考代码
#include <cstdio>
#include <algorithm>
using namespace std;
const int MAXN = 10005;
struct Line {
int x, y1, y2, flag;
bool operator<(const Line& other) const {
return x != other.x ? x < other.x : flag > other.flag;
}
};
Line lx[MAXN], ly[MAXN];
int x[MAXN], y[MAXN], xlen, ylen;
struct Node {
int l, r, cnt, len;
};
Node tree[MAXN * 4];
void pushup(int cur, int a[]) {
if (tree[cur].cnt) tree[cur].len = a[tree[cur].r] - a[tree[cur].l];
else if (tree[cur].l + 1 == tree[cur].r) tree[cur].len = 0;
else tree[cur].len = tree[cur * 2].len + tree[cur * 2 + 1].len;
}
void build(int cur, int l, int r, int a[]) {
tree[cur].l = l; tree[cur].r = r;
if (l + 1 == r) return;
int mid = (l + r) / 2;
build(cur * 2, l, mid, a); build(cur * 2 + 1, mid, r, a);
}
void update(int cur, int l, int r, int d, int a[]) {
if (tree[cur].l >= r || tree[cur].r <= l) return;
if (tree[cur].l >= l && tree[cur].r <= r) {
tree[cur].cnt += d; pushup(cur, a); return;
}
update(cur * 2, l, r, d, a); update(cur * 2 + 1, l, r, d, a);
pushup(cur, a);
}
int main()
{
int n;
scanf("%d", &n);
for (int i = 1; i <= n; i++) {
int x1, y1, x2, y2;
scanf("%d%d%d%d", &x1, &y1, &x2, &y2);
lx[i] = {x1, y1, y2, 1}; lx[n + i] = {x2, y1, y2, -1};
ly[i] = {y1, x1, x2, 1}; ly[n + i] = {y2, x1, x2, -1};
x[i] = x1; x[n + i] = x2; y[i] = y1; y[n + i] = y2;
}
n *= 2;
sort(x + 1, x + n + 1); xlen = unique(x + 1, x + n + 1) - x - 1;
sort(y + 1, y + n + 1); ylen = unique(y + 1, y + n + 1) - y - 1;
sort(lx + 1, lx + n + 1); sort(ly + 1, ly + n + 1);
build(1, 1, ylen, y);
int ans = 0;
for (int i = 1; i <= n; i++) {
int y1 = lower_bound(y + 1, y + ylen + 1, lx[i].y1) - y;
int y2 = lower_bound(y + 1, y + ylen + 1, lx[i].y2) - y;
int pre = tree[1].len;
update(1, y1, y2, lx[i].flag, y);
ans += abs(tree[1].len - pre);
}
build(1, 1, xlen, x);
for (int i = 1; i <= n; i++) {
int x1 = lower_bound(x + 1, x + xlen + 1, ly[i].y1) - x;
int x2 = lower_bound(x + 1, x + xlen + 1, ly[i].y2) - x;
int pre = tree[1].len;
update(1, x1, x2, ly[i].flag, x);
ans += abs(tree[1].len - pre);
}
printf("%d\n", ans);
return 0;
}

浙公网安备 33010602011771号