sqrt-data-structure

根号数据结构

本文只是对 nzhtl1477 的课件内例题写写自己的理解,侵删。


最基础的动态分块

1.区间加,区间求小于 x 的数的个数

每块内维护 OV(有序表),整块查询 lower_bound,散块暴力查询。

修改的话散块可以重构,或者你注意到修改的位置在 OV 里拿出来也是有序的,归并一下也行。

整块直接打标记即可,查询记得带上标记。块长取 nlogn 时复杂度 O(mnlogn)

2.(Ynoi2017 由乃打扑克)区间加,区间求 k 小值

k 小值的话可以二分,然后就变成了求区间小于 x 的数的个数,就是上面那个东西。

然而直接 O(mlognnlogn) 不够优秀。

设块长为 B,修改显然是 O(B+nB) 的,每次二分的整块询问是 O(nBlogn) 的。

问题在于二分内的散块询问,是 O(B) 的。

我们考虑将两个散块归并到一起,变成一个块,和整块一起询问。这样全部复杂度就是 O(B+nB+nBlog2n)

B=nlogn 有最优复杂度 O(nnlogn)


根号平衡

3.O(1) 单点修改,O(n) 求区间和。

维护整块内和即可。

4.O(n) 单点修改,O(1) 求区间和。

维护块内前 x 个数的和以及前 x 个块的和,求区间和用两个前缀和减一下就好了。

5.O(n) 区间加,O(1) 求单点值。

每块维护标记就好了。

6.O(1) 区间加,O(n) 求单点值。

原数组差分一下就变成了上面一个。

7.集合,O(1) 插入,O(n)k 小值。

可以假设值域 O(n)(离散化)

考虑值域分块,维护每个块内数的个数,求 k 小值直接暴力跳块即可。

8.集合,O(n) 插入,O(1)k 小值。

考虑集合排个序然后序列分块,插入一个数先跳块找到它插入的地方。

插入时会导致后面的数后移一位,你发现只有 n 个数改变了它所属的块(从这个块的末尾到下个块的开头)。

那么块内用双端队列维护,查询找到对应的块查对应下标(双端队列支持随机访问)。

9.(CodeChef Chef and Churu)

长为 n 的序列,给定 m 个函数,每个函数为序列中第 li 到第 ri 个数的和。

支持单点修改序列,求区间的函数值的和。

对函数序列分块,块内存函数值的和。散块再对原序列分块,O(1) 查询区间和,查询就是 O(n) 的了。

修改考虑这个 x 对所有函数的贡献,枚举每个函数块,我们需要知道这个块内有多少个函数包含 x

这个简单,每个块内预处理,用线段树或者分块给 [li,ri]1 就好了。

那么就搞定了,时空复杂度 O(nn),也许可以离线做到线性空间。

10.(P3863 序列)

长度为 n 的序列,m 次操作,支持:区间加 k,求 ap 在过去多少时间内不小于 y,时间随着操作流逝。

对序列扫描线,从 1 扫到 n,遇到一个区间修改的 l 就把它的时间到 m 全部加上 k,遇到 r+1 就减。

查询就是求时间 [1,t] 内不小于 y 的个数,也就是例题 1

复杂度 O(nmlogm)


简单莫队算法

11.求区间每个数出现次数平方和。

直接莫队,平方和展开来,维护出现次数的数组。

12.(AHOI2013 作业)查询区间中值在 [a,b] 内的不同数个数。

莫队,维护出现次数数组和一个值为 0/1 的值域分块。

如果出现次数变为 0 了或者从 0 变为非 0 了,就修改分块中对应位置。

查询直接求区间和,用例 3 的方法即可平衡复杂度。

13.(Ynoi2016 这是我自己的发明)给一棵树,支持换根,求 x,y 子树内各选一个点点权相等的点对数。

子树变成 dfn 区间上的操作,换根就是可能把 dfn 反转,可以不用管。

那么就是求两个区间内相等的点对数。考虑差分:

