前置知识:
1.离散化
2.线段树
解决的问题:
平面内多个矩形面积、周长并或重合面积等问题。
本文先采用洛谷模板题为例
算法思想:
首先来看这张图(以模板题样例为例)
扫描线的核心思想,其实就是用一根竖直(或水平)的线水平(或纵向)扫过整个图。为什么可以这样做呢?结合这张图,我们不难发现,若用一根竖直的线水平扫过图形,有图形覆盖的线的长度只会在标红的线(即纵向线)处产生变化。又因为,两条像这样标红的纵线之间的部分均可以看成矩形,所以我们可以用纵线将整个图形分成若干个小矩形,逐个计算相加。
在实现的过程中,因为坐标之间的距离可能很大,所以我们要先进行离散化,并进行去重,再进行计算。
那么有一个重要的问题待解决:如何计算变化的线长?有一个巧妙的做法是:将矩形的左边记为一条权值为1的边,将矩形的右边记为权值为-1的边。维护一个数组 c[ i ],表示在 i 这个区间里有多少个矩形覆盖。扫描线每到达一条新的边,便把这条边的权值加到 c[ i ] 的对应位置上。当要计算线长时,只要看c数组中有几个数不为1就好了。
这时,我们发现 c 数组的操作涉及区间修改和区间查询,所以我们可以将它与线段树联系到一起,在 c 上建立线段树。
上图中,1、2、3、4为y坐标离散后的值,线1-线2之间的部分为c[1],以此类推。黑色的纵边为正权,红色的为负权。
代码:
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int MAXN=1000008;
ll ans;
int n,k,lx;
int raw[MAXN];
struct ST{
ll A[MAXN];
struct node{
ll l,r;
ll val,len;
}tree[MAXN*4];
void build(int u,int l,int r){
tree[u].l=l,tree[u].r=r,tree[u].len=0,tree[u].val=0;
if(l==r) return;
int mid=(l+r)/2;
build(u*2,l,mid);
build(u*2+1,mid+1,r);
return;
}
void push_up(int x){
int l=tree[x].l,r=tree[x].r;
if(tree[x].val) tree[x].len=raw[r+1]-raw[l];
else tree[x].len=tree[x*2].len+tree[x*2+1].len;
}
void Add(int u,ll l,ll r,ll add){
int pl=tree[u].l,pr=tree[u].r;
if(l<=pl&&r>=pr){tree[u].val+=add;push_up(u);return;}
int mid=(pl+pr)/2;
if(l<=mid) Add(u*2,l,r,add);
if(r>=mid+1) Add(u*2+1,l,r,add);
push_up(u);
}
}c;
struct Line{
ll x,y1,y2;
int o;
int operator <(Line b)const{
return x<b.x;
}
};
Line Lines[MAXN];
int main()
{
cin>>n;
for(int i=1;i<=n;i++){
ll x1,y1,x2,y2;
cin>>x1>>y1>>x2>>y2;
raw[i*2-1]=y1,raw[i*2]=y2;
Lines[i*2-1]={x1,y1,y2,1};
Lines[i*2]={x2,y1,y2,-1};
}
n*=2;//每个矩形都有建2条边
sort(raw+1,raw+1+n);
sort(Lines+1,Lines+n+1);//排序
k=unique(raw+1,raw+1+n)-raw-1;//去重
c.build(1,1,k);
for(int i=1;i<=n;i++){
Lines[i].y1=lower_bound(raw+1,raw+k+1,Lines[i].y1)-raw;
Lines[i].y2=lower_bound(raw+1,raw+k+1,Lines[i].y2)-raw;
} //离散化,此时的y1、y2已经变为离散后的值
for(int i=1;i<=n;i++){
Line nowline=Lines[i];
if(lx) ans+=(nowline.x-lx)*c.tree[1].len;//长乘宽
c.Add(1,nowline.y1,nowline.y2-1,nowline.o);
lx=nowline.x;
}
cout<<ans<<endl;
return 0;
}
例题一:窗口的星星
题意分析
给定n个点(x,y),和它们各自的权值 L ,现在用一个长 W 高 H 的矩形来框这几个点(矩形边缘不算),问这个矩形可以框到的点权和最大为多少。
思考一下后可以想到,对于一个点(x,y),当矩形的右上角在(x,y)~(x+W,y+H)时,该点可以被框入。但我们注意到边框不算,而且矩形右上角坐标可以是小数,我们写程序时显然只能选择整数。有一个操作可以解决这两个问题:将每个点的坐标向左下角移动0.5,这样矩形右上角坐标最大值就变为了(x+L-1,y+H-1),易证得该操作对答案没有影响。所以,一个点(x,y,L)事实上可以转化为一个左下角为(x,y)右上角为(x+W-1,y+H-1),权值为L的矩形,我们要求的答案就是这n个矩形重叠权值的最大值。(如下图)
代码实现
我们按照扫描线的思路,将每个矩形分成左边与右边。不过,边的权值应定为 L 与 -L 。这样之后,我们维护 c 数组中的最大值,每个位置上c数组的最大值中的最大值即为答案。
!注意:因为重边部分需要先加再减,所以给边排序时还要按照边权排一遍。
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int MAXN=100008;
ll ans;
int n,k,lx,W,H,T;
int raw[MAXN];
struct ST{
struct node{
ll l,r;
ll val,lazy;
}tree[MAXN*4];
void build(int u,int l,int r){
tree[u].l=l,tree[u].r=r,tree[u].val=0;
if(l==r) return;
int mid=(l+r)/2;
build(u*2,l,mid);
build(u*2+1,mid+1,r);
return;
}
void Spread(int u){
tree[u*2].val+=tree[u].lazy;
tree[u*2+1].val+=tree[u].lazy;
tree[u*2].lazy+=tree[u].lazy;
tree[u*2+1].lazy+=tree[u].lazy;
tree[u].lazy=0;
}
void Add(int u,ll l,ll r,ll add){
int pl=tree[u].l,pr=tree[u].r;
if(l<=pl&&r>=pr){
tree[u].val+=add;
tree[u].lazy+=add;
return;
}
int mid=(pl+pr)/2;
Spread(u);
if(l<=mid) Add(u*2,l,r,add);
if(r>=mid+1) Add(u*2+1,l,r,add);
tree[u].val=max(tree[u*2].val,tree[u*2+1].val);
}
}c;
struct Line{
ll x,y1,y2,o;
int operator <(Line b)const{
if(x!=b.x)
return x<b.x;
else return o>b.o;//注意这里
}
};
Line Lines[MAXN];
int main()
{
cin>>T;
for(int o=1;o<=T;o++){
cin>>n>>W>>H;
lx=0;memset(raw,0,sizeof(raw));memset(c.tree,0,sizeof(c.tree));
for(int i=1;i<=n;i++){
ll x,y,l;
cin>>x>>y>>l;
raw[i*2-1]=y,raw[i*2]=y+H-1;
Lines[i*2-1]={x,y,y+H-1,l};
Lines[i*2]={x+W-1,y,y+H-1,(-1)*l};
}
n*=2;
sort(raw+1,raw+1+n);
sort(Lines+1,Lines+n+1);
k=unique(raw+1,raw+1+n)-raw-1;
c.build(1,1,k);
for(int i=1;i<=n;i++){
Lines[i].y1=lower_bound(raw+1,raw+k+1,Lines[i].y1)-raw;
Lines[i].y2=lower_bound(raw+1,raw+k+1,Lines[i].y2)-raw;
}
for(int i=1;i<=n;i++){
Line nowline=Lines[i];
c.Add(1,nowline.y1,nowline.y2,nowline.o);
ans=max(ans,c.tree[1].val);
}
cout<<ans<<endl;
ans=0;
}
return 0;
}
例题二:矩形周长
这题中,矩形面积改为了矩形周长。不过,扫描线的基本思想还是适用的。只不过要维护的东西多了点。竖着的边不可怕,它们的长度在我们之前维护len的时候就算出来了。但是横向边尤其让人头疼,它的长度是(扫描线上不相邻的线段条数*2)*(x的跨度)。接下来就来介绍这(扫描线上不相邻的线段条数)如何维护。
我们给线段树上每个节点再带三个元素:num(扫描线上不相邻的线段条数),lflag(左端点val是否>0),rflag(右端点val是否>0)。我用一段代码具体解释它们的作用和维护方法。
void push_up(int x){
int l=tree[x].l,r=tree[x].r;
if(tree[x].val){//如果这个节点被完全覆盖
tree[x].num=1;//那么它整个只有一段线段
tree[x].lf=tree[x].rf=1;//左右端点都有被覆盖
tree[x].len=raw[r+1]-raw[l];
}
else if(l==r) tree[x].len=tree[x].num=tree[x].lf=tree[x].rf=0;//如果这是一个叶节点且未被覆盖,则全设为0
else{//普通节点维护
tree[x].len=tree[x*2].len+tree[x*2+1].len;
tree[x].num=tree[x*2].num+tree[x*2+1].num;//先把左右加起来
if(tree[x*2+1].lf&&tree[x*2].rf) tree[x].num--;//如果左儿子的右端点和右儿子的左端点都被覆盖,则说明这两条线段可以连起来变成一整条线段,二变一,线段数-1
tree[x].lf=tree[x*2].lf;
tree[x].rf=tree[x*2+1].rf;
}
}
有了这个push_up函数,就可以很方便地维护线段树了。
最后的问题就是答案的统计了。推算可得,从一个x1到另一个x2,纵边增加的长度是两次树根的len的差的绝对值,横边增加的长度是x1时的num*2*(x2-x1)。
代码
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int MAXN=1000008;
ll ans;
int n,k,lx=INT_MIN,l;
int raw[MAXN];
struct ST{
ll A[MAXN];
struct node{
ll l,r;
ll val,len,num;
bool lf,rf;
}tree[MAXN*4];
void build(int u,int l,int r){
tree[u].l=l,tree[u].r=r,tree[u].len=0,tree[u].val=0;
if(l==r) return;
int mid=(l+r)/2;
build(u*2,l,mid);
build(u*2+1,mid+1,r);
return;
}
void push_up(int x){
int l=tree[x].l,r=tree[x].r;
if(tree[x].val){
tree[x].num=1;
tree[x].lf=tree[x].rf=1;
tree[x].len=raw[r+1]-raw[l];
}
else if(l==r) tree[x].len=tree[x].num=tree[x].lf=tree[x].rf=0;
else{
tree[x].len=tree[x*2].len+tree[x*2+1].len;
tree[x].num=tree[x*2].num+tree[x*2+1].num;
if(tree[x*2+1].lf&&tree[x*2].rf) tree[x].num--;
tree[x].lf=tree[x*2].lf;
tree[x].rf=tree[x*2+1].rf;
}
}
void Add(int u,ll l,ll r,ll add){
int pl=tree[u].l,pr=tree[u].r;
if(l<=pl&&r>=pr){tree[u].val+=add;push_up(u);return;}
int mid=(pl+pr)/2;
if(l<=mid) Add(u*2,l,r,add);
if(r>=mid+1) Add(u*2+1,l,r,add);
push_up(u);
}
}c;
struct Line{
ll x,y1,y2;
int o;
int operator <(Line b)const{
if(x!=b.x)
return x<b.x;
else return o>b.o;
}
};
Line Lines[MAXN];
int main()
{
cin>>n;
for(int i=1;i<=n;i++){
ll x1,y1,x2,y2;
cin>>x1>>y1>>x2>>y2;
raw[i*2-1]=y1,raw[i*2]=y2;
Lines[i*2-1]={x1,y1,y2,1};
Lines[i*2]={x2,y1,y2,-1};
}
n*=2;
sort(raw+1,raw+1+n);
sort(Lines+1,Lines+n+1);
k=unique(raw+1,raw+1+n)-raw-1;
c.build(1,1,k);
for(int i=1;i<=n;i++){
Lines[i].y1=lower_bound(raw+1,raw+k+1,Lines[i].y1)-raw;
Lines[i].y2=lower_bound(raw+1,raw+k+1,Lines[i].y2)-raw;
}
for(int i=1;i<=n;i++){
Line nowline=Lines[i];
int num=c.tree[1].num;
c.Add(1,nowline.y1,nowline.y2-1,nowline.o);
ans+=abs(c.tree[1].len-l);
if(lx!=INT_MIN) ans+=(nowline.x-lx)*num*2;
l=c.tree[1].len;
lx=nowline.x;
}
cout<<ans<<endl;
return 0;
}