浅谈扫描线
可能更好的阅读体验:隙间传送
Preface
本文部分题目摘自 lxl 的数据结构系列课件
由于工程量巨大,难免会出现错字、漏字的情况,如果影响到了您的阅读体验,还请私信我,我会在第一时间修复。
考虑到一些题目不易访问,所以在部分题目下面给出了简要题意,其余未给出简要题意的题目均给出了题目链接 qwq。
本文建议大家有一定的数据结构基础后再来阅读 /heart。
个人感觉扫描线不是难点,难点在于怎么抽象模型。
首先需要明白一个东西,叫做
在一个
B 维直角坐标系下,第i 维的坐标在范围[l_i, r_i] 之内,则符合这些限制的点构成的的点集为B 维正交范围。
当每个维度都是两个端点的限制时,我们管它叫 2B-side 的
如果部分维度只有一个端点的限制,我们管它叫 A-side 的
如果有维度没有限制,则显然,这个维度不参与构成正交范围。
一维扫描线
当处于一个静态的二维平面上时,我们不难想到用扫描线来扫描其中的一个维度,用数据结构来维护另一个维度。
在扫描线扫描的过程中(例如从左到右扫),可能会在数据结构上产生一些修改和询问。
如果信息可差分,则直接差分,否则需要分治。
如果从序列的角度上来看的话,不难发现扫描线做的实际上是枚举询问右端点,维护左端点答案。
假设当前的信息是一个 4-side 的信息,这个时候如果信息满足差分,就可以把 4-side 矩形差分成 3-side,然后这个时候就可以直接做一维扫描线,我们让扫描线扫描一个 side,剩下所需要维护的信息就是一个 2-side 的信息,说人话就是一个区间操作的信息。
静态区间查询类问题
静态的序列,只有区间查询。
一般维度只有
二维数点
给定一个长为
n 的序列,有q 次询问,每次问你区间[l, r] 中有多少位置上的数在范围[x,y] 之内。
首先,不难发现所查询的是一个 4-side 矩形内部的点的个数,这类问题一般叫做「二维数点」,是一类很经典的问题。
首先考虑一下怎么建系,由于题目直接把坐标系怼你脸上了,所以不难看出可以让一个轴表示值域维,另一个轴表示序列维,然后显然,序列维上是容易差分的,所以在序列维上差分,将范围从 4-side 降到 3-side,这个时候就好办了,扫描线扫描序列维剩下的那一个 side,然后在扫描线上就变成了查询
基础问题
给定
n 个矩形,然后有m 次询问,每次询问给定一个点,问这个点被多少矩形所包含。范围懒得给了,反正要求单
\log 。
如果站在询问的点的角度来看,似乎问题不是很好解决。
不妨这次来试着拆一下贡献,看看每个矩形都贡献了什么,不难发现每个矩形都将这个矩形内的点被套的次数
这样就爽了,考虑直接建系,横轴就是题目中的
时间复杂度
P1972 [SDOI2009] HH 的项链
老典题了。
不妨先尝试建系,让一个轴为序列维,让一个轴为值域维,然后你会发现这个不同的数的个数似乎特别不好维护。
不妨让点都具有超能力,当扫描线扫描到和他相同的值时,把前驱的贡献转移到当前位置(可以认为点直接瞬移到扫描线所在的位置上),这样的转化就不难将原问题变成一个 2-side 的区间了,考虑维护一个 now 数组,表示当前这个点所在的位置。然后扫描序列维(对应询问
BZOJ 3489 A simple RMQ problem(弱化版)
给定长为
n 的序列,m 次查询区间中有多少值只出现一次。要求单
\log 。
你不难发现当一个区间内出现两个相同的数时,这个数就不产生贡献了。
于是类比上面的「HH 的项链」,我们不难想到当我们扫描线扫到一个数时,让其贡献为
CF522D Closest Equals
考虑直接建系,设横轴为序列维(对应询问
现在考虑扫到一个数要做什么,首先,显然的是,至少要有一对数才有「距离」这个概念,然后我们在扫描的时候都是在扩张询问的右端点,所以不妨让相邻两个点中靠左的那个点的位置维护距离,这样就变成了在线上维护「单点修改,区间查询最小值」了。
时间复杂度
P4137 Rmq Problem / mex
考虑建系,设横轴为序列维,纵轴为值域维,在序列维上做扫描线,线上维护最小值,然后扫描线的移动就是线上的单点修改,查询就是在扫描线上进行二分,这里用线段树维护线上的信息即可。
时间复杂度
未知渠道获得的题
给定一棵
n 个点的树,然后有q 次查询,每次查询区间[l,r] ,表示询问当这个树仅剩点和边的编号在[l,r] 之间时连通块的个数。要求单
\log 。
考虑这里的连通块个数等于啥,显然等于「点数 - 边数」,然后点的个数是简单的(恒定
但是边不一定,因为有可能它连接了超出
这不失为一个非常优秀的性质,它允许我们将限制条件转化为让
现在考虑建系,设横轴为
时间复杂度
BZOJ 4212 神牛的养成计划
一个经典的 trick 是对字符串建立 Trie 树,然后转化到 DFS 序上的点。
此时我们不难想到对模式串正着建个 Trie,记为
这个时候我们开始建系,考虑设横轴为
由于是强制在线,所以把扫描过程扔到主席树上,此时依然是单
时间复杂度
UOJ 637. 【美团杯2021】A. 数据结构
Trick Point:
这种查有多少元素的题一般都考虑利用不同值对答案贡献独立的性质。
先考虑对每个元素计算一下其对哪些询问有贡献,然后使用数据结构批处理贡献。
考虑每个元素对答案的贡献,有一种常见方法是考虑对于什么询问这个元素对答案没有贡献。
如果一个出现了
k 次的数所影响的范围可以用O(k) 个矩形表示出来,我们也就解决了这个问题,因为\sum k = n 。
由于一个数
- 数
x 全都被+1 了。 - 数
x-1 全没被+1 。
现在需要将这些限制条件转变成平面上的限制条件,考虑建系,设横轴为序列维(对应询问
-
数
x 全部被+1 了。简单的,维护一下数
x 第一次出现的位置L 和最后一次出现的位置R ,那么显然当询问[l,r] 存在l \le L 且r \ge R 的时候对区间没有贡献。 -
数
x-1 全没有被+1 。考虑数
x-1 相邻两次出现的位置i,j ,则显然的是[i+1,j-1] 这一段内是不存在x-1 的。对应到二维平面上,就是矩形
[i+1,j-1] \times [i+1,j-1] 。换句话说,所有
x-1 不出现的位置构成了O(cnt_x + 1) 个矩形。
然后不难发现,限制
考虑此时的询问就变成了平面上的若干点,这时候我们将问题转化为上面的「基础问题」,做删贡献扫描线即可。
时间复杂度
百度之星 2021 初赛第三场 1008
给定一个长度为
n 的序列A ,下标从1 到n ,有m 次查询操作,每次给出一个区间[l,r] ,求一个子区间[l',r'] ,满足l \le l' \le r' \le r ,使得[l',r'] 中的颜色数比[l,r] 中出现的颜色数少,且其长度r'-l'+1 最大。如果l' > r' ,我们认为没有值在[l',r'] 中出现过。
首先感谢高效率原题自动机 do_while_true 为我找到了原题链接。
考虑答案会是什么样子,不难想到要么是一个前缀,要么是一个后缀,要么是一个中间一部分的子区间。
前缀咋做呢,显然的,只要找到了最靠右出现第一次出现的那个数的位置,在这个位置之前的都是合法的答案,后缀同理。这两个「扫描线 + 树状数组 + 倍增」都是轻松做的,在这里不多赘述。
至于「中间一部分的子区间」这一类情况,套用 UOJ 637 那道题的 trick,考虑「以
时间复杂度
UVA1608 Non-boring sequences
唯一的难点在于建系。
假设一个数的前驱、后继的位置为
然后你发现如果这样直接在序列上统计的话似乎不直观,而且好像还算重了很多部分,所以不妨把它放到二维平面上。具体的讲,就是以横轴为序列维,纵轴也为序列维建系,然后此时一个点所支配的就是一个矩形
时间复杂度
经典问题
有一个长度为
n 的序列,有m 次询问,每次询问一个区间,问区间上出现奇数次数的数的异或和为多少。要求线性。
根据异或的性质,发现当一个数异或偶数次自己,贡献为
所以原问题可以转化成区间异或和,考虑做异或前缀和即可。
时间复杂度
不那么经典的问题
有一个长度为
n 的序列,有m 次询问,每次询问一个区间,问区间上出现偶数次数的数的异或和为多少。要求单
\log 。
根据「经典问题」,我们知道了出现奇数次怎么做,但是感觉偶数次似乎很难做的样子。
考虑正难则反,由于异或的性质「自己为自己的逆运算」,所以只要能够统计出「区间内出现的数的异或和」,然后再异或上「区间异或和」,我们就得到了出现偶数次数的数的异或和。
那对于这种区间颜色单贡献有关的部分,容易想到「HH 的项链」,显然这个题可以类比「HH 的项链」,当我们扫描到一个数的时候,让这个数的前驱的贡献变为
时间复杂度
CF1083D The Fair Nut's getting crazy
假设
考虑维护序列
建系,设横轴为
时间复杂度
CF793F Julia the snail
考虑离线,设横轴为询问右端点,线上维护询问左端点,设
由于没有区间加,所以时间复杂度
CF407E k-d-sequence
首先特判
现在考虑
- 区间内所有数模
d 均为一个数。 - 不出现重复的数字。
-
可以将序列分成若干个「模
现在需要对每个连续段考虑限制
考虑扫描线扫描序列右端点,线上维护序列左端点,即在线上维护
考虑限制
综上,我们需要在线上维护「区间加、首端删除、末端加入,区间上二分」这些操作,简单的,用线段树搞一下就好了。
时间复杂度
CF453E Little Pony and Lord Tirek
考虑每一次询问都会导致一部分直接推平,然后推平后就成了简单的分段函数型(一次函数与常值函数)了,我们设时间为每个位置的颜色,则此时每一次区间查询就被我们转换到区间染色,这样贡献被拆成了若干染色段的同时也方便我们维护时间差。
考虑
时间复杂度
P5490 【模板】扫描线(矩形面积并)
(好好好,现在才做模板题是吧)
由于出题人直接把坐标系怼在我们脸上了,只需要考虑扫描线的部分就好了。
把矩形拆成入边和出边,然后在
这里的线段树略微有一点点技巧,我们可以维护区间
未知渠道获取的题
有
n 个矩形和q 次询问。每次询问给你一个矩形,问这个矩形与
n 个矩形之并的交的面积。原版要求强制在线,不过个人认为没有必要,离线就好。
要求单
\log 。
承上启下的好题了,给下面打个基础 qwq。
在 P5490 中,我们知道了怎么做矩形并,首先将矩形差分,然后线上维护「区间
然后这个题特别有意思,先形式化一下题意,此时查询操作为「查询询问矩形内
依旧考虑扫描线,由于查询信息可差分,直接差分成 3-side 矩形,然后问题变成了维护区间
那怎么强制在线呢,依旧是简单的,将扫描过程丢到主席树上,此时可能需要一个标记永久化,由于原题还需要离散化,所以需要还有一个垃圾分讨……反正很恶心就对了/tuu。
注意,这个问题还有一个对偶版本(应该可以这么叫吧)是这样的:
有
n 个矩形和q 次询问。每次询问给你一个矩形,问这个矩形与
n 个矩形的并的面积。
两者本质上等价,只不过一个是需要拿询问矩形的面积减一下,一个直接就是答案,如果哪场模拟赛出了这个问题,可以直接爆锤出题人 /cf
可供练习的题:
- P5070 [Ynoi2015] 即便看不到未来
- CF799F Beautiful fountains rows
区间子区间问题
给你一个序列,每次询问区间有多少个子区间满足某个条件。
遇到这类问题,通常以两轴都为序列维建系(横轴为
不过由于区间有个性质是
l\le r ,在平面上反馈过来的就是只有一个半平面才有贡献,所以我们所说的「矩形」具体的讲应该是一个「三角形」的样子,不过我们还是可以把它看成 2-side 矩形。
问题就被转化为查询
如果刚开始不能理解的话(像我一样),可以尝试这样理解:
我们对
y 轴上的每个位置i 维护当前是否合法的值a_i 。然后再给每个位置
i 引入一个计数器cnt_i 。每次扫描线向右侧移动的时候,就是在进行全局
cnt_i \gets cnt_i +a_i 的操作。
P3246 [HNOI2016] 序列
一个比较经典的问题,如果看懂上面内容的话就可以一眼秒了。
首先将区间转化为平面上的点,即建系,横坐标为
如果你笨一点的话,可以使用分治将所有矩形预处理出来,如果你不想被别人说笨蛋的话,也可以使用单调栈维护。
不难发现查询是一个 4-side 矩形,考虑差分,将其变成 3-side 矩形,发现线上需要维护一个「区间覆盖,区间查询历史版本和」这样的操作,然后这个东西可以转换,变成「区间加,区间和」,线段树普通维护即可。
当然这个问题存在更牛逼的「 强制在线 +
时间复杂度
CF997E Good Subsegments
(被析合树橄榄了)
转化一下题意先:
给定一个长度为
n 的排列P ,每次查询区间[l,r] 中有多少子区间[l',r'] 满足\max_{i=l'}^{r'}p_i-\min_{i=l'}^{r'}p_i = r' - l' 。
考虑套用上面写的套路,将每个区间表示为二维平面上的点
首先初始化
呆一点的,在序列上进行最值分治(你用单调栈也行),具体地说,每次分治的序列中的最大/最小值都是分支中心,然后
此时,问题被转化为给定一个平面,进行
由于矩形内元素一定非负,所以套用「矩形面积并」时的线段树维护套路,即可计算
问题来了,我们在扫描线后的 2-side 矩形询问,需要查询一个区间在前缀时间中
时间复杂度
EC final 2020 G. Prof. Pang's sequence
问题转化:
区间有多少子区间颜色数为奇数
\iff 区间每个子区间颜色数\bmod 2 的和。
依旧是考虑将区间转化为平面上的点,建系,让横轴为序列维(对应询问
考虑扫描的过程中遇到一个值
现在问题就简单了,由于我们线上的每个位置只会是
不过这样查询显然是要死掉的,所以差分一下,就变成了查询区间历史和,仿照上一个题用线段树简单维护即可。
时间复杂度
P8421 [THUPC2022 决赛] rsraogps / P9335 [Ynoi2001] 雪に咲く花
全新的视角。
离线做扫描线,将询问变成矩形,由于询问信息可差分,直接在每个位置
现在考虑怎么维护这个前缀和,考虑扫描线向右移动
时间复杂度
可供练习的题:
- P8868 [NOIP2022] 比赛
- P9747 [KDOI-06-S] 签到题
对一维分治
有些时候,我们所维护的信息不能差分,这个时候就需要祭出我们的分治。
考虑对一维度分治后,此时将一个 4-side 矩形变成两个 3-side 矩形(考虑从中间劈开,然后变成两个开口相对的 3-side 矩形),跑两边扫描线,就可以做到只有插入没有删除了。
由于分治每一次处理所有跨过分支中心的矩形,然后在向下继续分治,所以复杂度会多一个
P6109 [Ynoi2009] rprmq1
由于这个题实在是太毒瘤了,这里就不放题解了,大家自己做做就好 /dk
二维扫描线
我们发现,一维扫描线是对某个维度排序,每次查询
那啥是二维扫描线呢?套用一维扫描线的行为,我们尝试得到二维扫描线的行为:
每次询问
[l,r] 的信息,以一种方式排序,使得经过所有询问,并且复杂度可接受。
实际上就是莫队,这个可以看看 Alex_Wei 的根号算法学习笔记,这里不多赘述。
自由度
考虑一维扫描线
然而实际上普通的维护序列问题实际上也属于两个维度。
因为是按照给定的操作序列操作的,这里不乏出现时间的维度。
所以每一次操作都可以看做是一个 3-side 矩形,然后时间一维,序列一维,在时间维上做扫描线,就将二维静态问题转化为了一维动态问题。
于是这启发我们:
扫描线序列维,数据结构时间维
由于序列上的动态操作可以看做成是序列一维,时间一维。
所以在有些情况下我们直接沿着时间维度跑不好处理,这个时候我们就可以考虑在序列维度上做扫描线,然后线上维护时间维。
这种问题一般都是在线上查询一个单点的信息,当然,形势好的话查询区间信息也是可以的。
Comet OJ - Contest #14 D / P8512 [Ynoi Easy Round 2021] TEST_152
直观的看这个题似乎特别不好处理。
不妨从贡献的角度看这个题,把「经过操作序列上
这会产生两个性质:
- 某个时间产生的贡献会随着时间的流逝逐渐消失,换句话说,时间点
i 产生的贡献是不增的。 - 时间点
i 的贡献是无后效性的,他不会受到前面的时间的影响,他只会被后面的时间所影响,这样就意味着执行了[1,y] 操作和执行了[x,y] 操作后,时间点x 的贡献一致。
现在容易想到建系,横轴为操作序列维,纵轴为时间维,然后辅助维护一个执行完
考虑扫描线在横轴上跑,线上维护每个时间点对当前
现在考虑扫描线右移会发生什么,假使遇到了操作
由于在我们给
P5524 [Ynoi2012] NOIP2015 充满了希望
感觉正着扫做操作
考虑一个操作
再考虑操作
然后站在操作 vector
维护每个位置的操作
更加深入的,发现交换操作等价于「两次单点染色」,发现待覆盖的区域和一次覆盖的区域可以颜色端均摊,用 ODT 维护这个颜色端均摊就好。
综上,不难得出扫描线左移是我们需要干什么:
- 遇到操作
1 时:直接交换两个位置的vector
,ODT 上交换两个位置的颜色。 - 遇到操作
2 时:ODT 上区间染色1 ,然后对每一个颜色为0 的位置依次处理完询问,并清空vector
。 - 遇到操作
3 时:ODT 单点染色0 ,然后将当前操作编号压入对应位置的vector
中。
每次查询的时候就是一个区间和,直接树状数组维护就好,时间复杂度
P7560 [JOISC 2021 Day1] フードコート
建系,横轴为序列维,纵轴为时间维,扫描线在横轴上跑。
此时操作序列中的修改变成了横着的线,询问变成了竖着的线,横向差分一下,就变成了「单点修改、区间查询」。
思考如何做查询(也就是做操作 pop
必然都是有效的,否则的话中间必然还要再空一次,不满足「最近」这一个条件,这样查询操作就变成了在这一个区间里面二分了。
考虑如何刻画这个最近一次队列空,设 push
pop
先证明此时空了:由于我们是最大后缀和,所以这之前必定是空的,否则我们就可以通过左端点向右扩张来增大我们的后缀和。
在证明此后不空:假设
x 时刻是最大后缀和左端点,y 时刻队列空了,x \lt y ,那么必然出现了[x,y] 的和\lt 0 ,我们的最大后缀和一定不会去选择一个负数的前缀,所以该情况不可能出现。
由于会出现单点
现在我们已经找到了最近一次队列空的时间 pop
量,然后给这个 pop
量加上 pop
量的位置,然后读取这个位置的属性,即为答案,时间复杂度
当然,如果你不是很会怎么单
\log 在线段树的一个区间上二分,你可以像我一样来一个笨的方法,就是先区间查出[1, t-1] 位置上的push
和,然后把他丢进pop
量里就可以了。
别忘了还原消减操作!!!
P8518 [IOI2021] 分糖果
建系,横轴为序列维,纵轴为时间维,套用 P7560 的套路,只要我们找到了最后一次触碰上界或者下界的时刻
那么目前亟待解决的问题就是如何描述「最后一次触碰上/下界」这一个东西。
设当前正在被操作的原序列
先忽略上界,考虑什么时候会碰到下界,不难发现在时间序列的最小前缀和
假设
h 时刻被制裁,那么如果t 时刻还想被制裁,则必须要保证时间序列上t 位置上的数< -\max(0, \sum_{i=h+1}^{t-1} time_i) ,否则一定不会被制裁。这就意味着如果你选上了一段和为非负数的区间,那么必然要选其后面的那一个被制裁的地方,此时才能保证当前前缀和更小,否则一定不会选那一段区间。
现在考虑加上上界,不难发现下面这个性质:
- 若
i 时刻触碰上界,j 时刻触碰下界,则时间序列上[i,j] 区间的和一定\lt -c 。 - 若
i 时刻触碰下界,j 时刻触碰上界,则时间序列上[i,j] 区间的和一定\gt c 。
综上,得「相邻的上下界触碰事件」之间区间的和的绝对值一定
现在我们的「最后一次触碰上/下界」一定在最靠左满足这一条件的区间的左端点或者右端点,然而左端点只会有一种情况(就是有可能在触碰下界后一段或触碰上界后一段的区间和的绝对值
最后就剩怎么找我们所需要的那个区间,简单的,考虑线段树维护「前缀最小和、前缀最大和、区间和」,然后绝对值最大子段和就是「前缀最大和
时间复杂度
UOJ 515. 【UR #19】前进四
你会发现这个东西长得很像我们的楼房重建,那么自然不难想到两个
考虑离线,建系,设横轴为序列维,纵轴为时间维,考虑倒着在序列维上做扫描线,线上维护时间。
转换角度观察这个问题,发现每一次单点修改都影响了从「本次修改」到「下一次修改」的整个时间,故在线上维护取
上面这一车东西考虑做「Segment tree beats」,时间复杂度
计算几何中的扫描线
回归扫描线的本质,实际上就是拿一条线去扫平面来获得所需要的信息。
因此较为显然的,我们也可以将扫描线用在计算几何中。
通常的套路是维护扫描线与几何图形截点的位置,与几何图形下一个拐点的位置。
由于我的计算几何很菜,所以只能说两个比较简单的题目。
交点检测
给定平面上
n 条线段,输出所有交点的位置。保证输出量
\le 10^5 。
扫描线扫描
这样有了一个较好的性质,就是如果线段
那么此时维护扫描线上相邻线段什么时候发生交点,计算交点并在扫描线上交换两个线段的位置。
由于是线段,所以还需要维护线段的加入事件和删除事件。
综上,用平衡树维护一下上述操作就好了,设交点数为
平面图点定位
给定平面上
n 条直线,将平面划分为了一个平面图你需要建出这个平面图。
类比交点检测,发现如果出现了交点,则说明这个的一个区域被封闭了。
这样一直做就可以找到所有区域以及相邻区域。
设区域数为
可供练习的题:
- P3268 [JLOI2016]圆的异或并
- P5525 [Ynoi2012] WC2016 充满了失望
- P6106 [Ynoi2010] Self Adjusting Top Tree
树上扫描线
考虑在序列上的时候我们的询问会转化为
现在问题上树,不难想到将树上问题转化为序列上的问题,故进行树链剖分。
这里利用了一个树链剖分的一个非常优秀的性质:「树上的任意一条路径划分成不超过
由于这一个性质,我们将拆分出来的链分别做矩形,此时最多会出现
如果考虑路径上点之间的关系的话,则最多会出现
然后我们就在原先复杂度基础上多了一个或者两个
未知渠道获取的题
定义一个点对
(i,j)(i \not= j) 是好的当且仅当:\exist k \in N^{*},\qquad \lfloor\dfrac{i}{k}\rfloor = j 给定一棵
n 个点的树,树上点的权值互不相同,点权的值域为m ,共有q 次询问。每次询问给定一个路径
(x,y) ,问路径上多少个点对(u,v) 满足(a_u, a_v) 是好的。
考虑整除分块,预处理出所有符合条件的点对。
建系,设横纵坐标都为序列维(dfn 维),则点对变成平面上的点。
对于每个路径,预处理所有可能的询问矩形,即两个链组合在一起组成一个询问矩形,由于树链剖分的性质,不难发现矩形数是
然后现在的问题就变成一个简单的二维数点,时间复杂度
可供练习的题:
- P1600 [NOIP2016 提高组] 天天爱跑步