Sqrt Technology(长期更新)
Yuno loves sqrt technology!
分块
分块是一种优雅的暴力,巧妙地平衡了时间复杂度。
基本思想是对每一块预处理,查询/修改时对散块进行适当的暴力,使得复杂度正确。
时间复杂度取决于块长,一般可以用均值不等式求出理论最优块长。但是理论最优不等同于实际最优,根据实际情况调试。
基础部分
区间加,区间和
对每块维护\(sum,tag\)。
区间加:对散块暴力修改\(a_i\)和\(sum\),对整块维护\(tag\)和\(sum\)。
区间和:对散块暴力将\(a_i\)和\(tag\)加起来,对整块直接加上\(sum\)。
时间复杂度:设总长度为\(n\),\(m\)次操作,(\(m,n\)同阶)块长为\(B\),则一共有\(\frac{n}{B}\)个块,一次操作时间复杂度\(O(B+\frac{n}{B})\),当\(B=\sqrt{n}\)时最优,\(O(n\sqrt n)\)。
区间加,区间乘,区间和
对每块维护\(sum,add,mul\)。
区间加:同上。
区间乘:对散块先下放已有的标记(暴力修改一遍),再对需要修改的暴力修,对整块维护\(sum,add,mul\)标记(都要乘)。
区间和:对散块中的元素用\(add,mul\)计算出实际值,暴力加起来,对整块直接加上\(sum\)。
时间复杂度\(O(n\sqrt n)\)。
区间加,区间查小于\(x\)的值的个数
对每块维护\(tag\)和排序后的数列。
区间加:对散块暴力修改,改完后重新排序(重构),对整块维护\(tag\)。
区间查:对散块直接暴力数(记得考虑\(tag\)),对整块在块内二分一下(记得考虑\(tag\))。
时间复杂度\(O(n\sqrt n\log n)\)。
区间加,区间查\(x\)的前驱
要维护的东西同上。
区间加同上。
区间查:散块中暴力找小于\(x\)的数中最大的,整块中二分找,求\(\max\)即可。
时间复杂度\(O(n\sqrt n\log n)\)。
区间开方,区间求和
对每块维护是否全为\(0/1\)的\(tag\)和\(sum\)。
区间开方:观察到在进行若干次开方操作后一段区会都变成\(0/1\),且开方操作下一个数最多\(5\)次就会变成\(0/1\),于是散块和没有\(tag\)的块暴力修改即可,跳过有\(tag\)的块,改完后维护\(tag,sum\)。
区间求和:散块暴力加,整块加\(sum\)。
时间复杂度:暴力修改的次数其实很少,对一个数暴力开方不会超过\(5\)次,所以\(m\)次区间开方的总复杂度可以视作\(O(n)\)的,于是时间复杂度取决于区间求和以及块长,直接取块长\(B=\sqrt n\),时间复杂度约为\(O(n\sqrt n)\)。
单点插入,单点询问
块状链表,将原数列串成链表再分块,设块长阈值为\(B\)。
单点插入:直接插入。若某块块长\(\ge 2B\),就分裂成两个块,若某块块长\(\le \frac{1}{2}B\),与相邻块合并。
感觉不太好写,提供另一种想法:
定期重构一下,每插入\(\sqrt n\)次就重构,一共重构\(\sqrt n\)次,每次\(O(n)\),于是重构的总复杂度为\(O(n\sqrt n)\),是正确的。
单点查询:先跳整块直到在同一块中,再一个一个跳。
时间复杂度\(O(n\sqrt n)\)。
区间赋值,区间查等于\(x\)的数的个数
维护每个块是否纯色以及是哪种颜色。
区间赋值:散块暴力下放标记再赋值,整块直接维护标记。
区间查:散块暴力查,纯色块直接查,杂色块直接暴力。
看上去太暴力了,但是经过一通分析后是\(O(n\sqrt n)\)的但我分析不太明白。
可以观察到一次修改最多使首尾两个散块所在的块从纯色变为杂色,于是均摊下来一次还是\(O(\sqrt n)\)的。
区间查最小众数
预处理\(f_{i,j}\)表示从第\(i\)块到第\(j\)块的众数,\(g_{i,j}\)表示\(f_{i,j}\)的出现次数。(话说\(g_{i,j}\)可以用下文的\(cnt\)算出来啊QwQ)。
于是可以观察到,一段区间的最小众数只可能是整块区间中的众数或者散块中出现的数。
于是再预处理\(cnt_{i,j}\)表示前\(i\)块中\(j\)的出现次数。
查询时枚举散块中的每种数,数出其在散块中的出现次数,再进行比较即可。
散块中数的个数是\(\sqrt n\)级别的,预处理\(O(n\sqrt n)\),块长取\(\sqrt n\),于是总复杂度\(O(n\sqrt n)\)。
另一种解法:
求一个区间内\(x\)的出现次数,可以先用个vector存\(x\)出现的下标,将区间左右端点在vector中二分一下即可。块长要取\(\sqrt{n\log n}\),有点慢,可以借鉴思路。
Ynoi大分块
我妻由乃太可爱了!
虽然叫分块,但是后面的题目还是需要莫队的知识啊。
怀疑有生之年能补完吗......
最初分块
望月悲叹的最初分块。
维护序列\(a\),支持以下两种操作:
-
将区间\([l,r]\)中所有的\(x\)修改为\(y\)。
-
查询区间\([l,r]\)中的第\(k\)小。
Sol:
第二分块
突刺贯穿的第二分块。
维护序列\(a\),支持以下两种操作:
-
将区间\([l,r]\)中所有大于\(x\)的数减去\(x\)。
-
查询区间\([l,r]\)中\(x\)的出现次数。
Sol:
第四分块
拭尽破净的第四分块。
第六分块
深潜循藏的第六分块。
第七分块
????的第七分块。
第十分块
????的第十分块。
第十一分块
沉滞留驻的第十一分块。
第十三分块
????的第十三分块。
第十四分块
点缀光辉的第十四分块。
莫队
对于序列上的区间询问问题,如果从\([l,r]\)能\(O(k)\)地扩展到\([l+1,r],[l-1,r],[l,r+1],[l,r-1]\),那么可以\(O(kn\sqrt m)\)回答所有询问。
具体而言,将询问离线并排序,通过暴力移动当前答案区间\([l,r]\)来回答询问。其复杂度由分块和排序方式保证。
小寄巧:写完add之后,可以从下到上扫描你的add,然后从上到下在你的del函数中执行相反的操作。
可以理解成add和del的对称性很高,但是别全用这个寄巧,能想明白细节还是尽量想,别寄了。
莫队擅长于解决单点对于点集的贡献,通常贡献形式为点对,于是考虑问题的时候要抽象成点对的形式,利用一下前缀和/差分之类的。
普通莫队
不带修,区间转移都为\(O(1)\)。
排序方式:左端点所在块的编号升序,右端点升序。
还可以优化:奇偶化排序。左端点所在块的编号升序,当左端点所在块的编号为奇数时,右端点升序,反之右端点降序。
设置块长为\(\frac{n}{\sqrt m}\)最优。\(n,m\)同阶时,取\(\sqrt n\)。
时间复杂度\(O(n\sqrt m)\)。
注意移动区间时要先扩大再缩小,以防出现神秘错误。
带修莫队
强行给普通莫队加上一维时间维,就可以修改了。
也要保证时间维上的移动是\(O(1)\)的。
时间维上的移动,要考虑是加上修改还是撤销修改,并且要考虑对当前答案的影响。
块长一般取\(n^{\frac{2}{3}}\)。
排序方式:第一关键字:左端点所在块升序;第二关键字:右端点所在块升序;第三关键字:时间升序。
时间复杂度\(O(n^{\frac{5}{3}})\)。
树上莫队
回滚莫队
如果在莫队的过程中增加/删除两种操作之一不能实现(或复杂度不正确),就考虑回滚莫队。
回滚莫队不实现难做的操作,而是用撤销影响的方式代替。
不删除莫队
具体操作如下:
-
将询问按左端点所在块升序,左端点同一个块内则右端点升序排序。
-
如果询问的左右端点在同一个块内,特判,直接暴力回答。
-
如果处理到了下一个块,那么清空莫队(撤销影响而非删除),并令莫队的左端点移动到新块的右端点加一处,右端点移动到新块的右端点。并且要清空原有的答案。
-
左端点在同一个块内时,右端点是单调不降的,可以直接移动右端点,不必回滚。
-
而左端点是乱序的,每次移动左端点回答询问后都令左端点回滚到起点,并且恢复原来的答案(具体实现时可以不修改原有答案,用\(tmp\)保存原有答案后对\(tmp\)进行修改,回答询问后把\(tmp\)扔了就行)(还是撤销影响而非删除)。
这样就只有撤销和增加的操作,避免了删除。
分析一下时间复杂度(认为\(n,m\)同阶):
-
暴力部分,一次最多\(O(\sqrt n)\),总共\(O(n\sqrt n)\)。
-
清空莫队操作,一次\(O(n)\),最多有\(\sqrt n\)个块,执行\(\sqrt n\)次,总共\(O(n\sqrt n)\)。
-
移动右端点,同一个块内移动右端点最多\(O(n)\),总共\(\sqrt n\)个块,总共\(O(n\sqrt n)\)。
-
移动左端点,一次询问最多\(O(\sqrt n)\),总共\(O(n\sqrt n)\)。
综上,时间复杂度\(O(n\sqrt n)\)。
不增加莫队
类比上文:
-
将询问按左端点所在块升序,左端点同一个块内则右端点降序排序.
-
左右端点同一个块内不需要暴力,但是也可以暴力回答。
-
处理到新块就将左端点设为新块的左端点,右端点移动到序列末尾。(这里要求能够在正确的时间复杂度里进行初始化答案)。
-
同一个块内右端点单调不增,直接移动。
-
左端点乱序,移动后进行回滚。
复杂度同上文分析,\(O(n\sqrt n)\)。
莫队二次离线
当增加和删除的转移复杂度都很高时,考虑二次离线。
我们可以将莫队移动的这些端点同样离线下来计算。前后离线两次,故称二次离线。
具体流程如下:
- 先跑一遍莫队,增/删一个点产生的贡献拆成前缀相减的形式(这里要求贡献具有可减性,可以进行差分)。且形式很特殊,下面进行讨论:
记\(f(x,[l,r])\)表示位置\(x\)对区间\([l,r]\)产生的贡献。
\([l,r]\rightarrow [l,r+1]:f(r+1,[l,r])=f(r+1,[1,r])-f(r+1,[1,l-1])\)
\([l,r]\rightarrow [l,r-1]:-f(r,[l,r-1])=-f(r,[1,r-1])+f(r,[1,l-1])\)
\([l,r]\rightarrow [l+1,r]:-f(l,[l,r])=-f(l,[1,r])+f(l,[1,l])\)
\([l,r]\rightarrow [l-1,r]:f(l-1,[l,r])=f(l-1,[1,r])-f(l-1,[1,l-1])\)
真是酣畅淋漓的讨论啊。
-
找到
发明一种数据结构,使得可以\(O(1)\)或稍劣的复杂度查询点对点集的贡献,\(O(\sqrt n)\)或稍劣修改点集。 -
先预处理,扫一遍求出形如\(f(i,[1,i-1])\)以及\(f(i,[1,i])\)的贡献(有利于常数),显然用找到的数据结构直接扫一遍是\(O(n\sqrt n)\)的。预处理的贡献要做一遍前缀和,方便查询。
-
跑一遍莫队,把要求的形如\(f(x,[1,l-1])\)的\(x\)记录下来,挂在前缀\([1,l-1]\)上。注意到这些点\(x\)是一段连续区间,储存时直接存区间即可。注意到产生的区间数是\(O(n)\)的,空间复杂度从\(O(n\sqrt n)\)优化到了\(O(n)\)。总点数因为在跑莫队进行\(n\sqrt n\)次移动,是\(n\sqrt n\)的。这里可以用预处理的贡献的前缀和快速计算已知的贡献,\(O(n)\)。
-
最后在扫一遍前缀,对每个前缀处理它产生的贡献。由于一共\(n\)个前缀,每个前缀修改一次,这部分\(O(n\sqrt n)\)。一共产生了\(n\sqrt n\)个询问点,单次查询\(O(1)\),这部分也是\(O(n\sqrt n)\)的。所以总体复杂度\(O(n\sqrt n)\)。
找到的数据结构很可能是分块,也可能是暴力加人类智慧。
修改和查询的复杂度可以稍劣,按照以上复杂度分析下来是对的就行。
特别注意: \(f(l,[1,l])\)和\(f(l-1,[1,l-1])\)这种自己对自己贡献的东西,考虑是否会算重,是否要算,注意到\(f(l,[1,r])\)中同样有自己对自己的贡献。小心考虑。
一些经典问题
区间颜色种数
典,直接上普通莫队,维护每种颜色出现次数,次数从\(1\)减到\(0\)或者从\(0\)加到\(1\)时增减颜色种数即可。
区间颜色出现次数与排名之积
这是一次比赛题(2024.11.25 T2)的简化题意:
询问一个区间\([l,r]\)中,最小化\(\sum\limits_{1\le k\le n}(k-1)\sum\limits_{l\le i\le r} [a_i=col_k]\)的值,其中\(col_k\)是自己选择的一个排列。
\(a\)数组长为\(n\),\(m\)次询问。\(n,m\le 2\times 10^5\)。
看到数据范围猜测莫队。然后看到神秘的区间询问更是莫队了。
式子的后面那一坨其实就是区间内这种颜色的出现次数。
首先是上排序不等式,发现逆序乘上去可以最小化(就是大的出现次数和尽量小的\((k-1)\)相乘)。然后就是维护一个类似出现次数乘上次数的排名这样的东西。
考虑莫队移动端点时会如何变化。发现考虑次数的排名会遇到“次数的出现次数”的这种概念。
但是先别急,考虑莫队增加一个点的变化,设这个点的颜色为\(p\),颜色\(p\)已经出现了\(k\)次,加上这个点后这个点的颜色出现了\(k+1\)次。设在加入这个点之前,原本出现了\(k+1\)次及以上次数的颜色种类数有\(s\)种。那么这\(s\)种对应乘上了\(0\)到\(s-1\)。那么颜色\(p\)原本的贡献为\(s\times k\),现在新加一个点后贡献为\(s\times (k+1)\),所以贡献增加了\(s\)。
然后考虑莫队减少一个点的变化,同上设,颜色\(p\)已出现\(k\)次,减去后出现\(k-1\)次,出现次数为\(k\)次及以上次数的颜色种类数有\(s\)种。于是颜色\(p\)原本的贡献为\((s-1)*k\),减去一个点后贡献为\((s-1)\times (k-1)\),于是贡献减少了\(s-1\)。
思考一下要是有一些颜色的出现次数相同怎么办?贡献似乎不能说就是\(s\times k\)或者是\((s-1)\times k\)。但实际上是没有影响的。出现次数相同意味着对答案的贡献只有前面排名处乘上的系数不同。若是颜色\(p\)此刻要发生变化,可以认为将\(p\)移动到了对答案的贡献序列中同种出现次数的首/尾,即钦定了它的贡献是\(s\times k\)或者\((s-1)\times k\)。那么就不必再多想了。
转移就可以做到\(O(1)\)了,维护一下出现次数在\(k\)次及以上次数的颜色种类数,以及每种颜色的出现次数,然后修改的时候只用改一下相邻的就好。
总的还是普通莫队,\(O(n\sqrt m)\)。
根号分治
我们可以设置阈值\(B\),发现题目中一个东西\(\le B\)时暴力可做,\(>B\)时有另一个东西\(\le B\),对另一个东西暴力也可做,于是这道题就可以做了。
看看题。
二进制位上的根号操作
一般是前一半二进制位和后一半二进制位分开操作,可以平衡复杂度,做到\(O(\sqrt V)\)。
可能会用到子集和DP的一些思路(?
总之要分析一下操作和询问的性质。

浙公网安备 33010602011771号