扫描线
简介
扫描线一般运用在图形上面,顾名思义,就是一条线在整个图上扫来扫去,它一般被用来解决图形面积,周长等问题。
扫描线的主要思想是虚拟出一条平行与 x 或 y 轴的无限长的线一路扫过去,如果发现触碰到矩形的边(被当前扫描线完全覆盖)就停下进行相关操作。
一、面积并
题型:在二维坐标系上,给出多个矩形的左下以及右上坐标,求出所有矩形构成的图形的面积。
考虑一个这样的例子
假设一个自下而上的扫描线,每次会在碰到横边的时候停下来
为了快速计算出截线段长度,可以将横边赋上不同的权值,具体为:对于一个矩形,其下边权值为 1,上边权值为 −1
然后把所有的横边按照 y 坐标升序排序。这样,对于每个矩形,扫描线总是会先碰到下边,然后再碰到上边。那么就能保证扫描线所截的长度永远非负
这样操作以后,就可以和线段树扯上关系。先把所有端点在 x 轴上离散化(其实就是把所有点的横坐标存到 X 数组里,然后排个序并且去重
在这个例子中,4 个端点的纵投影总共把 x 轴切割成了 5 部分。取中间的 3 部分线段,建立一棵线段树,其每个端点维护一条线段(也就是一个区间)的信息,之后整个问题就转化为了一个区间查询问题,即每次将当前扫描线扫到的边对应的信息按照之前赋上的权值更新,然后再查询线段树根节点的信息,最后得到当前扫描线扫过的面积
一些具体实现
对于线段树,我们大部分情况维护一个 sum 为当前区间被几个矩形覆盖,以及一个 len 表示当前区间被覆盖的区间长度
建树
void build(int i, int l, int r) {
tree[i].l = l; tree[i].r = r;
tree[i].len = tree[i].sum = 0;
if (l == r) return;
int mid = (l + r) >> 1;
build(i << 1, l, mid);
build(i << 1 | 1, mid + 1, r);
return;
}
pushup
我们扫到一条线,就对这个区间的 sum 进行修改,再由 cnt 对 len 修改,并且把 len 向上更新
inline void pushup(int i) {
int l = tree[i].l, r = tree[i].r;
if (tree[i].sum) tree[i].len = x[r + 1] - x[l]; //被覆盖过则更新长度
else tree[i].len = tree[i << 1].len + tree[i << 1 | 1].len; //合并儿子信息
}
在 pushup 函数中我们对于被完全覆盖的区间,是用了其区间右端点 +1 进行计算。这是因为在线段树的叶节点上,线段树上的区间是 [l, l] 但是这样的区间在几何中长度为 0 了,所以我们令区间 [l, r] 表示的区间实际为 [x[l], x[r+1]] (x 是离散化数组) 就可以解决这个问题
例题
P5490 【模板】扫描线
#include <bits/stdc++.h>
using namespace std;
const int maxn = 1e6 + 10;
int n;
long long x1, yy1, x2, y2, ans;
long long x[maxn << 1];
struct scanline{
long long l, r, h;
int flag;
bool operator < (const scanline &num) const {
return h < num.h;
}
}line[maxn << 1];
struct node{
int l, r, sum;
long long len;
}tree[maxn << 2];
void build(int i, int l, int r) {
tree[i].l = l; tree[i].r = r;
tree[i].len = tree[i].sum = 0;
if (l == r) return;
int mid = (l + r) >> 1;
build(i << 1, l, mid);
build(i << 1 | 1, mid + 1, r);
return;
}
inline void pushup(int i) {
int l = tree[i].l, r = tree[i].r;
if (tree[i].sum) tree[i].len = x[r + 1] - x[l];
else tree[i].len = tree[i << 1].len + tree[i << 1 | 1].len;
}
void update(int i, long long L, long long R, int num) {
int l = tree[i].l, r = tree[i].r;
//注意: l、r和L、R的意义完全不同。l、r表示这个节点管辖的下标范围,而L、R则表示需要修改的真实区间
if (x[r + 1] <= L || x[l] >= R) return;
if (L <= x[l] && x[r + 1] <= R) {
tree[i].sum += num;
pushup(i);
return;
}
update(i << 1, L, R, num);
update(i << 1 | 1, L, R, num);
pushup(i);
}
signed main() {
cin >> n;
for (int i = 1; i <= n; i++) {
scanf("%lld%lld%lld%lld", &x1, &yy1, &x2, &y2);
x[2 * i - 1] = x1; x[2 * i] = x2;
line[2 * i - 1] = (scanline){x1, x2, yy1, 1};
line[2 * i] = (scanline){x1, x2, y2, -1};
}
n <<= 1;
sort(x + 1, x + 1 + n);
sort(line + 1, line + 1 + n);
int tot = unique(x + 1, x + 1 + n) - (x + 1);
build(1, 1, tot - 1);
//[1, tot - 1]描述的就是[X[1], X[tot]]
for (int i = 1; i < n; i++) {
update(1, line[i].l, line[i].r, line[i].flag);
ans += tree[1].len * (line[i + 1].h - line[i].h);
}
printf("%lld\n", ans);
return 0;
}
POJ-1151 Atlantis
#include <iostream>
#include <cmath>
#include <cstring>
#include <cstdio>
#include <algorithm>
using namespace std;
const int maxn = 1e6 + 10;
int n, cnt;
double x1, yy1, x2, y2, ans;
double x[maxn << 1];
struct scanline{
double l, r, h;
int flag;
bool operator < (const scanline &num) const {
return h < num.h;
}
}line[maxn << 1];
struct node{
int l, r, sum;
double len;
}tree[maxn << 2];
inline void pushup(int i) {
int l = tree[i].l, r = tree[i].r;
if (tree[i].sum) tree[i].len = x[r + 1] - x[l];
else tree[i].len = tree[i << 1].len + tree[i << 1 | 1].len;
}
void build(int i, int l, int r) {
tree[i].l = l; tree[i].r= r;
tree[i].len = tree[i].sum = 0;
if (l == r) return;
int mid = (l + r) >> 1;
build(i << 1, l, mid);
build(i << 1 | 1, mid + 1, r);
pushup(i);
return;
}
void update(int i, double L, double R, int num) {
int l = tree[i].l, r = tree[i].r;
if (x[r + 1] <= L || x[l] >= R) return;
if (L <= x[l] && x[r + 1] <= R) {
tree[i].sum += num;
pushup(i);
return;
}
update(i << 1, L, R, num);
update(i << 1 | 1, L, R, num);
pushup(i);
}
signed main() {
while (cin >> n && n) {
ans = 0; cnt++;
for (int i = 1; i <= n; i++) {
cin >> x1 >> yy1 >> x2 >> y2;
x[2 * i - 1] = x1; x[2 * i] = x2;
line[2 * i - 1] = (scanline){x1, x2, yy1, 1};
line[2 * i] = (scanline){x1, x2, y2, -1};
}
n <<= 1;
sort(x + 1, x + 1 + n);
sort(line + 1, line + 1 + n);
int tot = unique(x + 1, x + 1 + n) - (x + 1);
build(1, 1, tot - 1);
for (int i = 1; i < n; i++) {
update(1, line[i].l, line[i].r, line[i].flag);
ans += tree[1].len * (line[i + 1].h - line[i].h);
}
printf("Test case #%d\n", cnt);
printf("Total explored area: %.2f\n\n", ans);
}
return 0;
}
二、周长并
以 P1856 [IOI1998] [USACO5.5] 矩形周长Picture 为例,假使有如下几条扫描线,考虑所截得的竖边长
发现:纵边总长度 = ∑ (2 × 被截得的线段条数 × 扫过的高度)
发现:横边总长度 = ∑∣上次截得的总长 − 现在截得的总长∣
所以和面积并比起来,周长并中的线段树还要维护线段的条数。另外,横边和纵边需分别计算
具体表现为:
#include <bits/stdc++.h>
using namespace std;
const int maxn = 5e3 + 10;
int n, x1, yy1, x2, y2, ans, pre;
int x[maxn << 1];
struct scanline{
int l, r, h, flag;
bool operator < (const scanline &num) const {
if (h == num.h) return flag > num.flag;
return h < num.h;
}
}line[maxn << 1];
struct node{
int l, r, sum, len, cnt;
//cnt表示区间线段条数
bool lc, rc;
}tree[maxn << 2];
void build(int i, int l, int r) {
tree[i].l = l; tree[i].r = r;
tree[i].lc = tree[i].rc = 0;
tree[i].len = tree[i].sum = tree[i].cnt = 0;
if (l == r) return;
int mid = (l + r) >> 1;
build(i << 1, l, mid);
build(i << 1 | 1, mid + 1, r);
return;
}
inline void pushup(int i) {
int l = tree[i].l, r = tree[i].r;
if (tree[i].sum) {
tree[i].len = x[r + 1] - x[l];
tree[i].lc = tree[i].rc = true;
tree[i].cnt = 1; //做好相应的标记
}
else {
tree[i].len = tree[i << 1].len + tree[i << 1 | 1].len;
tree[i].lc = tree[i << 1].lc;
tree[i].rc = tree[i << 1 | 1].rc;
tree[i].cnt = tree[i << 1].cnt + tree[i << 1 | 1].cnt;
//如果左儿子右端点和右儿子左端点都被覆盖,那么中间就是连续的一段,所以要-1
if (tree[i << 1].rc && tree[i << 1 | 1].lc) tree[i].cnt--;
}
}
void update(int i, long long L, long long R, int num) {
int l = tree[i].l, r = tree[i].r;
if (x[r + 1] <= L || x[l] >= R) return;
if (L <= x[l] && x[r + 1] <= R) {
tree[i].sum += num;
pushup(i);
return;
}
update(i << 1, L, R, num);
update(i << 1 | 1, L, R, num);
pushup(i);
}
signed main() {
cin >> n;
for (int i = 1; i <= n; i++) {
scanf("%d%d%d%d", &x1, &yy1, &x2, &y2);
x[2 * i - 1] = x1; x[2 * i] = x2;
line[2 * i - 1] = (scanline){x1, x2, yy1, 1};
line[2 * i] = (scanline){x1, x2, y2, -1};
}
n <<= 1;
sort(x + 1, x + 1 + n);
sort(line + 1, line + 1 + n);
int tot = unique(x + 1, x + 1 + n) - (x + 1);
build(1, 1, tot - 1);
for (int i = 1; i < n; i++) {
update(1, line[i].l, line[i].r, line[i].flag);
ans += abs(pre - tree[1].len);
pre = tree[1].len;
ans += 2 * tree[1].cnt * (line[i + 1].h - line[i].h);
}
ans += line[n].r - line[n].l; //特判一下枚举不到的最后一条扫描线
printf("%d\n", ans);
return 0;
}
参考:
【学习笔记】扫描线
cbdsopa-扫描线