【学习笔记】根号算法
1.分块
最基础的就是区间修改查询。
我们把整个序列割成 \(s\) 个块,则块长为 \(\frac{n}{s}\),对于一个跨越区间 \([l,r]\) 的修改/询问,很容易看出它最多包含两个散块,然后中间有一堆整块。
考虑对于整块我们类似线段树的维护方法打 tag,然后对于散块 直接暴力。
分析复杂度,最多有 \(s\) 个块,散块暴力大概要 \(\frac{n}{s}\) 次,则总复杂度是 \(O(q\times(s+\frac{n}{s}))\)。
由基本不等式可得 \(min_{s+\frac{n}{s}}=2\sqrt n\)。
所以当 \(s\) 取根号时复杂度最优,为 \(O(q\sqrt n)\)。
这就是基本分块了,但是对于一些 毒瘤 题可能要玄学修改块长。
很多时候分块用于处理线段树不好处理的问题(比如交换两个数的位置)或者用来平衡复杂度(线段树树状数组固定都是 \(\log n\),但是某些情况下分块支持 \(O(1)\) 修改或查询)。
2.莫队
- 普通莫队
对于一些区间询问,我们可以考虑维护端点,然后一个一个移到规定的询问区间,在这中间进行 \(O(1)\) 的修改。(尽量做到 \(O(1)\) 不然要炸)
很显然如果按照输入顺序搞会卡到 \(n^2\)。
我们考虑把询问离线下来,然后 按操作左右端点分块。具体地,如果左端点 所在的块 不同就按左端点排,如果左端点相同就按右端点排。
然后重新根据上面的思路进行操作,就结束了。
考虑复杂度为什么是对的。
首先修改操作是 \(O(1)\) 的。然后对于每一块,左端点只会在块内移动,那么左端点移动是 \(O(块内询问数\times 块长)\) 的,总共是 \(O(块长 \times q)\)。然后对于右端点,在同一块里右端点严格不往左移,那么在一个块里右端点的移动是 \(O(n)\) 的。
然后转到另一个块,用不超过 \(O(n)\) 的复杂度从当前右端点转回去。
所以总共是 \(O(块数\times n)\)。
那么我们要让 \(块长\times q\) 和 \(块数 \times n\) 基本均衡,很容易想到分块中经典的设成根号。
好像有人说莫队正确块长应该取 \(\frac{n}{\sqrt q}+1\),这边也不是很清楚,不过被卡常了可以试试?
一个比较显然的优化是将右端点进行奇偶性排序,也就是如果是块的编号奇偶不同就从大到小/从小到大。这样子我们可以在路上顺便一起处理了。
看不懂就记板子。
inline bool operator <(const QQr &stmp)const{
if(b[ql]!=b[stmp.ql])return b[ql]<b[stmp.ql];
return (b[ql]&1?qr<stmp.qr:qr>stmp.qr);
}
- 带修莫队
考虑再开一个时间维度。询问时把时间一起调。块长要设成 \(n^{\frac{2}{3}}\)。
有一个很好玩的 trick 是修改值直接把待改的和当前的值交换就好,因为如果要往回退时间维度肯定会调回来然后这两个值就会被换回来。
- 回滚莫队
对于一些增加删除只能支持一种的莫队,我们可以考虑回滚。
显然这道题很难进行删除操作。这一次我们对于每个块单独进行处理。
先把左右端点在同一块的询问暴力掉。复杂度 \(\sqrt n\)。
然后假设我们的左端点固定,只看右端点,我们会发现右端点一直在往右走,所以右端点对于每一个块仍然是 \(O(n)\) 的,所以总复杂度仍然是 \(O(n\sqrt n)\)。
但是我们没有考虑的左端点呢?
我们对于每一个询问都单独进行左端点的添加。即,记录不管左端点(左端点在这个块的最右端的右端)时当前答案,然后左端点开始加点,得到答案,把左端点对一些数组的改动回滚回来,把答案重新回滚回左端点移动前的答案。因为左端点每次最多移动 \(\sqrt n\) 次,所以复杂度仍然能够保证正确。
- 树上莫队
如果是 dfs 序,就直接普通莫队做了。
- 欧拉序
dfs 序在退出的时候不会记录,但是欧拉序会记录,也就是说每个节点在序列中出现两次,扫到的时候出现一次 \(l_x\),退出的时候出现一次 \(r_x\)。
推荐自己画一棵树然后求欧拉序捏。
我们很容易知道在欧拉序上一段区间出现过两次的点并不在我们的询问路径上。
对于在欧拉序上的点 \(x,y\),如果他们的 lca 是他们其中之一,那么询问的区间就是 \([l_x,l_y]\)。
否则是 \([r_x,l_y]\),因为 \([l_x,r_x]\) 这一段是没用的。
然后如果是第二种情况要添加上 \(lca(x,y)\)。原因是只有在子树递归完后才会在欧拉序上出现,而 lca 此时是没有递归完子树的,但是 lca 又一定在路径上,所以加上。
- 二次离线莫队
在一些莫队问题中若更新答案的复杂度 \(t(n)>O(1)\),那么莫队的复杂度是 \(O(n\sqrt n\times t(n))\)即使是 \(\log\) 也很难保证不超时。二次离线莫队可以解决其中部分问题,当且仅当对于 \(f(x,l,r)\) 表示 \(x\) 对区间 \([l,r]\) 的贡献时,\(f(x,l,r)=f(x,1,r)-f(x,1,l-1)\),即满足可减性。
同时一般情况下 \(f(x,1,x)=f(x,1,x-1)\),毕竟大多数题目不会允许一个数对自己造成贡献。
我们现在考虑端点移动造成的贡献。
当 \(l\to l-1\) 时:\(ans\leftarrow ans+f(l-1,l,r)\) 即 \(f(l-1,1,r)-f(l-1,1,l-1)\)。
当 \(r\to r+1\) 时:\(ans\leftarrow ans+f(r+1,l,r)\) 即 \(f(r+1,1,r)-f(r+1,1,l-1)\)。
同样地推一下发现另外两种情况只不过是换了符号。
其中 \(f(l-1,1,l-1)\) 和 \(f(r+1,1,r)\) 很好维护,可以在一开始顺推一下求出来。
然后我们看剩下的那两个。
\(f(l-1,1,r)\) 的转移。发现从 \(l\to ql(l>ql)\) 的经过是 \(f(l-1,1,r),f(l-2,1,r),\dots,f(ql,1,r)\)。
\(f(r+1,1,l-1)\) 的转移。发现从 \(r\to qr(r<qr)\) 的经过是 \(f(r+1,1,l-1),f(r+2,1,l-1),\dots,f(qr,1,l-1)\)。
那么一次的转移就是 \(f([ql,l-1],1,r)\),\(f([r+1,qr],1,l-1)\)。
也就是这些段是连续的。我们可以搞一个五元组 \((x,l,r,id,k)\),表示在第 \(i\) 个询问中 \([l,r]\) 对 \(a_1\) 到 \(a_x\) 的贡献,\(k\) 代表应该加还是减。
按照 \(x\) 进行一个类似扫描线的东西,然后在更新答案的时候直接暴力处理 \([l,r]\) 即可,注意在每次查询答案的复杂度要控制在 \(O(1)\),原因是莫队移动区间长度总和是 \(n\sqrt n\) 范围的,我们总共要进行这么多次查询,仍然要保证 \(O(1)\) 的转移。
莫队花费的总复杂度是 \(O(2(n\times t(n)+n\sqrt n))\),分别是处理 \(f(x,1,x)\) 的消耗,跑莫队的复杂度,第二次处理离线时重新更新答案的消耗以及处理离线后的查询询问消耗。
有一个常数优化能够优化成 \(O(n+2n\times t(n)+n\sqrt n)\),具体地,将 \(f(x,1,x)\) 进行前缀和,跑莫队的时候可以直接 \(O(1)\) 转移。
因为是毒瘤且冷门的根号算法,还跟某人有关,buff 叠满,所以必要的常数优化是很重要的。
诸如 逆序对 之类的问题会带有方向性,需要我们从前往后从后往前扫两遍。
然后对于这个有很多的不同,所以建议每次做莫队二离的题都重新手推一遍再写。
3.根号分治
原理是一种暴力,根据询问范围使用不同的做法。询问范围由一个阈值 \(s\) 决定。一般来说 \(s\) 取根号。
我们考虑维护一个答案数组 \(sm_{i,j}\),代表 \(\sum^{n}_{k=1}a_k*(k=j\pmod i)(j<i)\)。并且这个答案数组 仅维护 \(i\le s\) 的值。
同时我们仍然维护原数组 \(a\)。
对于操作一,若我们用 \(x\) 暴力把 \(sm_{i}\) 修改一遍,每个 \(x\) 只会对应一个 \(j\),所以枚举的复杂度是 \(s\)。然后我们 \(O(1)\) 修改 \(a_x\)。
对于操作二,若 \(x\le s\),那么我们的答案在答案数组中有过保存直接 \(O(1)\) 输出即可,否则我们枚举所有 \(i=y\pmod x\) 进行求和,复杂度 \(O(\frac{n}{s})\)
这种复杂度见过很多次了,\(s=sqrt(n)\) 时最优。
根号分治很多时候看不出来,但 某个值总量一定 时可以尝试考虑根号分治。