扫描线 (学习笔记)(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; }
小结
其实这个算法厉害就厉害在它可以巧妙的求并集问题,在同高度的时候更新完再更新下一个答案
又一个巧妙点在于,因为传统线段树模板需要下传懒标记,但是这里不需要下传。原因就在于,只需要有一次修改就足够,可以直接当下求解,当下更改。比如可以理解为,我们需要的是当前区间被覆盖次数,如果修改,就会有新的被更改(加减),不会具有对于其它的后效性,查询也是正常查询,所以不用下传。(其实不用下传cnt的原因还在于最后查询的时候是直接查询t[1],所以根本不需要再细分,贪心而想,如果一个大的区间被覆盖,肯定没有更小的区间会影响答案)

浙公网安备 33010602011771号