根号?分块?莫队?总结?
重头戏 \(\times 1\)。莫队的起名应该是因为队长叫莫涛吧。
它可以处理一些重要的贡献不独立的问题。且个人觉得有必要在分块之前讲解。
但是不要因为它是一个复杂度关于根号的算法就和分块比较。
贡献不独立 , 啥意思 ?
举一个简单的例子 , 区间内的 \(\sum\limits^{\text{数字种类个数}}_{c=1}(\sum\limits^r_{i=l} [num_i=c])^2\) , 十万询问。
在说分块之前提一下 , 这个东西可以用 \(n^{\frac 2 3}m\) 做出来。
不过莫队不止于此。
如果你看到一个数据结构题目 , 它没有复杂的修改 , 并且答案的计算方式固定 ,
每一个询问没有一个对于答案的计算方式有影响的变量 , 只会有常数。
那么这道题你可以用莫队。
如果你是暴力的话 , 那么时间复杂度莫不是 \(O(nm)\) ?
但是如果你把所有询问按照左端点第一关键字右端点第二关键字来排个序再来跑 ,
复杂度就是 \(O(\text{玄学})\) 了。(定义 \(\text{玄学} \leq nm\))
但是很容易想到这样子是可以被卡到 \(nm\) 的。
莫队的意思是 , 你给 \(l\) 标一个块 , 按照 \(l\) 所在的块为第一关键字和 \(r\) 为第二关键字排序 ,
那么你的复杂度就变成了 \(O(nm^{\frac 1 2})\)。
我们来算一下 , 假设块里面有 \(k\) 个询问 , 那么移动的的总量其实是 \(\frac {n^2} k+mk\)。
要求两者平均 , 列个方程 , 解出来 \(k=m^{\frac 1 2}\)。 所以总的复杂度就是 \(O(nm^{\frac1 2})\)。
所以说一般情况下 \(n\) 越小 \(m\) 越大莫队其实越快。
我们算复杂度的时候要把莫队的关键字弄进去。
莫队可以看成是排序后用将关键字加一或者减一的方式来达到你的询问的一种状态 , 再每一次移动某一个关键字的时候顺便记录一下这个询问到答案 , 并且使移动次数相对少的算法。这是我玩了怎么多年的总结。
所以说不要进入两个误区 : 一是询问的时候如果有对答案有影响的参数 (那么这道题就不是莫队)。二是你可能认为用多个桶来存答案来提高复杂度 , 这个是不行的 ! 莫队只能保存现有的一个状态 !
对于第二个误区 , 举个例子 , 考虑二维的莫队 , 矩阵 \(n \times n\) ,
如果我把询问拆成一维 , 行吗 ? 可以啊!
每一个二维的询问我拆成很多个一维的询问 , 跑完以后每个询问开数组进行合并。
空间不够 ? 那么我就把询问分成几组 , 一组一组搞。
好像是正确的 , 复杂度算出来是 \(n^2m^{\frac 2 3}\) , 看起来很快。
但是我们可以清晰的发现这样子是不对的 ,
我们无法同时转移多个数组 !
说起二维莫队 , 我就来讲讲莫队复杂度的规律。
通常情况下 , 你的莫队有 \(k\) 个关键字的话 (就像带修莫队有 \(3\) 个关键字) :
你的复杂度就是 \(O(\text{可移动位置个数} \times \text{询问}^{\frac{k-1} k})\) , 简单写就是 \(O(nm^{1-k^{-1}})\)。
这时候你的块就是 \(\frac{n}{m^{k^{-1}}}\)。
对于通常情况下的 \(k\) 维莫队 , 可以认为它的复杂度是 \(O(n^km^{\frac {2k-1} {2k}})\) ,
所以说二维莫队的复杂度就是 \(O(n^2m^{\frac 3 4})\)。是不是很慢。
其实还好 , 当然我们可以认为 \(k\) 维分块的复杂度是 \(O(n^{\frac {k-1}{k}}m)\)。
莫队的复杂度这样定义以后不会再因为其他数据结构的加入而更改 ,
所以一般情况下我们不会用莫队套用 \(\log\) 数据结构 , 复杂度太高。
有时候如果我们不支持加入或删除操作 , 比如区间众数 ,
我们可以用回滚莫队 , 比较简单 , 自己可以网上查阅。
当然我们可以套一个分块进去 , 因为分块的修改可以达到 \(O(1)\) , 而查询通常可以达到 \(n^{\frac 1 2}\) , 这样子复杂度就比较小 \(O(n^{\frac 1 2}m+nm^{\frac 1 2})\)。
莫队还有一个骚操作 , 就是二次离线莫队了。
当然我还不是全部都会 , 比如我不知道如何将空间变为 \(O(m)\)。留坑。
对于一些数对的题目 , 可以用这个东西 , 比如查询区间逆序对。
你会发现其实每一次的移动的贡献就是查询某个区间比我小或者比我大的东西 ,
而这个贡献其实是可以差分的 , 但是直接做的话又变成 \(nm^{\frac 1 2} \log n\) 了……
所以我们可以把这些询问存下来 , 再次离线处理这 \(nm^{\frac 1 2}\) 询问。 我们只需要用分块或者其它东西 ,
满足这些询问在一起处理的时候达到 \(O(1)\) 的复杂度即可。
时空复杂度都是 \(O(nm^{\frac 1 2})\)。
\(\text{upd : 2019-12-19}\)
先顺带一提 , 我第一次看懂二次离线莫队的博客是 ZZQ 的博客。
巨佬 alpha1022 给我讲了 \(n+m\) 空间的做法。
下方的箭头有很多意义 , 有 到 和 对 的意思。
首先我们考虑从 \([l,r] \implies [l,r+k]\) , 这些询问是不是有一些特点。
假如一个 \(i \in (l,r]\) , 那么每一个的贡献其实就是 \(num_i \implies [l,i)\)
套路差分一下就是 \(num_i \implies [1,i)-num_i \implies [1,l)\) 了。
\(num_i \implies [1,i)\) 是可以预处理的 , 因为右端点都是 \(i\)。
左端点看起来不能处理 , 但是会发现每一个询问的转移 \(l\) 是不会变的。
所以说我们的空间复杂度就变成了 \(O(n+m)\)。
但是这样子好像不那么严谨 , 我继续写出来所有的转移供打码。
为了方便我还是写只有 \(\pm1\) 的情况。
\([l,r] \implies [l,r+1]\) : \(+num_{r+1} \implies [1,r]-[1,l-1]\)
\([l,r] \implies [l,r-1]\) : \(-num_r \implies [1,r-1]-[1,l-1]\)
\([l,r] \implies [l+1,r]\) : \(-num_l \implies -[1,l]+[1,r]\)
\([l,r] \implies [l-1,r]\) : \(+num_{l-1} \implies -[1,l-1]+[1,r]\)
注意一下我们把第一种情况都写在前面 , 第二种都是后面。
\(\text{upd : 2019-12-27}\)
我自己出了一道题目 , 给教堂拿去比赛了。不过没人切。
题目描述 : \(\sum\limits_{1 \leq i,j \leq n}[num_i \mod num_j=k]\) ,
我们其实要考虑到 , 如果 \(num_i=k\) 且 \(num_j > k\) 的话也是有贡献的,
所以我们不妨把这个东西拆成两种贡献的数对 ,
第一种贡献叫做被膜为 \(k\) , 第二种贡献为膜你为 \(k\) 即可。
Link
重头戏 \(\times 2\)。分块是一种数据结构 , 也是一种思想。
具体的可以看国家集训队的论文 , 那个还是挺有用的。
分块的作用有三个 ,
一 . 将某种操作变成极快 , 把相对的操作变为较快。如查询 \(O(1)\) , 修改 \(O(n^{\frac 1 2})\)。
二 . 将某些东西简单化快速化 , 比如分段打表 , 我写的平衡树优化。
三 . 将某些经典数据结构做不了的东西变为可行。
分块还可以套各种东西 ,
只有你觉得可以 , 分块就可以。
用处一 ,
比如在上面的莫队那道题中 , 我们用到了修改为 \(O(1)\) 的分块。
权值分块等可以在 \(O(1)\) 插入的限制内做很多查询为 \(O(n^{\frac 1 2})\) 的东西。
那什么时候查询是 \(O(1)\) 呢? 可以参考 OI wiki。
具体的话可以看一道叫作业的题目。
\(\text{upd : 2019-12-27}\)
我自己出了一道题目 , 给教堂拿去比赛了。不过也没人切。Link。
题目描述 : \(\max\limits_{l \leq i,j \leq r\ ,\ num_i \not = num_j ,\ \text A(num_i)-\text A(num_j) \leq K} \text A(num_i)\) 。\(\text A(T)=\sum\limits^n_{i=1} [T=num_i]\) 。
这道题题面看起来挺吓人 , 其实很简单。
考虑到只有其它的数字的出现次数跟自己的相减小于 \(K\) 的时候 , 有贡献
所以我们就可以维护一个出现次数的数组 ,
再维护一个出现次数再我和我 \(-K\) 的数的个数的数组 , 这个东西维护 \(O(1)\)。
这样子的话用分块进行维护 \(O(1)\) 就可以了啊 , 查询 \(O(n^{\frac 1 2})\)
具体方法是将上面两个数组相乘 , 如果不是 \(0\) 就是有贡献 ,
这个时候用分块去求最大值即可 , 回滚莫队也行 , 不过麻烦。
那我们还是要说说为什么有些时候我们的分块查询和修改都不是 \(O(1)\) ,
简单的说 , 就是要求查询和修改的复杂度平均了 , 也就是某一个操作太复杂 ,
比如说 , 我查询这个东西其实是要前缀和的,
那么我们的复杂度就上去了。
用处二 ,
那么我就举上面说的两个例子 :
第一个例子 , 虽然这种方法很逊而且不实用 , 但是我还是讲讲。
众所周知 \(\text{FHQ Treap}\) 是一个常数及其大的算法。
但是我们可以进行分块 , 数据大的时候 , 我们可以做到期望复杂度 \(O(\log \sqrt{n})\)。
其实我们就是开一个线段树 , 对每一段权值进行分块 , 每一段权值的总信息交给线段树维护 , 每一段权值的分信息交给 \(\text{Treap}\) 维护即可。
这样子的话操作的复杂度就变为了 \(O(\log k+\log \left\lfloor\frac{n}{k}\right\rfloor)\) (\(k\) 为块的数量)
当然代码很长 , 而且容易被卡到 \(O(\log n)\) , 所以我说是对“常数”的优化。
这个东西的广义版就更强了 , 应该就是 \(\text{Sqrt tree}\) , 然而我不会。
奇技淫巧 ? 看下一个 :
第二个例子 , 题目是求 \(T=10^{11}\) 内的质数个数 , 正解洲阁筛暴毙 ,
本人没有实际做过这道题 , 所以这里是不标准的浅谈。
所以我们分段打表即可 , 我们考虑对预处理一个 \([1,\left\lfloor\frac{T}{k}\right\rfloor \times k]\) 的表 (\(k\) 为块的数量)
那么我们存储的空间为 \(O(k)\) , 查询的时间复杂度为 \(O(\left\lfloor\frac{T}{k}\right\rfloor)\)。
那么显然 \(k\) 可以设置成 \(k=n^{\frac 1 2}\) , \(10^{5.5}\)。
由于我们每次查询一个值其实是可以用 \(\text{Miller}\) 来求的 , 假如这种算法的复杂度为 \(30 \log n\) ,
那么这个算法的复杂度就将是 \(3 \times 10^{6.5} \log 10^{11}\)。
会不会卡不过去啊 , 其实就是卡不过去 , 这样子只能骗分。
我们不妨比较精确的列一个方程 , 如 \(3 \times \left\lfloor\frac{T}{k}\right\rfloor \log 10^{11}=k\) , 当然不用准确求出。
好奇的算一下 \(\left\lfloor\frac{T}{k}\right\rfloor=\left\lfloor\frac{k}{30}\right\rfloor\) , \(k=(30T)^{\frac 1 2}\)。
这样子我们的空间加了 \(\sqrt{30}\) 倍 , 时间复杂度降了 \(\sqrt{30}\) 倍。
上述办法可能还是会被卡 , 其实可以将 \(k\) 直接调成 \(10^7\) ,
这样子复杂度就只有 \(3 \times 10^5 \log 10^{11}\) 了!
于是这种复杂度就变成了 \(O(\frac T k \log T)\) , 常数 \(30\)。空间 \(O(k)\)。
洲阁筛复杂度 \(O(\frac{T^{\frac{3}{4}}}{\ln T})\) , 空间 \(O(n^{\frac 1 2})\)。
用处三 ,
分块的形式千变万化。
查询区间第 \(k\) 小。
正解的话最快的是划分树 , 其次是主席树 , 时间复杂度都是 \(O(m \log n)\) 的。
分块的话我们可以考虑权值分块+区间分块 ,
前面不是说过主席树维护前缀和吗? 那我分块也效仿 ,
设 \(\text{Recf}[l,r]\) 为 \([1,l]\) 区间中数值在第 \(r\) 块的个数 ,
再来一个 \(\text{Times}[l,r]\) 为 \([1,l]\) 区间中数值为 \(r\) 的个数。
查询的时候直接差分一下跑分块即可。时间复杂度 \(O(mn^{\frac 1 2})\) , 空间 \(O(n^{\frac 3 2 })\)。
但是不要以为分块只是为了把经典数据结构思路简单化了。
能不能考虑如果区间第 \(k\) 小带单点修改呢 ? 难道我们要用 \(n^{\frac 1 2} \log n\) 的二分 ?
考虑我们的查询复杂度是 \(O(k \log n)\) , 修改是 \(O(\left\lfloor\frac{n}{k}\right\rfloor)\) , 我们列一个方程。
最后可以解出 , \(k=\left\lfloor\frac{n}{\log n}\right\rfloor^{\frac 1 2}\) , 那么复杂度就变为 \(n^{\frac 1 2} \log^{\frac 1 2} n\) , 人们喜欢写成 \(\sqrt{n \log n}\)。
有一些经典的问题 , 比如区间众数。
\(\text{lxl}\) 跟我说如果有人用主席树做出来了那个人可以得图灵奖。
我们把这个数列分成 \(\frac n k\) 块 , 每个块有 \(k\) 个数。
然后我们预处理一个 \(\text{Ans}[l,r]\) 代表 \(l\) 块到 \(r\) 块的答案 , \(\text{Times}[l,x]\) 代表 \(1\) 到 \(r\) 块数字为 \(x\) 的个数。
这样子的话 , 我们每一次查询 , 就已经有了一个整块的答案 ,
然后在散块那里去更新看有没有比这个众数的出现次数更多的。
时间复杂度 \(O(mk)\) , 空间复杂度 \(O(\frac{n^2}{k})\)。两者可达最优 , 为 \(O(nm^{\frac 1 2})\)。
最最最经典的问题 , 莫过于树套树了。主要操作可以看二逼平衡树。
我们设一个 \(\text{Times}[i,j]\) 为在第 \(i\) 块,权值为 \(j\) 的个数。设 \(\text{Fock[i,j]}\) 代表 \(1\) 到第 \(i\) 块权值为 \(j\) 的个数。
设 \(\text{Block}[i,j]\) 代表 \(1\) 到第 \(i\) 块,权值在第 \(j\) 块的个数。查询的时候我们直接搞一搞 , 修改我们也搞一搞就好了。
查询 \(k\) 的排名 , 只需要对边角暴力一下 , 然后用 \(\text{Block}\) 统计答案 (要对本块再次进行暴力)。
查询排名为 \(k\) 的数字,我们再次把边角加起来 , 然后同 \(\text{Block}\) 一起统计答案。
单点修改 , 我们把 \(\text{Times}\) 改一下 , 然后对后面的 \(\text{Fork}\) 和 \(\text{Block}\) 进行实时维护。
查 \(k\) 的前继,我们把边角加起来 , 然后看 \(k\) 这个块有没有前继 , 没有用 \(\text{Block}\) 往前跳 , 后继同理。
这题 , 未来日记。比较好玩。
如果有人将此题的数据卡 \(\text{std}\) 的复杂度到 \(O(mn^{\frac 1 2} \log _{1+\frac n m}n)\) , 那么他巨。
首先我们设一个 \(\text{Times}[i,j]\) 为第 \(i\) 块,权值为 \(j\) 的个数。
设 \(\text{Fock}[i,j]\) 代表 \(1\) 到第 \(i\) 块权值为 \(j\) 的个数。设 \(\text{Block}[i,j]\) 代表 \(1\)~\(i\) 块 , 权值在第 \(j\) 块的个数。
满足 \(\text{Times}[i,j]\) 实时更新, 且一次我们只触及 \(x,y\) 两个数,等于单点修改 , 所以只需要 \(n^{\frac 1 2}\) 的时间就可以更新 \(\text{Fock}_{i,j}\) , \(\text{Block}_{i,j}\) 同理。
其次 , 我们对大块进行修改的时候,直接一个并查集即可。
不过我们会发现 , 对于残余的块修改的时候 , 我们可能会违背这个块的并查集。所以对残余块修改时重构一下这个块的并查集。
其它题解上面说只重构 \(x,y\),常数减。
然后就是一些关于位置的问题。
每次修改关于区间 \([l,r]\) , 将 \(num_l\) 放在 \(num_r\) 的位置上 , \((l,r]\) 向左平移。
每次查询是一个区间某个数字的出现次数 , \(n,m=10^5\)。
考虑分块 , 如果每一个块都开一个队列的话 , 并且实时记录一下这个块的出现次数的话,
就可以做到修改查询 \(O(n^{\frac 1 2})\)。
还有一道题 , 天降之物。
因为这道题我并没有 \(\text{A}\) 且我的方法会被卡 (应该)。 所以我就随便说说。
由于答案只能由左右两边的点与自己构成 , 意思就是一个 \(x\) 和一个 \(y\),
如果它们之间存在着 \(x_1\),那么对答案产生贡献的就只有 \((x_1,y)\) 而不可能是 \((x,y)\) , \(\text{Dis}(x,y)\) 一定大于 \(\text{Dis}(x_1,y)\)。
考虑分块,记录一个 \(\text{Ans}[i,x,y]\) 代表第 \(i\) 块 \(x\) 和 \(y\) 的答案 , 动态空间可以做到 \(n^{\frac 3 2}\)。
然后记录一个 \(\text{Dis}[i,x,0/1]\) 代表第 \(i\) 块最左边 (或者右边) 离块的边缘的距离是多少。
前者先记录单独每一个块的答案,后者用来记录跨块的答案贡献。当然 , 如果你觉得自己的空间写得十分的小的话,你可以尝试搞两个分块,那就不用后者了。
但是考虑到 \(\text{Ans}[i,x,y]\) 的空间需要动态 , 所以需要一个 \(\text{Num}[i,x]\) 记录第 \(i\) 块 \(x\) 的编号,然后还要开一个队列来维护这些编号 , 空间常数巨大。

浙公网安备 33010602011771号