扫描线相关

前置知识:

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;
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值