本篇博客在上一篇博客基础上讲解,链接:线段树
引例
来看一道题目:Atlantis
这道题的题意是给定若干个平行于坐标轴的矩形,求出这若干个矩形的面积之和,如果有重合的部分,只算一次。
首先考虑暴力的算法,即遍历每个矩形,用一个vis数组记录每个点的访问状态,最后可以统计出总的面积,这种方法在数据小且边长为整数的时候适用。
这类问题应该引入扫描线来做,下面来讲讲扫描线的概念。
扫描线在并行面积并中的应用
首先得定义扫描线 ,扫描线就是一条设想的线,它可以是从水平方向上下扫描,也可以从竖直方向左右扫描,对所有的矩形进行遍历,计算面积,两个方向的效果都是一样的,本文下面所讲都是前者的扫描方法。
如下图1.1所示,数字1所标识的是两个矩形,数字2所标识的是经过扫描线的扫描,将所有的矩形分成若干个部分来分别计算。虚线就代表扫描线,蓝色箭头方向表示扫描的方向,我们可以发现,矩形被分割成几个部分,是取决于矩形的上下边的 ,每遇到一条上边或者下边,就分割出了一个部分。
![](https://i-blog.csdnimg.cn/blog_migrate/fa193b97295cd47c9110dea0eb756aec.jpeg)
明白了扫描线的基本概念之后,我们再来讨论,再将所有矩形划分成若干个部分后,到底应该如何计算每个部分的面积。
显然如果我们将扫描线以从下向上的方式来进行扫描(如上图),那么我们需要记录每条水平的边,这样以扫描线的视角来扫描每条水平边的时候,不难想到 ,如果我们记录了每条水平边的长度和竖直方向的高之后,我们可以很容易的计算每个部分的高了。
![](https://i-blog.csdnimg.cn/blog_migrate/86d458439ed7ba01c13edbe3376f2850.png)
如上图1.2所示,如果我们记录了红色部分的下边H= 10, 上边 H= 20,那么我们将扫描线从1扫描到2的时候,显然红色部分的高可以由两者之差轻易取得。
现在关键在于扫描每个部分之后,我们应该如何求出该部分的长度,这里就用线段树来维护每个部分的长度,下面讲讲如何实现。
为了实现每次都能动态的得到每个部分的长度,我们需要将每个矩形的下边记为1,上边记为-1。因为我们实际上扫描到一个矩形的下边的时候,我们已经计算了这个部分的面积,所以再次扫描到这个矩形的上边的时候,我们只需要撤销这个部分的长度即可。而记录或者撤销某个部分的长度,在线段树中的操作就是统计那个区间的长度,我们将某个区间的每个点加1,就是记录了该区间的长度,而将某个区间的每个点减1,就是撤销了该区间的长度。
此外,我们还得注意, 由于矩形的长度可以很大,而且可以为浮点数,因此我们需要离散化长度的数据,这样才能更便于线段树的构造。
所谓的离散化,就是将一些数据做一下映射(个人理解),使之变得易于处理 。
如下图1.3所示,我们可以看到矩形的长度有浮点数,所以我们可以将坐标10映射为下标1,坐标15映射为下标2,坐标20映射为下标3,坐标25.5映射为下标4,然后将所有坐标记录在一个数组中,这样我们可以只用下标构造线段树,需要用到实际的值的时候,用下标在数组中查询即可。
![](https://i-blog.csdnimg.cn/blog_migrate/a85cc620486cbe8112590f0d26bac4d3.png)
完整代码
贴出引例中的完整代码 ,这里需要注意我们在求每条水平边左右点的low_bound的时候,边的右点必须先减1,最后在线段树中再加回1。用上图1.3来说明, 我们扫描第一条水平边的时候,如果用区间[1,3]记录这条边的长度,那么显然在线段树中它会被变成[1,2]和[3,3]来分别记录,但[3,3]会被记录成0,因此最后边记录就会出现问题。如果我们开始减 1,用区间[1,2]来记录,就不会出现这样的问题
#include <iostream>
#include <cstring>
#include <cstdlib>
#include <stdio.h>
#include <string.h>
#include <algorithm>
#define maxn 1000010
using namespace std;
double x[maxn<<4];
struct no{
double l,r;
double h;
int f;
}s[maxn<<4];
struct node{
int l,r;
int add;
double sum;
}T[maxn<<4];
bool cmp(no &n1, no &n2)
{
return n1.h < n2.h;
}
void build(int left, int right, int num)
{
T[num].l = left;
T[num].r = right;
T[num].add = T[num].sum = 0;
if(left == right) return;
int mid = (left+right) / 2;
build(left, mid, 2*num);
build(mid+1, right, 2*num+1);
}
void pushup(int num)
{
if(T[num].add)
T[num].sum = x[T[num].r+1] - x[T[num].l];
else if(T[num].l == T[num].r)
T[num].sum = 0;
else
T[num].sum = T[2*num].sum + T[2*num+1].sum;
}
void update(int x, int y, int c, int num)
{
if(x == T[num].l && T[num].r == y)
{
T[num].add += c;
pushup(num);
return;
}
int mid = (T[num].l + T[num].r)/2;
if(x > mid) update(x, y, c, 2*num+1);
else if(y <= mid) update(x, y, c, 2*num);
else
{
update(x, mid, c, 2*num);
update(mid+1, y, c, 2*num+1);
}
pushup(num);
}
int main()
{
int Count = 1;
int m;
int n;
double x1, y1, x2, y2;
while(~scanf("%d", &n) && n)
{
double ans = 0;
int m = 0;
for(int i = 1; i <= n; i++)
{
scanf("%lf%lf%lf%lf",&x1, &y1, &x2, &y2);
x[++m] = x1;
s[m] = (no){x1,x2,y1,1};
x[++m] = x2;
s[m] = (no){x1,x2,y2,-1};
}
sort(x+1, x+m+1); // 横坐标排序
sort(s+1, s+m+1, cmp); // 上下边排序 1表示下边
int k = unique(x+1, x+m+1) - x; // 返回不同元素的后一个地址值
build(1, k-1, 1);
for(int i = 1; i < m; i++)
{
int l = lower_bound(x+1, x+k, s[i].l) - x; //返回大于或等于val的第一个元素位置
int r = lower_bound(x+1, x+k, s[i].r) - x - 1;
update(l, r, s[i].f, 1);
ans += T[1].sum * (s[i+1].h - s[i].h);
}
printf("Test case #%d\nTotal explored area: %.2f\n\n", Count++, ans);
}
return 0;
}