线段树之扫描线(HDU-1542)
扫描线也算是线段树的经典应用了,本人也是因为扫描线而接触到线段树的,当时做题的时候发现扫描线都是处理一条条的线段,然后搭配上线段树,让我以为线段树的线段就是这么来的(其实我现在也不知道线段树为什么叫做线段树)。
一、常见应用题型
个人认为扫描线的做法主要是用在处理矩形并或者立体交的情况下,矩形并可以求的是多个矩形任意重叠后整个图形的面积 or 重叠了两次(多次)的区域面积 or 整个图形的周长 ……,立体交常见的是求多个长方体任意重叠后整个立体的体积 ……
如果使用普通其他的做法效率低下,使用线段树的话性能就十分优秀了!
二、基本思路
水平有限,这里只讨论平面矩阵交的情况:
如图所示的两个矩阵重叠在一起,假设我们现在需要求这个图形的总面积,不要想着求出三个矩阵的面积,然后两个大的减去重叠的矩阵面积,这种做法在两个矩阵的时候尚且可以接受,但是一旦数量变大,这样就极其麻烦。
换个想法,这个图形的面积可以由这三个部分组成:
这样子看的话,我们只需要从下面往上将三个矩形的面积相加就可以了!
那么如何让计算机替我们完成这样一件事呢,那就是线段树+扫描线啦!
我们需要保存这①-④四条线段的左右端点以及高度,即 left、right 以及 h 三个参数。
从下往上扫描,遇到线段①,记录线段的长度 len = x3 - x1,
继续,遇到线段②,面积 S += len * (y2-y1),记录线段的长度 len = x4 - x1,
遇到线段③,面积 S += len * (y3-y2),长度 len = x4 - x2,
遇到线段④,面积 S += len * (y4-y3),长度 len = 0,
结束,面积 S 就是矩形并的面积。
下面从实现的角度开始分析:
首先我们需要定义线段的结构保存每条线,
struct Edge {
double left, right;
double h;
int flag;
bool operator < (Edge other) const {
return h < other.h;
}
}edges[2*maxn];
为了正确计算当前总共线段的长度 len,因此需要将线段分成入边以及出边,用flag来区分。
而重载 < 比较字符,是为了后续的 sort 排序使用。
接下来我们还需要将线段涉及到的 x 坐标进行离散化并且记录,如果不清楚离散化的话,可以参考我博客中线段树专题的 “线段树之离散化”。
然后我们对所有的线段以及离散过后的 x 坐标数组进行 sort 排序并 unique 去重。
这样前期工作就做完了,我们就可以对开始建树了,先是结点的结构:
struct Node {
int left, int right;
int cnt;
double len;
}tree[4*maxn]
其中 cnt 用来保存区间 [left, right] 这条线段被覆盖的次数,在扫描遇到入边的时候,则 cnt++,遇到出边则 cnt–,因为总是先扫描到入边再扫描到出边,因此可以保证 cnt 总是 >= 0 的;
len 用来保存区间 [left, right] 这条线段中需要被记录的线段长度,故当前总共线段的长度 = tree[0].len,很方便,当扫描到第 i 条 (i > 0) 线段的时候,面积 S += len * (edges[i]-edges[i-1]);
三、例题
Atlantis HDU - 1542
这道题目常常被用来当做矩阵并的模板。
题目大意:给定 n 个任意重叠的矩形地图(但地图的横边竖边分别平行于 x 轴与 y 轴),求重叠后整体的面积。
input:多组输入,第一行为 n,若 n = 0 则不处理结束程序,接下来 n 行每行都包含矩形地图的左上角坐标与右下角坐标。
output:按照 sample output 这样的形式输出整体的总面积,每组输入后要接一行空行。
sample input:
2
10 10 20 20
15 15 25 25.5
0
sample output:
Test case #1
Total explored area: 180.00
线段树扫描线的裸题,正是要求矩形并的面积,注意数据是 double 类型的,这样就更加表明了对数据进行离散化的必要性,还要注意每组输出之后都要接一行空行。
参考代码:
// 线段树 扫描线 模板
#include<stdio.h>
#include<string>
#include<algorithm>
#include<iostream>
#define LL long long
using namespace std;
const int maxn =210;
LL N;
double x[4*maxn];
struct Edge {
double left, right;
double h;
int flag; // 判断是入边还是出边
bool operator < (Edge other) const {
return h < other.h;
}
} edges[4*maxn];
struct Node {
LL left, right;
LL cnt;
double len;
} tree[4*maxn];
// 二分查找 val 离散化后的值
LL findPos(LL l, LL r, double val) {
LL mid;
while(l <= r) {
mid = (l+r)/2;
if(x[mid] > val) r = mid-1;
else if(x[mid] < val) l = mid+1;
else break;
}
return mid;
}
void build(LL k, LL l, LL r) {
tree[k].left = l;
tree[k].right = r;
tree[k].len = 0;
tree[k].cnt = 0;
if(l == r) return ;
LL mid = (l+r)/2;
build(2*k, l, mid);
build(2*k+1, mid+1, r);
}
void pushUp(LL k) {
if(tree[k].cnt)//非0,整段覆盖
tree[k].len = x[tree[k].right+1]-x[tree[k].left];
else if(tree[k].left == tree[k].right)//叶子
tree[k].len = 0;
else//部分覆盖
tree[k].len = tree[2*k].len + tree[2*k+1].len;
}
void update(LL k, LL l, LL r, LL val) {
if(l <= tree[k].left && tree[k].right <= r) { //全部包含
tree[k].cnt += val;
pushUp(k);
return ;
}
LL mid = (tree[k].left + tree[k].right)/2;
if(l <= mid)
update(2*k, l, r, val);
if(r > mid)
update(2*k+1, l, r, val);
pushUp(k);//计算该区间被覆盖的总长度
}
int main() {
LL K = 0;
LL l, r;
double x1, x2, y1, y2;
while(~scanf("%d", &N), N) {
LL cnt = 0;
for(LL i = 1; i <= N; i++) {
scanf("%lf%lf%lf%lf", &x1, &y1, &x2, &y2);
x[++cnt] = x1;
edges[cnt].left = x1;
edges[cnt].right = x2;
edges[cnt].h = y1;
edges[cnt].flag = 1;//下边
x[++cnt] = x2;
edges[cnt].left = x1;
edges[cnt].right = x2;
edges[cnt].h = y2;
edges[cnt].flag = -1;//上边
}
sort(x+1, x+cnt+1);//排序
sort(edges+1, edges+cnt+1);
// 这里没有去重操作 可以加上 unique
build(1, 1, cnt);
double ans = 0;
for(LL i = 1; i <= cnt; i++) { //拿出每条横线并且更新
l = findPos(1, cnt, edges[i].left);
r = findPos(1, cnt, edges[i].right)-1;
update(1, l, r, edges[i].flag);
ans += tree[1].len*(edges[i+1].h-edges[i].h);//求面积
}
printf("Test case #%d\nTotal explored area: %.2f\n\n", ++K, ans);
}
return 0;
}
【END】感谢观看!