学习NOTE 1——扫描线
刚学了扫描线,来写个笔记。
是什么?
扫描线,顾名思义,就是用线去扫描图形。
那扫描线是干什么的呢?
一般扫描线都用于计算 很多重合在一起的矩形 的总占地面积/外围的周长。
那它是怎么扫的呢?
给个图:


就是用一根线从左往右(或从下往上)扫过整个图形,与图形接触的部分为当前这一块的长(或宽),而扫过这一部分的时间就可以视作另一条边,然后长 \(\times\) 宽,求出面积,也是非常好用啊(?)。
感觉有点像数学上的割补法。
怎么实现?
好问题,怎么实现? 。。。沉默(挠头)
我们可以画一下:

土黄色那一块图象的左下角坐标为\((x1,y1)\),右上角坐标为\((x2,y2)\),那么左上角就为\((x1,y2)\),右上角即为\((x2,y1)\)。这一块图象的面积就是\((y2−y1)\times (x2−x1)\)。
但是一般的数据都奇大无比 (\(10^{18}\)),所以要 离散化。
我们还要记录每一根“重点线段”的 \(x\) 和 \(y\) 坐标,方便求长与宽,以求面积。
但是这就又出现了一个问题,怎么计算边的长度呢?
就像这样:

这个红色的边怎么算呢?
把蓝色和黑色的加起来,再减去重叠部分?
换个角度,可以这样算:

把很多短小的加起来就OK了。
否则就太麻烦了(可以自己模拟一下)。
那么我们可以用线段树!
不过,为什么呢?
因为我们可以把黑色、灰色、蓝色、紫色框起来的线段加起来,得出这条红色线段的长度。而这些框起来的地方就是一条线段。
那么,我们可以建立线段树,管理图中所有矩形的y坐标之间的线段。
节点的特征值就是当前扫描到的线段 在管理范围内 的总长度。
这样,求线段长度只需要区间求和就彳亍啦。
那怎么判断哪些区间包含当前扫到的线段呢?
我们只需要计算区间和就可以了。但是怎么算什么时候要加入哪条边呢?
这时候我们就可以打标记,记录在这个区间里第一条被扫到的边和最后一条被扫到的边,即“入边”和“出边”。当我们扫到入边时,就进入了这个矩形,扫到出边后,就离开了这个矩形。
然后——打标记!
是的,打标记。在进入矩形的时候,给标记\(“+1”\),出矩形的时候,给标记再\(“-1”\)。
这样一来,遇到一条出边时,就可以与之前一条入边加上的抵消掉。再根据标记的定义,判断这个节点所管理的范围是否包含被扫到的线段,有的话就加上。
TIP 1:
根据上面的一些例子可以发现,在这里线段树维护的是线段,而不是一个个点。每一个节点记录的是一整条线段,而不应该记录节点。
再举个例子:

这里有五个\(y\)坐标,区间是\([1,5]\),在扫到第一条线时,需要更改的区间就变成了\([2,4]\),如果按着线段树的板子就是要遍历\([2,3]\)与\([4,4]\)。\([4,4]\)?不对,这是一个点!!这不就漏掉\([3,4]\)了吗?!
在区间中,有\(n\)个点就会有\(n-1\)条边。是的 \(n-1\) 条边!!
所以我们可以把\([l,r]\)的区间——诶,变为\([l,r+1]\),这样就有\(n\)条边了!
回看到例子,当我们把\([2,3]\)变为\([2,4]\)就OK啦!!([2,3]不就是[2,4]吗) 而我们修改到了\([2,2]\)和\([3,3]\)时,实际上也正是在修改\([2,3]\)和\([3,4]\),这样就满足了我们的目的。
这样子的话,虽然还会出现修改查询区间\([i,i]\)的情况,但是这实际是区间\([i,i+1]\)。这样子就不再会出现记录节点的情况了。
所以,我们采用这种方法,在修改和查询区间\([l,r]\)的时候,实际上修改和查询的是\([l,r−1]\),这样就可以解决这个问题了。
TIP 2:
要修改区间,那我们要不要打懒标记呢?
其实不用。
我们要的只是\(tr_1.c\),也就是整个图象包含的被扫到的线段的长度。所以,我们根本没必要下放到各个节点。
相反,我们应该上放到根节点,不断维护根节点。
例子:洛谷P5490
板子题
代码:
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=2e5+10;
int n,ans,x_1,x_2,y_1,y_2,k[N],len;
struct scan_line{int y_1,y_2,x,mk;}li[N];
//y1,y2表示该线段上面的那个点和下面的那个点的y坐标
//x用来排序而已,mk表示该边是入边还是出边
struct segment_tree{int l,r,lc,rc,c,tag;}tr[N<<1];
//lc,rc为左/右儿子的编号
//tag用来判断扫到下一条线段时是否包括括这个矩形
bool cmp(scan_line a,scan_line b){return a.x<b.x;}
void build(int l,int r){//建树
len++;
int p=len;
tr[p]={l,r,-1,-1,0,0};
if(l==r) return ;
int mid=(l+r)>>1;
tr[p].lc=len+1;build(l,mid);
tr[p].rc=len+1;build(mid+1,r);
}
void uplz(int p,int l,int r){
if(!tr[p].tag){//若tag为0
if(l==r) tr[p].c=0;//如果l==r+1,则为树最底下的叶子节点,没有被线段覆盖,故c为0
else tr[p].c=tr[tr[p].lc].c+tr[tr[p].rc].c;
//虽然该节点没有覆盖,但其子节点可能被覆盖,所以加上自己的儿子节点
}
}
void modify(int p,int l,int r,int t){
//p为节点编号,l、r为修改范围,k用于修改
if(tr[p].l>r||tr[p].r<l) return ;
if(tr[p].l>=l&&tr[p].r<=r){
tr[p].tag+=t;
if(tr[p].tag) tr[p].c=k[tr[p].r+1]-k[tr[p].l];//若tag>0要加上。因为这里的r实为r-1的,但y数组没有该操作,故要加1
else tr[p].c=0;
uplz(p,tr[p].l,tr[p].r);//维护
return ;
}
int lc=tr[p].lc,rc=tr[p].rc;
int mid=(tr[p].l+tr[p].r)>>1;
if(r<=mid) modify(lc,l,r,t);
else if(mid+1<=l) modify(rc,l,r,t);
else modify(lc,l,mid,t),modify(rc,mid+1,r,t);
uplz(p,tr[p].l,tr[p].r);
}
signed main(){
ios::sync_with_stdio(0);
cin.tie(0),cout.tie(0);
cin>>n;
for(int i=1;i<=n;i++){
cin>>x_1>>y_1>>x_2>>y_2;
k[(i<<1)-1]=y_1,k[i<<1]=y_2;//离散化
li[(i<<1)-1]={y_1,y_2,x_1,1};
li[i<<1]={y_1,y_2,x_2,-1};//记录线段
}
n<<=1;//一个矩形就有两条线段
sort(k+1,k+n+1);//离散化
sort(li+1,li+n+1,cmp);//按照x坐标的大小来排序
int c=unique(k+1,k+n+1)-k-1;//去重计算有多少个不同的y,也就是这个图像有几个y。因为区间[l,r]表示的是[l,r+1],区间[l,r-1]实际上是[l,r],故总区间长度cnt要多减一个1
build(1,c);
for(int i=1;i<=n;i++){//扫描,面积并中扫到n-1条线段即可
y_1=lower_bound(k+1,k+c+1,li[i].y_1)-k;//当前这条线段的y_1坐标的离散值,表示线段的y1是在线段树的第(离散值)个节点
y_2=lower_bound(k+1,k+c+1,li[i].y_2)-k-1;//同上,因为[l,r]实为[l,r-1],故-1
modify(1,y_1,y_2,li[i].mk);//修改
ans+=tr[1].c*(li[i+1].x-li[i].x);//ans记录当前扫到的面积
}
cout<<ans;
return 0;
}
(图片取自https://www.luogu.com.cn/article/28yc6g1d,感谢dalao)

浙公网安备 33010602011771号