扫描线
线段树的一大应用是扫描线。
扫描线一个很经典的例题:在坐标轴上有若干个矩形,问他们覆盖的面积总和。
扫描线求重叠矩形面积
思路
使用一条垂直于X轴的直线,从左到右来扫描这个图形,明显,只有在碰到矩形的左边界或者右边界的时候,这个线段所扫描到的情况才会改变。
所以把所有矩形的入边,出边按 X 值排序。然后根据 X 值从小到大去处理。
用线段树来维护扫描到的情况,即每条线段映射到 Y 轴上进行区间维护。
算法流程
- 一条垂直于 x 轴的直线,从左边向右平移
- 此时 y 轴上有一棵线段树,它记录的是 y 轴上每个点的覆盖次数
- 每当遇到某个矩形的某一条边时,就计算面积 —— (该边 x 坐标 – 上一边 x 坐标)* (当前整个 y 轴上被覆盖的点数)
- 当遇到某个矩形的左边,将这个矩形的左边所对应的y轴区间的覆盖次数 + 1。当遇到某个矩形的右边时,就相应的 − 1。
- 继续向右移动……
(注意,3操作要在4操作之前!)
性质
如何计算面积?
两条扫描线的间距很容易得到,重点是线段树部分的实现。
线段树操作:
- 区间修改([L, R] + k)
- 区间查询(整个 y 轴上被覆盖次数大于 0 的点数)
在此可以发现这道题的两个性质:
-
对于询问,每次都查询的是根节点的信息,所以不需要
query
。 -
所有修改,都是成对出现,且对于每个区间一定是先执行 + {+} + 操作,在执行 − {-} − 操作,即 c n t > = 0 {cnt >= 0} cnt>=0 恒成立。
- 也就是说,相应的区间前后操作的都是线段树上完全相同的节点。(这很关键)
修改不需要pushdown
的两种情况讨论:
-
c n t > 0 {cnt > 0} cnt>0:
-
执行修改操作时,如果父节点 u {u} u的 c n t {cnt} cnt减 1 {1} 1变为了 0 {0} 0( t r [ u ] . c n t − 1 = = 0 {tr[u].cnt - 1 == 0} tr[u].cnt−1==0),则其孩子 l s , r s {ls, rs} ls,rs的 c n t {cnt} cnt 也要相应减 1 {1} 1,如果 t r [ l s ] . c n t {tr[ls].cnt} tr[ls].cnt变为了 0 {0} 0,它的 l e n {len} len就发生变化了。而 t r [ u ] . l e n = t r [ l s ] . l e n + t r [ r s ] . l e n {tr[u].len = tr[ls].len + tr[rs].len} tr[u].len=tr[ls].len+tr[rs].len,用到的 t r [ l s ] . l e n {tr[ls].len} tr[ls].len 是没更新过的 l e n {len} len。
-
换句话说,modify执行到这里时,是从上到下的顺序。可是父节点却需要用到了孩子的信息,显然会有问题。如果说先pushdown算的孩子信息,那么最坏情况当一个cnt减到0,则就要递归到最底层。
-
所以在此, t r [ u ] . c n t {tr[u].cnt} tr[u].cnt的含义是:当前区间被覆盖的次数,跟其它节点无关。
-
那么一个节点代表的区间被覆盖的次数不需要继承其父亲的信息。因此需要去掉pushdown,当cnt减为0, t r [ u ] . l e n = t r [ l s ] . l e n + t r [ r s ] . l e n {tr[u].len = tr[ls].len + tr[rs].len} tr[u].len=tr[ls].len+tr[rs].len就是对的。因为 t r [ u ] . c n t {tr[u].cnt} tr[u].cnt减到0跟 t r [ l s ] . c n t {tr[ls].cnt} tr[ls].cnt没有关系,此时 t r [ l s ] . c n t {tr[ls].cnt} tr[ls].cnt是最新值。
-
-
c n t = 0 {cnt = 0} cnt=0:
- 本在pushdown中,add为0就不需要下传懒标记信息,等同于没pushdown。
又因为query函数没有,所以本题可以不用pushdown。
首先将所有矩形的入边,出边都存起来,然后根据 X 值排序。用结构体,来存这些信息,然后排序。
struct Line {
double x, y1, y2;
double d; // 表示+1 or -1
bool operator<(const Line &A) const {
return x < A.x;
}
} line[N << 1];
- 线段树节点
struct node {
int l, r;
int cnt; // 重复包含次数
double len; // 当前节点维护的区间内部,有效的线段长度
} tr[N << 3];
pushup
void pushup(int u) {
// 当前区间被覆盖,该段长度就为右端点 + 1后在ys中的值 - 左端点在ys中的值
if(tr[u].cnt) tr[u].len = ys[tr[u].r + 1] - ys[tr[u].l];
// 该段不被完全覆盖,那么清空该段的len值,而由可能存在其子区间段对len的贡献来更新
else if(tr[u].l != tr[u].r) tr[u].len = tr[u << 1].len + tr[u << 1 | 1].len;
// 叶子节点,len=0
else tr[u].len = 0;
}
经典例题
247. 亚特兰蒂斯
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 100010;
struct Line {
double x, y1, y2;
double d;
bool operator<(const Line &A) const {
return x < A.x;
}
} line[N << 1];
struct node {
int l, r;
int cnt; // 重复包含次数
double len; // 当前节点维护的区间内部,有效的线段长度
} tr[N << 3];
vector<double> ys;
int n, t;
int find(double x) {
return lower_bound(ys.begin(), ys.end(), x) - ys.begin();
}
void pushup(int u) {
// 当前区间被覆盖,该段长度就为右端点 + 1后在ys中的值 - 左端点在ys中的值
if(tr[u].cnt) tr[u].len = ys[tr[u].r + 1] - ys[tr[u].l];
// 该段不被完全覆盖,那么清空该段的len值,而由可能存在其子区间段对len的贡献来更新
else if(tr[u].l != tr[u].r) tr[u].len = tr[u << 1].len + tr[u << 1 | 1].len;
// 叶子节点,len=0
else tr[u].len = 0;
}
void build(int u, int l, int r) {
tr[u] = {l, r, 0, 0};
if(l == r) { return ; }
int mid = l + r >> 1;
build(u << 1, l, mid), build(u << 1 | 1, mid + 1, r);
}
// 线段树中 l点到r点出现次数 +k
void modify(int u, int l, int r, int k) {
if(tr[u].l >= l && tr[u].r <= r) {
tr[u].cnt += k;
pushup(u);
return ;
}
int mid = tr[u].l + tr[u].r >> 1;
if(l <= mid) modify(u << 1, l, r, k);
if(r > mid) modify(u << 1 | 1, l, r, k);
pushup(u);
}
int main()
{
while(cin >> n, n) {
ys.clear();
double x1, y1, x2, y2;
int j = 0;
for (int i = 0; i < n; i++) {
cin >> x1 >> y1 >> x2 >> y2;
line[j++] = {x1, y1, y2, 1};
line[j++] = {x2, y1, y2, -1};
ys.push_back(y1), ys.push_back(y2);
}
sort(line, line + j);
sort(ys.begin(), ys.end());
ys.erase(unique(ys.begin(), ys.end()), ys.end());
build(1, 0, ys.size() - 2);
double res = 0;
for (int i = 0; i < j; i++) {
if(i) res += tr[1].len * (line[i].x - line[i - 1].x);
modify(1, find(line[i].y1), find(line[i].y2) - 1, line[i].d);
}
printf("Test case #%d\n", ++t);
printf("Total explored area: %.2lf\n\n", res);
}
return 0;
}