1.引入
扫描线多用于图形上,是一条线在图形上扫来扫去,它一般被用来解决图形面积,周长,以及二维数点等问题。
2.扫描线求面积并
如下图:
我们模拟一条扫描线,使它从下往上扫过整个平面,这条扫描线会在遇到横向线段的时候停下来更新一些东西。那么整个图形就可以找出四条线段,如图:
更新的内容就是计算线段的长度,所以要记录线段左右端点的坐标,我们把坐标存入一个数组,记作x[],并将x排序,如图:
接着考虑,在扫描线单调向上时,如何判断哪里有面积,哪里是空的?
因为一个矩形有上下底,在往上扫的时候,总是先遇到下底,再遇到上底,所以我们可以给每条横向线段赋一个权值,例如下底为1,上底为-1,所以扫描线有权值的部分就是要计算的面积
然后考虑面积并的问题,显而易见,重叠部分只计算一次面积,两个矩形相交,一个矩形的横向边上至少有一个另一个矩形边上的点
如上图,X[1]和X[3]作为左右端点组成的线段可以分成两部分,X[1]和X[2]组成线段和X[2]和X[3]组成线段
所以,这个图形横向分成了4条边3部分,纵向分成了4条边3部分
扫描线从下往上扫,只能处理横向问题,纵向需要用线段树维护
可以用线段树的每一个节点储存一个线段,当然,这条线段不一定是一组左右端点截成的,如图,可以建一棵线段树:
当一条线段被扫到时,立即更新线段树每个节点维护的线段的覆盖长度和权值,例如扫到最下面的线段时,节点1,2维护的线段覆盖长度和权值就会被更新,所以不难看出,线段树维护的左右节点其实是线段的编号,另外维护线段覆盖长度和权值,这样扫描线扫有权值的部分就有我们要计算的面积,那么就更新线段覆盖长度
对于每一条线段,我们记录它的左右端点和纵坐标,权值,按照纵坐标优先升序排序,保证扫描线从下向上扫。
不难看出,x[]数组排序的意义是为了线段树更新覆盖长度时能直接减,另外,相同的X[]我们其实只需要一个就够了,所以我们要离散化处理,所以有:
s=∑线段覆盖长度*扫过的高度(即纵坐标的差)
例题:[poj1151]亚特兰蒂斯
https://gxyzoj.com/d/hzoj/p/3444
#include<cstdio>
#include<algorithm>
#define lid id<<1
#define rid id<<1|1
using namespace std;
int n,cnt;;
double s[20005];
struct node{
double x,ay,by;
int fl;
}p[20005];
bool cmp(node x,node y)
{
return x.x<y.x;
}
struct seg_tree{
double sum,l,r;
int lazy;
}tr[80040];
void pushup(int id)
{
if(tr[id].lazy>0)
{
tr[id].sum=tr[id].r-tr[id].l;
}
else
{
tr[id].sum=tr[lid].sum+tr[rid].sum;
}
}
void build(int id,int l,int r)
{
tr[id].lazy=0;
tr[id].l=s[l],tr[id].r=s[r];
if(r-l<=1)
{
tr[id].sum=0;
return;
}
int mid=(l+r)>>1;
build(lid,l,mid);
build(rid,mid,r);
pushup(id);
}
void update(int id,double ay,double by,int fl)
{
if(tr[id].l==ay&&tr[id].r==by)
{
tr[id].lazy+=fl;
pushup(id);
return;
}
if(tr[lid].r>ay) update(lid,ay,min(tr[lid].r,by),fl);
if(tr[rid].l<by) update(rid,max(tr[rid].l,ay),by,fl);
pushup(id);
}
int main()
{
scanf("%d",&n);
while(n!=0)
{
for(int i=1;i<=n;i++)
{
double ax,ay,bx,by;
scanf("%lf%lf%lf%lf",&ax,&ay,&bx,&by);
p[i]=(node){ax,ay,by,1};
p[i+n]=(node){bx,ay,by,-1};
s[i]=ay;
s[i+n]=by;
}
sort(s+1,s+2*n+1);
sort(p+1,p+2*n+1,cmp);
build(1,1,2*n);
update(1,p[1].ay,p[1].by,p[1].fl);
double ans=0;
for(int i=2;i<=2*n;i++)
{
ans+=(p[i].x-p[i-1].x)*tr[1].sum;
// printf("%.2lf %.2lf\n",tr[1].sum,ans);
update(1,p[i].ay,p[i].by,p[i].fl);
}
printf("Test case #%d\nTotal explored area: %.2lf\n",++cnt,ans);
scanf("%d",&n);
}
return 0;
}
3.扫描线求周长并
首先考虑纵边,画个图:
标红的是要计算的纵边长度,不难发现,由于两线之间距离固定,所以当前纵边的贡献为和上条线的距离\(\times\)当前边数,当前边数即当前不相交的线段数 ×2(也就是把相交的线段拼在一起后统计),所以,可以用线段树维护当前有多少条线段
记c为当前区间的线段条数,考虑转移,当sum不为0时,其为当前区间长度;否则要从左右儿子转移而来,直接相加肯定不对,因为有一些左右儿子的线段会相交
观察左右儿子的交界处(左儿子右端点、右儿子左端点),当且仅当它们均被线段覆盖时会相交,c需要减 1,所以再维护一个区间左右端点是否被覆盖的lc,rc 即可。sum为0时分别为左儿子lc,右儿子 rc,否则均为 1
接着考虑横边,与求面积并很像,也要枚举区间被覆盖的长度,画张图:
其中,红线为周长的贡献,绿线为区间被覆盖的长度,容易发现,红线就是两两绿线的差
例题: [IOI1998] [USACO5.5] 矩形周长
#include<cstdio>
#include<algorithm>
#include<cmath>
#define lid id<<1
#define rid id<<1|1
#define ll long long
using namespace std;
int n,x[10005];
ll ans;
struct node{
int ax,bx,y,fl;
}a[10005];
bool cmp(node x,node y)
{
if(x.y!=y.y) return x.y<y.y;
return x.fl>y.fl;
}
struct tree{
int sum,len,c;
bool lc,rc;
}tr[80040];
void pushup(int id,int l,int r)
{
if(tr[id].sum)
{
tr[id]=(tree){tr[id].sum,x[r+1]-x[l],1,1,1};
}
else
{
tr[id]=(tree){0,tr[lid].len+tr[rid].len,tr[lid].c+tr[rid].c,tr[lid].lc,tr[rid].rc};
if(tr[lid].rc&&tr[rid].lc) tr[id].c--;
}
}
void update(int id,int l,int r,int ql,int qr,int v)
{
if(ql<=x[l]&&qr>=x[r+1])
{
tr[id].sum+=v;
pushup(id,l,r);
return;
}
int mid=(l+r)>>1;
if(ql<x[mid+1]) update(lid,l,mid,ql,qr,v);
if(qr>x[mid+1]) update(rid,mid+1,r,ql,qr,v);
pushup(id,l,r);
}
int main()
{
scanf("%d",&n);
for(int i=1;i<=n;i++)
{
int ax,ay,bx,by;
scanf("%d%d%d%d",&ax,&ay,&bx,&by);
a[i]=(node){ax,bx,ay,1};
a[i+n]=(node){ax,bx,by,-1};
x[i]=ax,x[i+n]=bx;
}
sort(a+1,a+2*n+1,cmp);
sort(x+1,x+2*n+1);
int m=unique(x+1,x+n*2+1)-x-1;
int lst=0;
// printf("1");
for(int i=1;i<2*n;i++)
{
update(1,1,m-1,a[i].ax,a[i].bx,a[i].fl);
ans=ans+1ll*2*tr[1].c*(a[i+1].y-a[i].y)+1ll*abs(tr[1].len-lst);
lst=tr[1].len;
}
printf("%lld",ans+a[n*2].bx-a[n*2].ax);
return 0;
}
标签:lid,int,线段,tr,笔记,学习,扫描线,id
From: https://www.cnblogs.com/wangsiqi2010916/p/18029241