F(l1,r1,l2,r2)=F(1,r1,1,r2)F(1,l11,1,r2)F(1,r1,1,l21)+F(1,l11,1,l21)

这样每一个询问只有两个维度,可以莫队了。直接维护两个桶即可。

14.(BZOJ3920 Yunna的礼物)求区间中出现次数 k1 小的数中第 k2 小的。值域大。

莫队,出现次数 k1 小显然可以直接维护一个值域分块,找到这个出现次数。

然后介绍一个科技:高维离散化。我们对于序列中出现过的所有数,如果它在序列中出现了 y 次,

我们对每个次数维护一个 vector,在次数 1y 对应的 vector 里全部塞一个 x

对于每个 vector 我们分别做离散化,这样我们得到了每个数在每种出现次数的离散化值。

我们在莫队时对每个出现次数维护一个值域分块,这个出现次数每新增一个数就把它的离散化值加进值域分块。

这样查询时就可以在对应值域分块里找 k 小值了。

当然也可以把所有离散化值串起来然后串起来维护值域分块,似乎更好写。

15.(BZOJ4241 历史研究)求区间中数值乘上出现次数的最大值。

可以回滚莫队,也可以不回滚莫队。

答案只可能是一个数乘上它的出现次数(废话)

把这些值 (x,2x,3x,,x×y) 放在一起离散化,那么就变成了插入,求最大值。

用值域分块维护即可。

16.(Ynoi2015 盼君勿忘)每次给模数,求区间所有子序列,将相同值去重后的和的和。

对于一个数 x,如果它在区间中出现了 k 次,区间长度为 len,它对这次询问的贡献是 x2lenk(2k1)

2lenk 表示除了 x 的数随便选,2k1 表示所有 x 至少要选 1 个。

贡献可以拆成 x2lenx2lenk。第一部分每次做快速幂就好了。

考虑第二部分:对于第二部分,考虑一个结论:不同的出现次数最多只有 n 种。

那么对于每种出现次数分别统计 x 的和就好了,查询直接 O(n) 查询。

为了做到 O(1) 快速幂,要每次根号预处理光速幂。

17.(HNOI2016 大数)数字串,每次求区间中有多少子串是质数 p 的倍数。p 一开始给定。

子串可以差分一下,后缀相减再除掉一个 10 的幂。

p=2,5 特判,然后相当于统计有多少个后缀模 p 相等。直接莫队即可。

18.求区间众数。

莫队,维护出现次数数组。加入直接加并更新最大值就好了。

为了支持删除,维护一下每个出现次数的数的数量就好了。

19.求区间有多少个子区间,满足区间内所有数出现次数为偶数,值域 26

把每个数 x 变成 2x,题目条件等价于子区间异或和的 popcount=0/1

区间异或和变成前缀异或和的异或,再莫队,每次枚举异或和(只有 27 个)就好了。

20.求区间逆序对个数。

莫队,变成求区间内有多少个数小于 x

由于每次转移都要查询,不能直接值域分块。

考虑差分,变成 [1,r] 内小于 x 的数个数减去 [1,l) 内小于 x 的数个数。

你可能会考虑主席树,但是这里用主席树复杂度会坏。那么用一个叫可持久化分块的东西就好了。

upd:忘记写二次离线的 sol 了

这题还有一种做法。仍然是莫队,假设向右转移,每次转移即要询问某个区间 [l,r] 大于 ar 的数的个数。

这个信息可以差分,差分成 [1,r] 中大于 ar 的个数减去 [1,l1] 中大于 ar 的个数。

前者平凡,考虑后者。我们可以扫描线扫 l1,把所有的 r 塞到 l1 里,用值域分块维护,O(n) 次修改 O(nn) 次查询。

现在空间是带根号的。我们可以优化:你注意我们只处理右端点的移动,每段移动中 l 不变 r 连续。

那么没必要把 r 一个一个存进去,存下移动的区间即可。

