[OI] 扫描线

扫描线的前置基础是 线段树

扫描线用于解决一些 覆盖问题或者重复问题, 比如说现在我有这样一道题:

题源 洛谷P5490 (CLOI 的 Vjudge 转存)

给出 \(n\) 个平行于坐标轴的矩形的坐标,求它们的面积并.

面积并指的是这些矩形的最大覆盖面积.

不难发现,当坐标绝对值很大时,我们采用标记法就会开不下,当 \(n\) 很大时,遍历法又会超时,因此,我们现在急需一种算法来快速求覆盖面积.

假如我们使用一根直线从左到右扫描(这样也可以保证每个矩形的左边总会比右边先扫到,避免了 \(cover\) 为负数. \(cover\) 是什么下面会提到),可以发现直线与全部覆盖面积的交集长度仅会在直线经过矩形边缘时更新,这样的话,我们只需要遍历所有的矩形左右边缘横坐标即可,\(O(2N)\) 的复杂度是我们能够承受的. 而且我们还发现,面积并实际上就是全部的区块宽度与该区域的覆盖长度之积的和.

那么现在的问题就变为:怎么更新直线被覆盖的长度?

依照线段树能够维护区间信息的思路,我们尝试用区间信息表示覆盖长度. 引入一个变量 \(cover\) 来表示完全覆盖当前区间的矩形个数. 为什么要引入这样一个变量?因为我们有如下定理:

  1. 当前被完全覆盖的区间对答案的贡献就是它的长度.
  2. 未被完全覆盖的区间对答案的贡献就是它的子区间贡献和.

这样这个题就可以完全使用线段树来解决了. 我们将矩形靠左的边称作出边,靠右的边称作入边,显然,当遇到入边时,它覆盖的区间 \(cover\) 增加一,遇到出边时减少一,更新区间贡献时,我们遇到完全覆盖的区间就直接返回,否则继续向下递归,返回时再算出总和,这就是这道题的总体思路.

此外,因为这个题中的纵坐标值可能很大,这样开线段树的时候内存无法承受,因此需要进行 离散化,将纵坐标收缩到一个很小的区间内. 而且,因为这个题的扫描对象是矩形的左边和右边,所以实际上数组要开到 \(2n\) ,线段树要开到 \(8n\). 而且要开 long long.

参考代码

By HaneDaniko 2024/2/28.

#define tol (i*2)
#define tor (i*2+1)
#define mid ((l+r)/2)
long long n;
long long ls[10000001];
struct line{ //储存矩形左右边
    long long l,r;
	long long len;
    int type;
    bool operator<(const line& A)const{
        return len<A.len;
    }
}l[20000001];
struct tree{
    int l,r; //线段树
	int cover;
    long long len;
}t[40000001];
void build(int i,int l,int r){
    t[i].l=l;
    t[i].r=r; //建树很朴素,不需要赋任何输入值
    if(l==r){
		return;
	}
    build(tol,l,mid);
    build(tor,mid+1,r);
}
void update(int i){
    if(t[i].cover){ //返回更新
		t[i].len=ls[t[i].r+1]-ls[t[i].l];
	}
    else{
        t[i].len=t[tol].len+t[tor].len;
    }
}
void change(int i, long long l, long long r, int m){
    if(ls[t[i].l]>=r||ls[t[i].r+1]<=l){
		return; //区间修改
	}
    if(ls[t[i].l]>=l&&ls[t[i].r+1]<=r){
        t[i].cover+=m;
        update(i); //假如被完全覆盖
        return;
    }
    change(tol,l,r,m); //否则为子区间的贡献和
    change(tor,l,r,m);
    update(i);
}

int main(){
    cin>>n;
    for(int i=1;i<=n;i++){
    	long long x1,y1,x2,y2;
        cin>>x1>>y1>>x2>>y2;
        ls[i*2-1]=x1;
        ls[i*2]=x2;
        l[i*2-1]=line{x1,x2,y1,1};
        l[i*2]=line{x1,x2,y2,-1};
    }
    n*=2;
    sort(l+1,l+n+1);
    sort(ls+1,ls+n+1);
    int tail=unique(ls+1,ls+n+1)-(ls+1);
    build(1,1,tail-1);
    long long ans=0;
    for(int i=1;i<n;i++){
        change(1,l[i].l,l[i].r,l[i].type);
        ans+=t[1].len*(l[i+1].len-l[i].len);
    }
    cout<<ans;
    return 0;
}

上述通过了一道例题来深入剖析了扫描线算法的基本流程. 当坐标为浮点数时,也可以通过离散化使用扫描线解决. 这里使用到的线段树是基础线段树,因此理论上树状数组也可以做,但因为涉及区间修改区间查询,因此写树状数组实际上不如线段树.

扫描线题的变种很多,但通常都万变不离其宗,需要维护特定的内容来表示当前区间的贡献情况. 比如下述例题:

给出 \(n\) 个平行于坐标轴的矩形的坐标,求它们覆盖区域的周长.

posted @ 2024-02-28 22:04  HaneDaniko  阅读(68)  评论(0编辑  收藏  举报