扫描线是一种求矩形面积并/周长并的方法。
下面由一道经典题目来引出扫描线—:
求
n
n
n 个四边平行于坐标轴的矩形的面积并,如图:
对于这种由矩形构成的图形,我们可以在
y
y
y 轴上添加一条扫描线,这样面积就等于
∑
\sum
∑ 与图形相交的长度(
d
l
d l
dl)
×
×
× 扫过的高度(
d
h
dh
dh) ,有种微积分的思想,效果如图:
那么问题就是实现这样的效果是十分困难,因为并不能很好的确定这条扫描线每次具体移动多远,但是,我们观察可以知道,并不是需要边扫边计算,只需要当扫描线遇到矩形的底边或顶边时,进行计算这段过程扫过的面积即可。即原图形被划分成:
其中,将这个图形进行分割的四条线我们就称为分割线,为了快速计算出截线段长度,可以将横边赋上不同的权值,具体为:对于一个矩形,其下边权值为1,上边权值为−1。将所有这样的分割线按照y轴坐标从小到大排序,这样扫描线进行扫描的时候,总会先碰到一个矩形的下边,再碰到一个矩形的上边,那么就能保证扫描线所截的长度永远非负了。
定义切割线结构体如下图:
struct line //切割线
{
int h,l,r;
int tag; //出边+1,入边-1
bool operator<(const line &t)const
{
return h < t.h ;
}
}L[N*2];
l 、 r l、r l、r 是扫描线和矩形相交的左右端点, h h h 是扫描线的y轴坐标, t a g tag tag 表示这条扫描线是矩形的下面的一条边还是上面的一条边,由于题中会告诉我们矩形的左下角坐标和右上角坐标,因此很容易得到两条切割线,{ y 1 , x 1 , x 2 , 1 y1,x1,x2,1 y1,x1,x2,1}、{ y 2 , x 1 , x 2 , − 1 y2,x1,x2,-1 y2,x1,x2,−1}。
然后将它们的端点在
x
x
x 轴上进行投影,整个问题就转化为了一个区间查询问题,用线段树进行维护,即:每次遇到一条切割线的时候,需要对线段树进行区间修改操作、查询根节点的覆盖长度计算面积。
我们已经知道,这棵线段树的每个节点都对应了一条线段。但是,仔细观察会发现,线段树维护区间和线段的区间有区别:
如果我们用线段树节点对应的区间来表示所对应的线段,那么会出现几个问题:
- 对于叶子节点, t [ u ] . l = = t [ u ] . r t[u].l==t[u].r t[u].l==t[u].r,对应一个点,而非一个线段;
- 对于非叶子节点左右儿子的区间不会重合(交集为空),但是看这样两条相邻的两条线段 [ 3 , 6 ] 、 [ 6 , 9 ] [3,6]、[6,9] [3,6]、[6,9],它们是会出现相交的部分的,即一个6号点,而我们并不能简单的把这个点划分到左边或者右边。
因此,我们需要改变所对应的关系,将节点 x x x 所对应区间仍然为 t [ u ] . l 、 t [ u ] . r t[u].l、t[u].r t[u].l、t[u].r,但对应的线段为 t [ u ] . l 、 t [ u ] . r + 1 t[u].l、t[u].r+1 t[u].l、t[u].r+1,这样的话我们就很完美的解决了上面的两个问题。
注意,只是改变所有节点所对应的线段,并未改变节点所对应的区间。
当我们进行修改操作的时候,也要进行改变,如果我们要修改线段 [ 1 、 3 ] [1、3] [1、3],那么对应线段树所要修改的节点区间应该为 [ 1 、 2 ] [1、2] [1、2]。(线段树节点的区间对应线段为 t [ u ] . l 、 t [ u ] . r + 1 t[u].l 、t[u].r+1 t[u].l、t[u].r+1)
线段树进行区间修改需要用到懒标记,查询的时候需要知道区间被覆盖的长度,所以,线段树节点存储信息如下:
struct tree
{
int l,r;
int tag; //区间覆盖次数
ll len; //区间覆盖长度
}t[N*4];
p u s h u p pushup pushup 函数:
- 节点 t a g > 0 tag > 0 tag>0 时,表明该节点所对应的线段全部被覆盖, 就用它自身的信息更新
- 节点 t a g = 0 tag =0 tag=0 时,用左右孩子的信息更新区间覆盖长度。
void pushup(int u)
{
int l = t[u].l , r = t[u].r;
if(t[u].tag) //区间被覆盖过 直接更新不需要孩子的信息
t[u].len = r+1 - l;
else //没被覆盖过,利用孩子更新区间信息
t[u].len = t[ls].len + t[rs].len;
}
仔细观察可以发现,对区间 [ l 、 r ] [l、r] [l、r] 赋值的操作会进行两次,一次是 + 1 +1 +1、一次是 − 1 -1 −1,那我们就不需要下传懒标记。懒标记需要下传时对应的条件是:
- 进行区间修改:当节点有懒标记,如果需要对其孩子进行修改时,不下传懒标记,它孩子的信息肯定是错误的(因为父节点的标记没有下传,可能会少一些区间覆盖),但是,由于该节点 有懒标记 t a g > 0 tag > 0 tag>0,而 t a g > 0 tag > 0 tag>0 是用自身的信息进行更新,并没有用到孩子错误的信息,因此不会产生错误,当该节点的 t a g = 0 tag=0 tag=0 时,该节点用它的孩子进行更新,此时节点没有懒标记,不存在下传信息缺失的情况,因此也不会产生错误。
- 当进行区间询问的时候:一般情况,如果询问的区间是某一个含有懒标记节点的一部分,需要进行下传懒标记,由于我们只需要询问根节点的信息,因此就不需要下传懒标记了。
c h a n g e change change 函数:
void change(int u,int ql,int qr,int k)
{
if(ql<=t[u].l && t[u].r<=qr)
{
t[u].tag += k;
pushup(u); //更新区间
return ;
}
int mid = (t[u].l + t[u].r) / 2;
if(ql<=mid)
change(ls,ql,qr,k);
if(mid+1<=qr)
change(rs,ql,qr,k);
pushup(u);
}
如果,坐标比较大或者坐标是浮点数需要进行离散化处理。
下面给出洛谷的一道题目扫描线的参考代码:
#include <bits/stdc++.h>
using namespace std;
const int N=1e6+10,INF=0x3f3f3f3f;
#define ll long long
struct line //扫描线
{
int h,l,r;
int tag; //出边+1,入边-1
bool operator<(const line &t)const
{
return h < t.h ;
}
}L[N*2];
struct tree
{
int l,r;
int tag; //区间覆盖次数
ll len; //区间覆盖长度
}t[N*4];
int X[N];
int n;
#define ls u<<1
#define rs u<<1|1
void build(int u,int l,int r)
{
t[u] = {l,r,0,0};
if(l==r) return ;
int mid = (l+r)/2;
build(ls,l,mid);
build(rs,mid+1,r);
return ;
}
void pushup(int u)
{
int l = t[u].l , r = t[u].r;
if(t[u].tag) //区间被覆盖过 直接更新不需要孩子的信息
t[u].len = X[r+1] - X[l];
else //没被覆盖过,利用孩子更新区间信息
t[u].len = t[ls].len + t[rs].len;
}
void change(int u,int ql,int qr,int k)
{
if(ql<=t[u].l && t[u].r<=qr)
{
t[u].tag += k;
pushup(u); //更新区间
return ;
}
int mid = (t[u].l + t[u].r) / 2;
if(ql<=mid)
change(ls,ql,qr,k);
if(mid+1<=qr)
change(rs,ql,qr,k);
pushup(u);
}
int main()
{
ios::sync_with_stdio(false) , cin.tie(0);
cin>>n;
for(int i=1; i<=n; i++)
{
int x1,y1,x2,y2;
cin>>x1>>y1>>x2>>y2;
L[2*i-1] = {y1,x1,x2,1} , L[2*i] = {y2,x1,x2,-1};
X[2*i-1] = x1 , X[2*i] = x2 ;
}
sort(L+1,L+2*n+1); //将扫描线排序
sort(X+1,X+2*n+1);
int tot = unique(X+1,X+2*n+1)-X-1; //去重
build(1,1,tot-1); //tot个点 tot-1个空隙
ll s = 0;
for(int i=1; i<2*n; i++)
{
int ql = lower_bound(X+1,X+tot+1,L[i].l)-X;
int qr = lower_bound(X+1,X+tot+1,L[i].r)-X;
change(1,ql,qr-1,L[i].tag);
s += (L[i+1].h-L[i].h)*t[1].len;
}
cout << s << '\n' ;
return 0;
}