扫描线 (学习笔记)(25.11.4)

扫描线 (学习笔记)

概述

扫描线,顾名思义是使用一根线去进行扫描,具体可以用来在一个数轴(或平面直角坐标系)上去计算面积和,区间权值和,点权和,点的总量等统计问题,如下为线的示例:

可以看见是一条线在慢慢向着一个方向扫描,而每次扫描出来的都是一些规整的矩形,我们只需要去将这些矩形的面积做统计,就可以做到对一个复杂组合图形的计算

细节过程

首先我们可以暴力去做一下这道题,先去在 \(O(值域)\) 上枚举,模拟一根线,在这根线从这一根到下一根的时候,我们要考虑中间经过的面积,也就是上一根线上的有图形存在的线的长度——这一条线上还存在着的图形长度,我们把这个长度统计,之后长乘宽并且统计到总答案里,(表示在每条线都去做一次枚举看看存在与这个线的区间的值是多少,具体要用到时间\(O(n)\))所以时间是\(O(值域*n)\),空间内存都会爆

考虑优化,首先我们考虑怎么去优化 维护一根扫描线上所存在的有意义的点 ,发现特点,如果一个图形进入或出去,对于这个线上的点是区间进行\(\pm\)操作,所以我们就可以想到用线段树去维护,在每到一个新线的时候去看看某些点有没有进入/出去,使用线段树可以把时间优化为 \(O(值域*\log(n))\)

然后我们考虑怎么把值域时间空间优化,因为输入的数据量是有限的,我们可以如上图一样,每次只考虑在一个图形边缘的情况,所以我们可以考虑离散化,把输入进行离散化,按序去做维护操作

代码实现

#include <bits/stdc++.h>
using namespace std;
const int N = 1e5+100;
#define ls(x) (x<<1)
#define rs(x) (x<<1|1)
#define ll long long
int n;
int x_1, x_2, y_1, y_2;

//用来记录扫描线的
struct line{
    int lx, rx, y;
    //用来判断线是进入还是离开
    int tag;
}
//确保扫描线是从下往上的
bool cmp(line A, line B){return A.y<B.y;}

//一个用来存线,一个用来存X的坐标,做离散化
vector <line> l;
vector <int> X;

struct tree{
    int l, r;
    int cnt, len;
}t[8*N];
//普通线段树建树
void build(int p, int l, int r){
    t[p] = {l,r,0,0};
    if(l==r) return;
    int mid = l+((r-l)>>1);
    build(ls(p),l,mid);
    build(rs(p),mid+1,r);
}

//此函数用于计算一个区间内的有效长度
//如果被覆盖了就说明长度是左端点与右端点的距离
//如果没有就递归加入左右叶子的值
void spread (int p){
    if(t[p].cnt) t[p].len = X[t[p].r+1] - X[t[p].l];
    else{
        if(t[p].l==t[p].r)t[p].len = 0;
        else t[p].len = t[ls(p)].len+t[rs(p)].len;
    }
}
//区间修改
void change(int p, int l, int r, int ad){
    //如果完全覆盖,那么就更新一下区间被完全覆盖的次数
    if(l<=t[p].l&&t[p].r<=r)t[p].cnt+=ad;
    else{
        //否则递归两边,去改变一下两边的完全覆盖数
        int mid = t[p].l + ((t[p].r-t[p].l)>>1);
        if(l<=mid) change(ls(p),l,r,ad);
        if(r>mid)  change(rs(p),l,r,ad);
    }
    //最后去求一下有效距离
    spread(p);
}
int main(){
    ios::sync_with_stdio(0), cin.tie(0);
    cin>>n;
    for(int i=1; i<=n; i++){
        cin>>x_1>>y_1>>x_2>>y_2;
        //这里存的是x坐标用于离散化
        X.push_back(x_1);
        X.push_back(x_2);
        //把线存起来用于后面的遍历
        l.push_back({x_1,x_2,y_1,1});
        l.push_back({x_1,x_2,y_2,-1});
    }
    
    //离散化,把X进行排序便于查找,将坐标对应到几个下标,然后将下标直接抽象化为左右边界去寻找
    sort(X.begin(),X.end());
    sort(l.begin(),l.end(),cmp);
    X.erase(unique(X.begin(),X.end()),X.end());
    
    build(1,0,X.size()-2);
    ll ans=0;
    for(int i=0; i+1<l.size(); i++){
        //这里找的是左右边界
        int lft = lower_bound(X.begin(),X.end(),l[i].lx)-X.begin();
        int rig = lower_bound(X.begin(),X.end(),l[i].rx)-X.begin();
        change(1,lft, rig-1, l[i].tag);
        //统计答案
        ans+=(ll)t[1].len*(l[i+1].y-l[i].y);
    }
    cout<<ans;
}

P5490 【模板】扫描线 & 矩形面积并

小结

其实这个算法厉害就厉害在它可以巧妙的求并集问题,在同高度的时候更新完再更新下一个答案

又一个巧妙点在于,因为传统线段树模板需要下传懒标记,但是这里不需要下传。原因就在于,只需要有一次修改就足够,可以直接当下求解,当下更改。比如可以理解为,我们需要的是当前区间被覆盖次数,如果修改,就会有新的被更改(加减),不会具有对于其它的后效性,查询也是正常查询,所以不用下传。(其实不用下传cnt的原因还在于最后查询的时候是直接查询t[1],所以根本不需要再细分,贪心而想,如果一个大的区间被覆盖,肯定没有更小的区间会影响答案)

posted @ 2025-11-04 21:48  Yuriha  阅读(0)  评论(0)    收藏  举报