扫描线专题
Zskioaert1106 · · 算法·理论
前置知识:离散化
离散化是一种缩减数据值域的办法。
对于一组数据
具体操作方法:先将原数组排序后去重,新数组中元素的下标就是原数组的映射。
我们可以使用 algorithm 头文件中的相关函数来方便快捷地离散化:
sort(c+1,c+1+n); // 排序,c 是需要离散化的数组,但通常我们会将其复制一份
int tot=unique(c+1,c+1+n)-(c+1); // 去重,tot 就是离散化后的值域了
题目引入:矩形面积并
P5490:给出
n 个边平行于坐标轴的矩形的位置,求它们覆盖的总面积。n \le 10^5,x,y \le 10^9 。
第一步肯定是将坐标离散化,从而将值域缩减到 bool 数组,统计最终有多少面积被覆盖过。这类实现中,最优的时空复杂度为
算法介绍
扫描线是一种算法策略,其大概的思想就是处理高于一维的空间问题时,遍历其中的一维,用数据结构等对其它维进行维护。其典型应用之一就是二维正交范围问题,也就是上面的矩形面积并。
B 维正交范围指在一个 B 维直角坐标系下,第
i 维坐标在一个整数范围[l_i,r_i] 间,内部的点集。——OI-Wiki通常来讲,一维正交范围称为区间,二维正交范围称为矩形,三维正交范围称为立方体。
对于本题,我们想象有一条直线
例题解决:矩形面积并
我们把每个矩形拆成两条线段,一条是矩形的下边,将它赋权为
又因为纵坐标相邻的两条线段之间,扫描线的总覆盖长度一定不会有任何变化,所以我们的扫描线只需要遍历所有矩形的上下边纵坐标
接下来考虑如何快速维护扫描线上被覆盖的位置。发现遍历到每条线段都形如区间加/减,于是自然想到线段树。同理,同一时刻横坐标相邻的位置之间要么全被覆盖,要么全无,所以建树也可以用离散化后的坐标。
线段树能处理的信息要具有可合并性,现在有一个困难:线段树要维护每个位置具体被覆盖了几层(否则删边时不好处理),但每个被覆盖的位置最终都只会贡献
遇到这种问题,我们就要想办法把它转化成可合并的信息。介绍一种简单的思路:我们可以用线段树维护当前区间内被覆盖的最小层数
加入线段时,使用线段树维护
算法实现:矩形面积并
首先是建线段树。这里维护的是区间,所以线段树的元线段(叶子结点)就可以取离散化后相邻的两个横坐标之间的区间。注意我们之前写线段树通常是维护序列上的数值,元线段取的是单个元素,可以视为点,所以此处以线段建立的线段树要相应地进行一些修改。
我采用的方法是:建树时用离散化后的下标,在 build 函数里处理好这段区间的真实长度和左右端点,之后的操作中就可以直接用原坐标了,更加方便。
:::info[建树的代码(行高亮部分是与序列建树不同的)]{open}
struct Segment_tree{
int L,R,len;
int s1,s2;
int tag;
}s[N*8]; // 这里的 8 是 2n 条线段乘 4 倍的线段树
void build(int u,int l,int r){
s[u].L=x[l],s[u].R=x[r]; // 此处线段树的叶子为一段线段而非一个点
s[u].len=x[r]-x[l];
if(r-l>1){
int mid=(l+r)/2;
build(u*2,l,mid),build(u*2+1,mid,r);//相邻的两个子区间
}
s[u].s2=s[u].len; // 最初覆盖层数全是 0
}
:::
:::info[线段树修改]{open}
不难发现区间在整体修改时其
void update(int u,int l,int r,short p){
if(l<=s[u].L&&s[u].R<=r){
s[u].s1+=p;
s[u].tag+=p;
}
else if(l<s[u].R&&s[u].L<r){ // 此处判断区间相交不能取等
pushdown(u);
update(u*2,l,r,p),update(u*2+1,l,r,p);
pushup(u);
}
}
:::
:::info[线段树查询]{open}
此处由于只会查询整个扫描线,就没必要专门写线段树的查询函数了。
if(s[1].s1==0) return s[1].len-s[1].s2;
else return s[1].len;
:::
pushup 和 pushdown 与普通线段树维护无异。接下来就是在主函数中按纵坐标遍历每条线段,当下一条线段的纵坐标不等于当前线段时,就说明一个时刻的扫描线更新完毕了,此时更新答案,即扫描线覆盖的长度乘上下一条线段与当前的纵坐标之差。
:::info[整体代码]
int n,x[N*2],tot;
struct line{
int x1,x2,y;
short p;
}a[N*2];
bool cmp(line a,line b){
if(a.y!=b.y)return a.y<b.y;
return a.p==1;
}
long long ans;
struct Segment_tree{
int L,R,len;
int s1,s2;
int tag;
}s[N*8];
void pushup(int u){
if(s[u*2].s1==s[u*2+1].s1){
s[u].s1=s[u*2].s1;
s[u].s2=s[u*2].s2+s[u*2+1].s2;
}
else if(s[u*2].s1<s[u*2+1].s1){
s[u].s1=s[u*2].s1;
s[u].s2=s[u*2].s2;
}
else{
s[u].s1=s[u*2+1].s1;
s[u].s2=s[u*2+1].s2;
}
}
void build(int u,int l,int r){
s[u].L=x[l],s[u].R=x[r]; // 此处线段树的叶子为一段线段而非一个点
s[u].len=x[r]-x[l];
if(r-l>1){
int mid=(l+r)/2;
build(u*2,l,mid),build(u*2+1,mid,r);
}
s[u].s2=s[u].len; // 最初覆盖层数全是 0
}
void pushdown(int u){
if(s[u].tag){
s[u*2].s1+=s[u].tag,s[u*2+1].s1+=s[u].tag;
s[u*2].tag+=s[u].tag,s[u*2+1].tag+=s[u].tag;
s[u].tag=0;
}
}
void update(int u,int l,int r,short p){
if(l<=s[u].L&&s[u].R<=r){
s[u].s1+=p;
s[u].tag+=p;
}
else if(l<s[u].R&&s[u].L<r){ // 此处判断区间相交不能取等
pushdown(u);
update(u*2,l,r,p),update(u*2+1,l,r,p);
pushup(u);
}
}
long long query(int u){ // 此处由于只会查询整个扫描线,就没必要专门写 query 函数了
if(s[u].s1==0)return s[u].len-s[u].s2;
return s[u].len;
}
int main(){
cin>>n;
for(int i=1,x1,x2,y1,y2;i<=n;i++){
cin>>x1>>y1>>x2>>y2;
x[++tot]=x1,x[++tot]=x2;
a[i*2-1].x1=x1,a[i*2-1].x2=x2,a[i*2-1].y=y1,a[i*2-1].p=1;
a[i*2].x1=x1,a[i*2].x2=x2,a[i*2].y=y2,a[i*2].p=-1;
}
sort(x+1,x+1+tot);
tot=unique(x+1,x+1+tot)-x-1;
build(1,1,tot);
n*=2;
sort(a+1,a+1+n,cmp);
for(int i=1;i<=n;i++){
update(1,a[i].x1,a[i].x2,a[i].p);
if(a[i].y<a[i+1].y){
ans+=query(1)*(a[i+1].y-a[i].y);
}
}
cout<<ans;
return 0;
}
:::
AC 记录。
常用技巧:点与矩形的相互转化
P1502:有
n 个点,每个点有坐标x,y 和权值l 。求长宽为W,H 的矩形能覆盖的点权之和最大值。Tn \le 10^5 。
:::info[解法]
考虑矩形放在哪里时一个点会被覆盖住,若矩形的左下角在
因此把
这个题告诉我们:在遇到点覆盖求区域的问题时,可以转化成矩形覆盖求单点的问题,从而更方便应用线段树等工具求解。
常用技巧:序列转二维正交
UVA1608:对于序列
a_1\sim a_n ,如果对于所有1\leqslant l\leqslant r\leqslant n ,都有在连续子序列a_l \sim a_r 中只出现1 次的元素,则称这个序列是“不无聊的”。判断一些序列是否是“不无聊的”。
:::info[解法]
考虑每个位置上的元素能对哪些
序列
这个题告诉我们,有些看起来是序列问题的题目,可以通过“数形结合”思想的转化,用扫描线解决问题。
经典应用:二维数点
P10814:给定一个长度为
n 的序列a ,有m 次询问,每次查询a_l \sim a_r 之间小于等于y 的元素个数。n,m,a_i \le 2\times 10^6 。
将
求逆序对的数量就可以用二维数点解决,问题可以转化为从后往前遍历序列,对于每个
更多练习题目
洛谷题单,不定时更新扫描线入门题。