当然你要用同样的方法处理向左移动的部分。复杂度 O(nn)。这就是二次离线莫队。

21.求区间内 |aiaj| 的最小值。

有 polylog 做法(刚考完),这里只说根号做法。

考虑莫队,那么就是要插入删除一个数,求前驱后继。

这个如果用平衡树之类的复杂度会带 log,并不是我们想要的。

考虑回滚莫队,变成只删除的,用值域链表维护,值域分块维护答案。

用根号查询最小值,O(1) 修改的值域分块即可。

本质上就是把插入改成了撤销删除,这个是可以 O(1) 的,而前者不可以。

22.求区间中值相同的位置差最大值。

回滚莫队,变成只加入。那么维护每个值最左端和最右端的位置即可。

撤销要记录之前的位置,一直跳过去。

23.(BZOJ4358 permu)排列,求区间中最长的值域区间,满足值域区间内数都在区间内出现过。

莫队,对于每个值域连续段,在左右端点分别记录它的另一个端点。

这个难以删除,那回滚成只加入的,插入时就是可能将两段合并成一段。直接维护答案就可以了。

24.(Cnoi2019 数字游戏)排列,求区间 [l,r] 有多少子区间的值都在 [x,y] 内。

对值域区间莫队,这样相当于单点修改 0/1,求区间中有多少个子区间全是 1

维护每个极长 1 段,这段对答案的贡献就是 len(len+1)2

如何维护极长 1 段?可以用上一题的方法,也可以搞个分块维护。


静态分块

一般来说,能用静态分块做的莫队都能做,但是静态分块可以解决强制在线的题。

25.求区间众数,强制在线。

合并众数的话有一个性质:将集合 B 中元素加入集合 A,新集合的众数要么是 A 原先众数,要么是 B 中某个数。

如果 B 较小,我们可以枚举它里面的数,检验每个数是否成为众数。

回到这题,对序列分块,预处理每两块之间的众数。

查询时,整块已经预处理(即为集合 A),我们枚举散块(集合 B)内的数检验它是否成为众数。

这里要快速查询区间内某个数出现次数,你可能又会考虑主席树,可是复杂度会坏。老样子使用可持久化分块即可。

其实也可以维护每个数在每个块中出现次数的前缀和,查询时差分即可。

但是!以上方法空间都是 O(nn) 的,不够优秀。

想一想,我们现在要求的是散块中每个数出现次数是否大于当前众数出现次数。

那么每个值开一个 vector 存它的出现下标,每个下标存它所在 vector 中的位置。

那么如果当前众数出现次数为 k,若散块中某个数 x 向后的 k 个相同数都在 [l,r] 中,x 即为新的众数。

(因为 x 出现了至少 k+1 次。)

你发现向后找第 k 个位置是 O(1) 的,这个做法就是时间 O(nn),线性空间的!

26.求区间逆序对,强制在线。

贡献拆成整块对整块,散块对散块,整块对散块。

整块对整块:预处理,设 fl,r 表示 lr 块中逆序对数。

可以容斥得到:fl,r=fl+1,r+fl,r1fl+1,r1+gl,r

其中 gl,r 表示块 l,块 r 两个块之间逆序对个数(不含两块内部贡献)。

g 可以归并预处理,这部分复杂度为 O(nn)

散块对整块:直接预处理每块某一段前(后)缀和前 i 个块的逆序对个数,询问时差分。

怎么预处理呢?先求出前(后)缀和第 i 个块的贡献,这个可以归并后 O(1) 拓展,最后求一遍前缀和即可。

散块对散块:两端散块内部的贡献可以先预处理,两个散块之间的贡献可以归并。

最后时空复杂度都是 O(nn)

27.求区间内 |aiaj| 的最小值,强制在线。

同样拆成三个部分。

散块对散块:归并处理。

散块对整块:预处理 fi,j 表示 i 到它所在块的末尾与第 i+1 个块到第 j 个块的答案,

gi,j 表示 i 所在块的开头到 i 与第 j 个块到第 i1 个块的答案。

