【学习笔记】扫描线
【别急,我也不会,没写完】
定义:
如图:
(图片来源:oiwiki)
像这样的一条线在图上扫描时,便是扫描线。
(呃呃和没说没有任何区别呢)
因此可见扫面线往往是求矩形面积并集或周长并集的好工具,当然,也可以运用在二维图中。
当然它不止可以从上往下扫,还可以从左往右扫:
(当然自己画的图可能丑一些,我解释一下,那个紫色的是扫描线)
如果说我们要求这个两个矩形的并面积怎么求?
当然你可能是直接容斥:。
但是要是数据范围很大呢?多步容斥?你又不知道几个矩阵是不是都有交集。
这下不得不跟您谈一谈我们内存及其优秀的且很快的 算法了。
求面积并:
我们跟着例题说这个:
【模板】扫描线
题目描述
求 个四边平行于坐标轴的矩形的面积并。
输入格式
第一行一个正整数 。
接下来 行每行四个非负整数 ,表示一个矩形的四个端点坐标为 。
输出格式
一行一个正整数,表示 个矩形的并集覆盖的总面积。
样例 #1
样例输入 #1
2 100 100 200 200 150 150 250 255
样例输出 #1
18000
提示
对于 的数据,。
对于 的数据,,,。
怎么求这两个矩形的面积并?
求三个小矩形面积相加嘛。
怎么求这三个矩形的面积?
宽是两条扫描线的之间的 差值,长是求边嘛。
我们把每条扫描线与矩形边重合部分的左右端点记录一下, 轴高度记录一下就可以了:
两条扫描线之间高度差扫描线覆盖长度
如何维护 长度?
毫无疑问,我们可以尝试用桶维护:
设四元组 表示一条横线,其中 表示覆盖次数,我们将 ,,,……, 这些桶都加 ,表示覆盖了 区间。
因此被覆盖的长度为 。
选择桶来维护扫描线上的 是否被覆盖,显然是有很多问题的:
-
Q:如果 不一定是非负整数?
-
A:如果 是浮点数或者较大(甚至不是非负数),我们需要选择离散化,使得下标对应实际的 。
-
Q:时间复杂度?
-
A:线段树优化。
-
Q:空间复杂度?
-
A:存储左右端点的 坐标。
什么什么什么,等等,线段树优化?
为什么能够线段树优化?
我们把每一条垂直于 轴的竖边提取出来,然后把他们延伸到 轴上,这不就把 轴分割成了线段?
因此,我们设 就是我们需要维护的区间,需要维护:
-
区间的左右端点 。
-
区间被覆盖的次数 。
-
区间被横边覆盖的长度 。
但是,线段树有一个前提问题:
线段树存在长度为 的点,但是这个节点对应的应该是一个 坐标,也就是说,像这样的叶节点,却保留了两种信息,而我们需要维护的是扫描线上每一段被覆盖的次数及其长度。
而且我们早已确定的是,左右儿子没有交集,但是相邻的两个线段 与 显然存在了交集 ,我们必须考虑改变区间与横边的映射关系。
因此,我们选择将原本表示区间 的节点表示区间 ,这样就兼容了。
以下的 表示离散后坐标, 表示原值。
然后,我们统计线段树根节点的长度标记,乘两条横边之间高度差就得到了一部分的面积。
对于一个四元组 ,我们在 上执行区间修改,该区间被划分为 个节点,将这些节点的覆盖次数 都加 。
因此,对于线段树上任意一个节点 ,若 ,则 。
(这里 与 都代表的是离散化后坐标)
(这是坐标,所以不用在后面。)
否则,该点的 等于子节点 之和。
然而然而,如果我们遇到矩阵的上边,必须清空信息时,我们选择:
将矩形的上边的和下边的赋为相反的权值。
由于我们的扫描线是从下往上走的,那么完全可以将矩形的下边赋值为 ,给矩形的上边赋值为 ,当我们遇到下边时,说明这个矩形还会在我们分割的多个小矩形部分出现,所以它对应的区间被覆盖的次数加 ,之后统计它的影响,当我们遇到一条上边,之前一定已经统计完下边的影响,应该将对应区间的覆盖次数 ,此时该矩形面积统计完毕。
Miku's Code
#include<bits/stdc++.h> using namespace std; #define lid (id<<1) #define rid (id<<1|1) typedef long long intx; const int maxn=1e6+50; struct TREE{ int l,r,cnt; intx len; //cnt:被覆盖次数 //len:区间覆盖长度 };TREE tr[maxn<<2]; struct Scanline{ intx l,r,h; int mark; //mark是权值,1/-1 bool operator <(const Scanline &sl)const{ return h<sl.h; } };Scanline line[maxn<<1]; int n,m; intx ans=0; intx X[maxn<<2]; void push_up(int id){ if(tr[id].cnt){ //被覆盖过,更新长度 tr[id].len=X[tr[id].r+1]-X[tr[id].l]; } else{ //否则合并儿子信息 tr[id].len=tr[lid].len+tr[rid].len; } } void build_tree(int id,int l,int r){ tr[id].l=l; tr[id].r=r; if(l==r){ tr[id].len=tr[id].cnt=0; return; } int mid=(l+r)>>1; build_tree(lid,l,mid); build_tree(rid,mid+1,r); push_up(id); } void input(){ scanf("%d",&n); int x1,y1,x2,y2; for(int i=1;i<=n;++i){ scanf("%d %d %d %d",&x1,&y1,&x2,&y2); X[2*i-1]=x1; X[2*i]=x2; line[2*i-1]=(Scanline)<%x1,x2,y1,1%>; line[2*i]=(Scanline)<%x1,x2,y2,-1%>; //存上上下边 } } void update(int id,intx x,intx y,int k){ int l=tr[id].l,r=tr[id].r; if(X[r+1]<=x||y<=X[l]) return; /*为什么加等号捏? 考虑[2,5][5,8]两个线段却要修改[1,5]的值 5在区间内但是[5,8]却不是我们希望修改的 因此加等号 */ if(x<=X[l] && X[r+1]<=y){ tr[id].cnt+=k; push_up(id); return; } update(lid,x,y,k); update(rid,x,y,k); push_up(id); } int main(){ input(); n=n<<1; sort(line+1,line+1+n); //所有横边y升序排序,先碰下边 sort(X+1,X+1+n); int siz=unique(X+1,X+1+n)-(X+1); build_tree(1,1,siz-1); //注意映射关系是[l,r+1] for(int i=1;i<n;++i){ //最后一条边不用改 update(1,line[i].l,line[i].r,line[i].mark); ans=ans+tr[1].len*(line[i+1].h-line[i].h); } printf("%lld",ans); return 0; }
扫描线应用
upd:2023/7/29
扫描线今年NOI T1考了?
好押
呃呃今天说扫描线思想,面积并是扫描线裸题。
Interesting Sections
题面翻译
给你一个长为 的非负整数序列 ,求有多少区间 满足 。
。
题目描述
William has an array of non-negative numbers . He wants you to find out how many segments pass the check. The check is performed in the following manner:
- The minimum and maximum numbers are found on the segment of the array starting at and ending at .
- The check is considered to be passed if the binary representation of the minimum and maximum numbers have the same number of bits equal to 1.
输入格式
The first line contains a single integer ( ), the size of array .
The second line contains integers ( ), the contents of array .
输出格式
Output a single number — the total number of segments that passed the check.
样例 #1
样例输入 #1
5 1 2 3 4 5
样例输出 #1
9
样例 #2
样例输入 #2
10 0 5 7 3 9 10 1 6 13 7
样例输出 #2
18
(ps:某个数的popcount
指某个数在二进制下 的总数)
就是给你一个长度为 的数组,要求输出该数组多少个区间的最大值和最小值的 是相等的。
那我们可以转换一下问题:给你一个长度为 的数组,问该数组有多少个区间的最大值最小值都在相同的 上。(也就是很多人题解写的“合适位置”)
这种区间带两个 的问题似乎都可以用扫描线或者 分治来做。
(但是对于本题来说 好像代码又短又快)
(据说对于维护的东西越多,线段树就越优,反之 就更优)
先枚举所有的 值,如果该区间最大值最小值都在我枚举的 值上,那赋为值 。
那就可以维护一个线段树,线段树每个节点表示当前 的每一个点为左端点,到现在扫到的 的 贡献。
暴力硬扫肯定能拿分,但不多,所以考虑优化:
- 考虑一个数的管辖区间,可以用单调栈。(就是一个数,它在这个区间里是最小值或最大值,扩张一点就不是啦)
-
最大值维护一个单调递减的栈 ,当放入元素 时,若 ,那么 出栈,其管辖区间就是其枚举起点到 。
-
最小值则维护一个单调递增的 ,当放入元素 时,若 ,那么 出栈,其管辖区间就是其枚举起点到 。
然后就初具扫描线的雏形了。
- 考虑一个数的贡献次数:
-
进栈对最大值最小值更新 次。
-
出栈对最大值最小值更新 次。
22 次
- 寻找答案的时候,我们的答案就是最大值管辖区间中最小值 与它相同的个数(最小值同)
-
最大值的管辖区间全部设为 ,统计相同的 的最小值管辖区间中 的个数。
-
最小值的管辖区间全部设为 ,统计相同的 的最大值管辖区间中 的个数。
类似于给管辖区间染色吗(?)
- 维护单调栈的产生贡献。
-
对一个 只有 相同的点产生贡献,所以我的单调栈里只维护相同 的点并计算贡献。
-
对于 不同的点,它会理应导致小的点出栈,但是它与我们制定的 不同,所以我们把小点叉出去,但是不让 不同的大点插进来。
最后简单卡卡常数,什么 啊,什么超级快读,什么 ,,都让他上前来!
Miku's Code
#include<bits/stdc++.h> using namespace std; #define lid (id<<1) #define rid (id<<1|1) #define mid ((l+r)>>1) typedef long long intx; const int maxn=1e6+10; inline char gc(){ static char buf[100000],*p1=buf,*p2=buf; return p1==p2&&(p2=(p1=buf)+fread(buf,1,100000,stdin),p1==p2)?EOF:*p1++; } inline intx read(){ intx t=0,f=0;char v=gc(); while(v<'0')f|=(v=='-'),v=gc(); while(v>='0')t=(t<<3)+(t<<1)+v-48,v=gc(); return f?-t:t; } intx a[maxn]; bool vis[70]; int n,popcnt[maxn]; int top1,stk1[maxn],top2,stk2[maxn],l1[maxn<<2],r1[maxn<<2],l2[maxn<<2],r2[maxn<<2]; //栈与管辖区间,1是max栈,2是min栈 int lazy1[maxn<<2],lazy2[maxn<<2]; intx sum1[maxn<<2],sum2[maxn<<2]; intx ans[maxn<<2],ansl; inline void change_max(int id,int l,int r,int type){ if(type==1){ sum1[id]=r-l+1; ans[id]=sum2[id]; //找min的popcount相同就是答案 lazy1[id]=1; } else{ sum1[id]=0; ans[id]=0; lazy1[id]=-1; } } inline void change_min(int id,int l,int r,int type){ if(type==1){ sum2[id]=r-l+1; ans[id]=sum1[id]; lazy2[id]=1; } else{ sum2[id]=0; ans[id]=0; lazy2[id]=-1; } } inline void push_up(int id){ ans[id]=ans[lid]+ans[rid]; sum1[id]=sum1[lid]+sum1[rid]; sum2[id]=sum2[lid]+sum2[rid]; } inline void push_down_max(int id,int l,int r){ change_max(lid,l,mid,lazy1[id]); change_max(rid,mid+1,r,lazy1[id]); lazy1[id]=0; } inline void push_down_min(int id,int l,int r){ change_min(lid,l,mid,lazy2[id]); change_min(rid,mid+1,r,lazy2[id]); lazy2[id]=0; } inline void update_max(int id,int l,int r,int x,int y,int val){ if(x<=l && r<= y){ change_max(id,l,r,val); return; } if(lazy1[id]) push_down_max(id,l,r); if(lazy2[id]) push_down_min(id,l,r); if(x<=mid) update_max(lid,l,mid,x,y,val); if(y>mid) update_max(rid,mid+1,r,x,y,val); push_up(id); } inline void update_min(int id,int l,int r,int x,int y,int val){ if(x<=l && r<=y){ change_min(id,l,r,val); return; } if(lazy1[id]) push_down_max(id,l,r); if(lazy2[id]) push_down_min(id,l,r); if(x<=mid) update_min(lid,l,mid,x,y,val); if(y>mid) update_min(rid,mid+1,r,x,y,val); push_up(id); } inline intx lowbit(intx x){ return (x&(-x)); } inline int get_popcount(intx x){ int res=0; while(x){ if(x) ++res; x^=(lowbit(x)); } return res; } inline void input(){ n=read(); for(register int i=1;i<=n;++i){ a[i]=read(); popcnt[i]=get_popcount(a[i]); vis[popcnt[i]]=true; } } inline void pre(){ for(register int i=1;i<=n;++i){ while(top1 && a[stk1[top1]]<=a[i]) --top1; l1[i]=stk1[top1]+1; stk1[++top1]=i; while(top2 && a[stk2[top2]]>=a[i]) --top2; l2[i]=stk2[top2]+1; stk2[++top2]=i; } } inline void work(int pop){ change_max(1,1,n,-1); change_min(1,1,n,-1);//清空初始化 top1=top2=0; for(register int i=1;i<=n;++i){ while(top1 && a[stk1[top1]]<=a[i]){ update_max(1,1,n,stk1[top1-1]+1,stk1[top1],0); //这是单调递减的栈,把次栈顶到栈顶的贡献区间删掉。 //若a[i]与pop相同,先删除后补回 //lazy设为-1,不再重复计数 --top1; } while(top2 && a[stk2[top2]]>=a[i]){ update_min(1,1,n,stk2[top2-1]+1,stk2[top2],0); --top2; } if(popcnt[i]==pop){ stk1[++top1]=i; update_max(1,1,n,l1[i],i,1); stk2[++top2]=i; update_min(1,1,n,l2[i],i,1); } //printf("%lld\n",ansl); ansl+=ans[1]; } } int main(){ //freopen("in.txt","r",stdin); //freopen("myout.txt","w",stdout); input(); pre(); /*for(int i=1;i<=n;++i){ printf("%d %d %d\n",i,l1[i],l2[i]); }*/ for(int i=0;i<=60;++i){ if(vis[i]==true){ work(i); } } printf("%lld",ansl); return 0; }
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!