扫描线
扫描线的一些经典应用:求n个矩形的面积并和周长并。
- 面积并(P5490 【模板】扫描线)
首先扫描线的思想就是假设有一条无限长度的线从一个方向到另一个方向扫一遍整个图形,这样这个图形就变成了一大堆小矩形,然后算每个矩形的面积。这个过程可以上棵线段树。
怎么搞?首先我们随便找一维(我找的是y轴)离散化一下,防止1e9的数据范围开不下。(事实上虽然这个题的数据范围是1e5,但是你开8e5会RE,最小得开1049000)
然后是具体的实现。我们按照x排序,这样可以顺序按x轴扫描每一条边。我们每条边可以附带一个权值,扫到时为1,结束时为-1。这样我们就可以通过线段树上的区间修改操作来维护一条线,此时我们线段树的区间\([l,r]\)是一条线段(由于一些不可描述的原因,区间\([l,r]\)实际上维护的是线段的\([l,r+1]\)部分)。
接着是一些具体实现方面。每次扫到一条边时:
- 首先更新这条边,插入线段树。
- 统计这条边与下一条边的距离,更新答案。
然后这个线段树需要稍微魔改一下,就是pushup和update部分。
大部分东西都放在注释里了。
#include <cstdio>
#include <algorithm>
#include <iostream>
#define lson rt<<1
#define rson rt<<1|1
#define int long long
using namespace std;
struct node{
int l,r,cnt,len;//cnt为被线段覆盖的次数 len为区间有线覆盖的长度
}tree[1050010];
struct stu{
int x,y1,y2,k;//存储每一条边
bool operator<(const stu &s)const{
return x==s.x?k>s.k:x<s.x;//按照x排序 x相同先取正
}
}edge[200010];
int n,lsh[200010],cnt,maxx,val[200010];
void pushup(int rt){
if(tree[rt].cnt){
tree[rt].len=val[tree[rt].r+1]-val[tree[rt].l];//如果被覆盖过 则更新长度
//注意我们稍微修改了一下映射关系 将右端点加了1 可以防止[1,2][2,3]这样程序看起来不连续的情况
}
else tree[rt].len=tree[lson].len+tree[rson].len;//由于只有我们直接修改的子节点会有cnt 所以其他节点需要从儿子上传
}
void build(int rt,int l,int r){
tree[rt].l=l;tree[rt].r=r;
if(l==r)return;
int mid=(l+r)>>1;
build(lson,l,mid);build(rson,mid+1,r);
}//基本的建树 不多说
void update(int rt,int l,int r,int val){
if(l<=tree[rt].l&&tree[rt].r<=r){
tree[rt].cnt+=val;pushup(rt);//这里一定要pushup一次
//因为如果不pushup直接return的话 没办法更改当前的len值
//要说的话就假设我们这一段中间原本缺了一块 不pushup的话就还是缺一块上传
return;
}
int mid=(tree[rt].l+tree[rt].r)>>1;
if(l<=mid)update(lson,l,r,val);
if(mid<r)update(rson,l,r,val);
pushup(rt);//剩下的就是板子
}
signed main(){
scanf("%lld",&n);
for(int i=1;i<=n;i++){
int x1,y1,x2,y2;
scanf("%lld%lld%lld%lld",&x1,&y1,&x2,&y2);
edge[i*2-1].x=x1;edge[i*2].x=x2;//存储每条边 一条+1一条-1
edge[i*2-1].y1=edge[i*2].y1=y1;
edge[i*2-1].y2=edge[i*2].y2=y2;
edge[i*2-1].k=1;edge[i*2].k=-1;
lsh[++lsh[0]]=y1;lsh[++lsh[0]]=y2;//离散化 实际上x y随便搞一维就可以
}
n*=2;
sort(lsh+1,lsh+lsh[0]+1);
cnt=unique(lsh+1,lsh+lsh[0]+1)-lsh-1;
for(int i=1;i<=n;i++){
int pos=lower_bound(lsh+1,lsh+cnt+1,edge[i].y1)-lsh;
val[pos]=edge[i].y1;//val是一个双射 方便之后回去找线段长度
edge[i].y1=pos;maxx=max(maxx,pos);
pos=lower_bound(lsh+1,lsh+cnt+1,edge[i].y2)-lsh;
val[pos]=edge[i].y2;edge[i].y2=pos;maxx=max(maxx,pos);
}
sort(edge+1,edge+n+1);
build(1,1,n);//建线段树
int ans=0;
for(int i=1;i<n;i++){
update(1,edge[i].y1,edge[i].y2-1,edge[i].k);//更新当前边
ans+=tree[1].len*(edge[i+1].x-edge[i].x);//顺便查询
}
printf("%lld",ans);
return 0;
}
其实和面积并是类似的。只不过多了一点东西。
首先周长的话我们一段区间内可能有不止两条线段,所以我们要记录一段区间内的线段条数来更新答案。
然后我们的扫描线可能增长,周长增加;也可能减小。和面积不同,周长也会增加。所以这里要记录一下。同理,一次要更新所有在同一位置上的边。
大部分东西还是在代码里。
#include <cstdio>
#include <algorithm>
#include <iostream>
#define lson rt<<1
#define rson rt<<1|1
using namespace std;
struct node{
int l,r,cnt,sum,len;//cnt是覆盖几次 len是覆盖长度 num是被几条线段覆盖
bool lflag,rflag;//左右端点是否被覆盖(合并用)
}tree[1000010];
int n,ans,maxx=-2147483647,minn=2147483647;
struct stu{
int x,y1,y2,k;
bool operator<(const stu &s)const{
return x==s.x?k>s.k:x<s.x;//面积并k哪个在前没关系 但是周长必须k大的在前
}
}edge[10010];
void pushup(int rt){
if(tree[rt].cnt){//被覆盖过 和面积相似地更新
tree[rt].sum=1;
tree[rt].len=tree[rt].r-tree[rt].l+1;
tree[rt].lflag=tree[rt].rflag=1;
}
else if(tree[rt].l==tree[rt].r){//叶子节点 即一个端点
tree[rt].sum=0;tree[rt].len=0;
tree[rt].lflag=tree[rt].rflag=0;
}
else{//其他情况直接按普通线段树更新
tree[rt].sum=tree[lson].sum+tree[rson].sum;
if(tree[lson].rflag&&tree[rson].lflag)tree[rt].sum--;//重复算的端点
tree[rt].len=tree[lson].len+tree[rson].len;
tree[rt].lflag=tree[lson].lflag;
tree[rt].rflag=tree[rson].rflag;
}
}
void build(int rt,int l,int r){
tree[rt].l=l;tree[rt].r=r;
if(l==r)return;
int mid=(l+r)>>1;
build(lson,l,mid);build(rson,mid+1,r);
}//还是普通的build
void update(int rt,int l,int r,int val){
if(l<=tree[rt].l&&tree[rt].r<=r){
tree[rt].cnt+=val;
pushup(rt);return;//这里要pushup一次 和之前一样
}
int mid=(tree[rt].l+tree[rt].r)>>1;
if(l<=mid)update(lson,l,r,val);
if(mid<r)update(rson,l,r,val);
pushup(rt);
}
int main(){
scanf("%d",&n);
for(int i=1;i<=n;i++){
int x1,x2,y1,y2;
scanf("%d%d%d%d",&x1,&y1,&x2,&y2);
maxx=max(maxx,max(y1,y2));
minn=min(minn,min(y1,y2));
edge[i*2-1].y1=edge[i*2].y1=y1;
edge[i*2-1].y2=edge[i*2].y2=y2;
edge[i*2-1].x=x1;edge[i*2].x=x2;
edge[i*2-1].k=1;edge[i*2].k=-1;//建边和之前是一样的
}
n*=2;
if(minn<=0){
for(int i=1;i<=n;i++){
edge[i].y1+=-minn+1;
edge[i].y2+=-minn+1;
}
maxx-=minn;
}//数据太水 不用离散化就能过 当然能搞还是搞一下最好
sort(edge+1,edge+n+1);
int last=0;//存上一次扫描线长度 用来统计长度少了多少累加进答案
build(1,1,maxx);
for(int i=1;i<=n;i++){
update(1,edge[i].y1,edge[i].y2-1,edge[i].k);//更新和上次是一样的
while(edge[i].x==edge[i+1].x&&edge[i].k==edge[i+1].k){
i++;//这里要注意一下要一次把这个位置上的所有的边更新出来
update(1,edge[i].y1,edge[i].y2-1,edge[i].k);
}
ans+=abs(tree[1].len-last);//更新答案
last=tree[1].len;//由于如果减少的话扫描线会收缩 周长也会增加
ans+=tree[1].sum*2*(edge[i+1].x-edge[i].x);//相差的线段条数用来更新
}
printf("%d",ans);
return 0;
}
快踩