如下图,表示 f 预处理的东西:

如何预处理?和上一题类似,归并后用双指针,可以做到均摊 O(1) 拓展。

剩下的整块对整块同样是预处理,这里可以直接利用 f,g 进行递推。

复杂度依旧是 O(nn)


根号分治

28.无向图有点权,支持单点加,求某点相邻点的权值和。

对度数根号分治:询问如果是度数小于根号的点直接暴力,否则我们预先存下所有度数大于根号的点的权值。

修改的时候,枚举所有度数大于根号的点,如果与这个点相邻就修改以下这个大点记录的权值和。

复杂度线性根号。

29.求全局最小的 |ij| 满足 ai=x,aj=y

对颜色集合大小根号分治,一开始预处理每个大颜色和所有其他颜色的询问答案。

如何预处理?考虑值域分块,将这这种大颜色的所有点放进值域分块里,

查询就是求 x 最小和 x 最大的数,容易处理。

如果询问的有一边是大集合我们直接输出预处理的东西,否则暴力归并一下即可。

复杂度还是根号。

30.(IOI2009)树,带点权 a。求有多少对 (u,v) 满足 uv 的祖先且 au=x,av=y

还是对颜色集合大小根号分治。uv 的祖先等价于 dfnv[dfnu,dfnu+sizu1]

对于每个大于根号的集合预处理它作为 au 以及 av 时到所有其他集合的答案。

预处理方法类似,现在变成了 O(n) 次单点加,O(nn) 次区间查询;

或者 O(nn) 次单点加,O(n) 次区间查询。用值域分块即可解决。

小集合到小集合的询问,可以考虑用类似的方法,把他们按 dfn 归并排序,有 O(n) 次单点修改,O(n) 次区间查询。随便搞个前缀和什么的都可以。

31.(SHOI2006)集合,支持加数,求 max{xmody|xS}。值域小。

y 根号分治。对于小的 y 记录答案,修改就直接一个个改。

对于大的 yy 的倍数个数比较小。我们枚举 ky,要查询 [ky,(k+1)y) 中最小的 x

容易用值域分块处理。修改 O(n),询问 O(1)

32.(Ynoi2015 此时此刻的光辉)求区间乘积的约数个数。

莫队,维护一下每个质数的指数和,加 1 乘起来。

这样会叫。每个数大概有 10 个不同质因子,每次转移要搞 10 下,过不去。

考虑设定阈值 B,对于每个小于 B 的质因子,做点前缀和就可以 O(1) 查询指数和。

大于 B 的怎么办?还是莫队,但是这次要处理的变少了,只有 logBV1 个。

没写,据说取 B=1000 能过。来点 PR 分解质因数,此时复杂度是 O(nV14+nBlogB+2nn)

33.(Ynoi2018 未来日记)区间 x 变成 y,区间 k 小值,值域与 n 同阶。

值域小,考虑值域分块。每次查询我们枚举一个值域块,试图 O(1) 得到:

  • 序列上 [l,r] 内数在这个值域块内的数的个数。

  • 以及 [l,r] 内等于 x 的数的个数。

再序列分块,序列上的散块可以每次拉出来搞点桶。整块,考虑维护

bi,j 表示序列前 i 块内在值域块 j 内的个数,ci,j 表示序列前 i 块内 =j 的个数。每次查询就拿两个前缀和相减。

考虑修改。对于某一块,把 x 修改成 y,如果原先 y 就存在,会导致块内颜色数减少 1

每块的颜色数只有可能散块修改的时候加 1。那么每次如果 y 存在就重构此块,此块总的重构次数不超过根号次。

重构复杂度显然是根号。那么这部分的复杂度就是 O(nn)

如果原先 y 不存在呢?简单搞点映射关系就好了。

散块修改直接重构就行。复杂度 O(nn)

posted @   iorit  阅读(28)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 25岁的心里话
· 按钮权限的设计及实现
点击右上角即可分享
微信分享提示