分块与根号算法
update:
- 2024.4.13:完工,修改与整理
0. 根号算法
一些无法以 \(polylog\) 复杂度实现的题,又不能暴力通过,这时根号算法就是一个不错的选择。
1. 整除分块
这一部分较为简单。
1.1 概念与解法
整除分块是要求形如 $$\sum_{i=1}^{n} f(i)\left\lfloor\dfrac{n}{i}\right\rfloor$$ 的式子,我们要求 \(f(n)\) 的前缀和是易求的。
暴力是 \(O(n)\) 的,不太好。
我们考虑把 \(\left\lfloor\dfrac{n}{i}\right\rfloor\) 相同的 \(i\) 一起计算,可以发现 \(\left\lfloor\dfrac{n}{i}\right\rfloor\) 最多只有 \(2 \times \sqrt{n}\) 种数字。
证明:
- 分类:\(i \leq \sqrt{n}\), \(\left\lfloor\dfrac{n}{i}\right\rfloor\) 显然只有 \(\sqrt{n}\) 个。
- \(i > \sqrt{n}\),\(\left\lfloor\dfrac{n}{i}\right\rfloor < \sqrt{n}\) 也只有 \(\sqrt{n}\) 个。
得证。
所以我们这样计算复杂度为 \(O(\sqrt{n})\)。
若有 \(l\),我们考虑求出最大的 \(r\),使 \(\left\lfloor\dfrac{n}{l}\right\rfloor = \left\lfloor\dfrac{n}{r}\right\rfloor\)
我们令 $k = \left\lfloor\dfrac{n}{l}\right\rfloor = \left\lfloor\dfrac{n}{r}\right\rfloor \leq \dfrac n r\
\Longrightarrow \left\lfloor\dfrac{n}{k}\right\rfloor \geq \left\lfloor\dfrac n {\frac n r} \right\rfloor = r $
得 \(r_{max} = \left\lfloor\dfrac n {\left\lfloor\frac n l\right\rfloor}\right\rfloor\) (不要忘了对 \(n\) 取 \(min\))
这样只需累加 \((S(r) - S(l-1)) \times (r-l+1)\) 即可。(\(S\) 代表 \(f\) 前缀和)
1.2 拓展
1.2.1 多维整除分块
即求 $$\sum_{i=1}^{n} f(i)\prod_{j=1}^{m} \left\lfloor\frac {n_j} i\right\rfloor$$
只需令 \(r = \min\limits_{j=1}^m \left(\left\lfloor\dfrac {n_j} {\left\lfloor\frac {n_j} l\right\rfloor}\right\rfloor\right)\)。
使所有 \(\left\lfloor\dfrac {n_j} l \right\rfloor\) 相等,复杂度 \(O(\sum \sqrt{n_j})\)。
1.2.2 常用式子
\(\sum\limits_{i=1}^n i^2 = \dfrac{n(n+1)(2n+1)} 6\) , \(\sum\limits_{i=1}^n i^3 = \left(\dfrac{n(n+1)} 2\right)^2\),\(\left\lceil\dfrac n m\right\rceil = \left\lfloor\dfrac {n+m-1} m\right\rfloor\),\(n \bmod i = n - i \times \left\lfloor\dfrac n i\right\rfloor\)
1.3 例题
I P2424 约数和
拆分问题则答案为 \(S(r) - S(l-1)\),考虑求 \(S\)。
求前 \(n\) 个数的约数和,我们考虑枚举约数 \(i\),可知前 \(n\) 个数中有 \(\left\lfloor\dfrac n i\right\rfloor\) 个数是 \(i\) 的倍数。
则每个约数 \(i\) 的贡献为 \(i\times\left\lfloor\dfrac n i\right\rfloor\)。
答案就是 \(\sum\limits_{i=1}^n i\times\left\lfloor\dfrac n i\right\rfloor\)。
#include <bits/stdc++.h>
using namespace std;
#define ll long long
ll l,r;
ll ask(ll n){
ll ans = 0;
for(ll l = 1,r;l <= n;l = r + 1){
r = min(n,n / (n / l));
ans += (r - l + 1) * (l + r) / 2 * (n / l);
}
return ans;
}
int main(){
scanf("%lld%lld",&l,&r);
printf("%lld\n",ask(r)-ask(l-1));
return 0;
}
II P2261 [CQOI2007] 余数求和 [cogs 3149]
求 \(\sum\limits_{i=1}^n k \bmod i\)。
则 \(\sum\limits_{i=1}^n k \bmod i = \sum\limits_{i=1}^n k - i \times \left\lfloor\dfrac k i\right\rfloor
= k\times n - \sum\limits_{i=1}^n \left\lfloor\dfrac k i\right\rfloor
= k\times n - \sum\limits_{i=1}^{min(n,k)}\left\lfloor\dfrac k i\right\rfloor\)
直接求即可。
代码
IIIP2260 [清华集训2012] 模积和[cogs 3668]
求 \(\sum\limits_{i=1}^n\sum\limits_{j=1}^m(n\bmod i) \times (m \bmod j),i\neq j\)
则原式可写为\(\sum\limits_{i=1}^n\sum\limits_{j=1}^m (n\bmod i) \cdot(m \bmod j) - \sum\limits_{i=1}^n(n\bmod i) \cdot(m \bmod i)\\\)
先考虑减号前的式子。
都可直接解决。减号后。
相当于二维整除分块。复杂度 \(O(\sqrt{n})\)。
注意 \(19940417\) 不是质数,需要别的方法求 \(2\) 与 \(6\) 的逆元。
代码
2. 分块
2.1 算法简介
分块是一种思想,实质是将通过分成多块后再整体修改与暴力重构的结合。
对于一个序列,我们把它分成 \(B\) 块,每块有 \(\dfrac n B\) 个元素,在修改时遇到整块就打标记,遇到散块就暴力重构,这样每次暴力重构的复杂度最多为 \(O(2\dfrac n B)\),打标记的复杂度最多为 \(O(B)\),总复杂度为 \(O(\dfrac n B + B)\),利用均值不等式容易得到最优复杂度为 \(O(\sqrt{n})\)。
当然,每道题的操作不同,整块与散块的复杂度也会不同,\(B\) 的最优复杂度也会不同,具体问题需要具体分析。
分块的优点是容易思考,比较暴力以及可以维护一些复杂的信息。
2.2 其他分块
2.2.1 值域分块
类似于值域线段树,当值域较小时可用分块来维护值域。值域分块可以用来平衡复杂度。
在单点修改,区间查询中。
值域分块可以 \(O(1)\) 修改, \(O(\sqrt{n})\) 查询,即正常单点改,区间暴力查。
也可以 \(O(\sqrt{n})\) 修改,\(O(1)\) 查询,即维护块内前缀和,差分查询。
这样在某些题中可以取得更优的复杂度。详见莫队例 III。
2.2.2 块状链表
可插入的序列。
设块长为 \(B\)。
根号重构:当一个块内添加新元素后达到了 \(2B\) 时,将该块拆为两大小为 \(B\) 的块。
这样最多分裂 \(O(\dfrac n B)\) 次,且每块大小都在 \([B,2B]\) 间。
2.3 经典问题
这里以最经典的区间修改,区间查询为例。
首先考虑修改:
- 对于整块,只需要一个加法标记即可。
- 对于散块,暴力加即可。
然后看查询:
- 对于整块,需要整块的 \(sum\) 和,以及累加上加法标记。
- 对于散块,暴力加即可。
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const int N = 1e5+10,M = 330;
const ll inf = 1e17;
int n,m;
ll a[N],sum[M],la[M];
int L[N],R[N],bl[N],B,t;
void prework(){
B = sqrt(n);t = (n-1)/B+1;
for(int i = 1;i <= t;i++)L[i] = (i-1)*B+1,R[i] = min(i*B,n);
for(int i = 1;i <= n;i++)bl[i] = (i-1)/B+1,sum[bl[i]] += a[i];
}
void modify(int l,int r,ll x){
int p = bl[l],q = bl[r];
if(p == q){
for(int i = l;i <= r;i++)a[i] += x,sum[p] += x;
return;
}
for(int i = l;i <= R[p];i++)a[i] += x,sum[p] += x;
for(int i = L[q];i <= r;i++)a[i] += x,sum[q] += x;
for(int i = p+1;i <= q-1;i++)la[i] += x;
}
ll ask(int l,int r){
int p = bl[l],q = bl[r];
if(p == q){
ll ans = 0;
for(int i = l;i <= r;i++)ans += a[i] + la[p];
return ans;
}
ll ans = 0;
for(int i = l;i <= R[p];i++)ans += a[i] + la[p];
for(int i = L[q];i <= r;i++)ans += a[i] + la[q];
for(int i = p+1;i <= q-1;i++)ans += la[i] * (R[i] - L[i] + 1) + sum[i];
return ans;
}
int main(){
scanf("%d%d",&n,&m);
for(int i = 1;i <= n;i++)scanf("%lld",&a[i]);
prework();
for(int i = 1;i <= m;i++){
int op,l,r;ll x;
scanf("%d%d%d",&op,&l,&r);
if(op == 1){
scanf("%lld",&x);
modify(l,r,x);
}
else printf("%lld\n",ask(l,r));
}
return 0;
}
2.4 例题
I P3372 【模板】线段树 1
II P2801 教主的魔法
区间加,区间询问 \(\geq x\) 的数的个数。
发现 \(c\) 的值很大,考虑分块,预处理时将每块内排序。
对于修改:
- 整块中,只需加上懒标记。
- 散块中,直接修改,暴力重构即可。
对于询问:
- 在整块中,我们可以在块内排序,块内二分查找即可。
- 在散块中,直接查询判断即可。
总复杂度 \(O(m\sqrt{nlogn})\)。
代码
III P4168 [Violet] 蒲公英[cogs 3231]
区间查询众数,输出最小众数,强制在线。
\(a\) 的值域很大,先离散化下。
考虑预处理,设 \(s[i][j]\) 为块 \(i\) 到块 \(j\) 的众数,设 \(cnt[i][j]\) 为数 \(i\) 前 \(j\) 块出现的次数,这样可以差分。
都可 \(O(nB)\) 求出。
考虑询问:
- 对于整块,直接提出 \(s\) 数组内众数即可。
- 对于散块,在桶内暴力统计,暴力比较整块与散块中个数和。
时间复杂度 \(O(n\sqrt{n})\),空间复杂度 \(O(n\sqrt{n})\)。
代码
但是仅仅这样还是不毒瘤,lxl卡了卡内存,变成了
IV P5048 [Ynoi2019 模拟赛] Yuno loves sqrt technology III
只能要 \(O(n)\) 的内存。首先把 \(s\) 变为区间众数的次数。
发现是 \(cnt\) 的问题,考虑优化。可以建立一个 \(vector\) 存每个数出现的下标,这样求区间 \([l,r]\) 中 \(i\) 出现的次数只需要两次二分即可。
这样空间复杂度降到了 \(O(n)\),不过时间复杂度变为了 \(O(n\sqrt{nlogn})\)。
这样就行了吗?不行!
本题必须严格根号复杂度才可过,继续考虑优化。
在询问中设当前整块的答案为 \(ans = s[p+1][q-1]\),若左部分散块的元素 \(x\) 在范围内的个数大于 \(ans\),则在 \(vector\) 中一定有 \(ve[x][d_i+ans]\) 的值小于等于 \(r\)(这里 \(d_i\) 指 每个元素在相应vector里的下标),直接暴力 \(ans++\)。右部分同理。
\(d\) 数组可以预处理,又因为散块中的数最多有 \(2\sqrt{n}\) 个,所以复杂度合理。
时间复杂度为严格 \(O(n\sqrt{n})\),空间复杂度为 \(O(n)\)。(不太卡常)
代码
V P4117 [Ynoi2018] 五彩斑斓的世界
分块不能没有 \(Ynoi\)。这里放一道不咋卡常的大分块。
修改:使区间 \([l,r]\) 内大于 \(x\) 的数减去 \(x\)。这和区间减少 \(x\),再将小于 \(x\) 的数加 \(x\) 是等价的。
查询:查区间 \([l,r]\) 内等于 \(x\) 的个数。
常有数据结构都无法维护,考虑分块。
首先看修改,对于散块,暴力重构即可。
而对于整块,一个显然的做法是若块内最大值 \(mx\) 小于 \(x\),则不修改,否则暴力修改,最坏情况下会爆。
我们观察数据范围,可知 \(x\) 的值域是与 \(n\) 同阶的,可以考虑一种关于值域的算法。
- 当 \(mx < 2x\),则暴力块内将 \([x,mx]\) 中的值减去 \(x\),复杂度 \(O(mx-x)\),块内 \(mx\) 减少了 \(mx-x\)。
- 当 \(mx \geq 2x\),则将块内 \([1,x]\) 中的值加上 \(x\),再打上区间减标记,这样复杂度 \(O(x)\),块内 \(mx\) 减少了 \(x\)。
这就相当于用 \(O(X)\) 的时间复杂度,使 \(mx\) 减少了 \(X\),而又因为 \(mx\) 不会增加,所以每块的均摊复杂度为 \(O(n)\)。
再来考虑如何维护,直接加减显然是不行的,因为询问的是值为 \(x\) 的个数,无法做到。
考虑形象的就是将值域 \([x,mx] \Rightarrow [0,mx-x]\) 与 \([1,x] \Rightarrow [x+1,2x]\)。
这种平移可以用并查集维护,复杂度 \(O(\alpha(n))\),但如果直接将每个数直接维护值域并查集的话显然会爆。
我们可以维护下标并查集,并在每个块内维护值域,这样时空复杂度都为 \(O(nB)\)。
最后考虑询问,每个块内维护值域时同时维护值域为 \(x\) 的 \(size\) 即可,是简单的。
整块直接询问并查集每块内的\(size\),散块暴力查询。复杂度 \(O(\dfrac {n^2} B)\)。
总最优时间复杂度 \(O(n\sqrt{n})\),空间 \(O(n\sqrt{n})\)。
代码
这样就完成了 CF896E Welcome home, Chtholly 。
但是 lxl 又又加强了,并卡了卡常,变为了这道题。
区别就是数据范围更大了,且内存限制变为了恐怖的64MB,就是说只能有 \(O(n)\) 的空间。
这个 \(trick\) 不太难,但比较智慧。
观察一下每块的操作,可以发现每块内的操作都是独立的,与其他块互不相关,所以我们可以离线!!,每次只需单独处理其中一块的操作。
每块共用一个值域,空间复杂度 \(O(n)\)。
3. 莫队
莫队————优雅的暴力
3.1 算法简介
莫队是一种 离线 算法,资瓷修改。
主要思想:对询问分块(相当于对 \(l\) 分块),将询问在以 \(l\) 所在块为第一关键字排序,再以 \(r\) 为第二关键字升序。同时维护两个指针 \(l\) 与 \(r\),根据询问伸缩区间,这样时间复杂度就为 \(O(n\sqrt{n} \times W)\),\(W\) 表示指针移动的复杂度。
证明:因为在一块内 \(r\) 为升序,最坏复杂度为 \(O(n)\),\(l\) 只在块内,最坏复杂度为 \(O(B^2)\),总 \(O(\dfrac {n^2} B + nB)\),取 \(B = \sqrt{n}\) 复杂度最优。得证。
- 奇偶性排序:当 \(l\) 在奇数块内 \(r\) 以降序排序,否则内 \(r\) 以升序排序,会使 \(r\) 呈类似折线形,可有效减少常数。
3.2 带修莫队
因为是离线,莫队一般不资瓷修改,但我们可以暴力增加一个 时间维,同 \(l,r\) 指针一起伸缩。
询问变成了三维,排序也要多以个关键字,类似普通莫队,我们以 \(O(n^{\frac 2 3})\)为块长。
我们分成了 \(n^{\frac 1 3}\) 块,第一关键字 \(l\) 所在块排序,第二关键字以 \(r\) 所在块排序,第三关键字是时间。
这样复杂度是 \(O(n^{\frac 3 5})\)。
证明:设 \(n,m,q\) 同阶。
- \(l,r\) 所在块不变,\(t\) 是单增的,复杂度为 \(O(n)\),总 \(O(\dfrac {n^3} {B^2})\)。
- 对于 \(l\),在块内移动最多 \(O(B)\) 次,总 \(O(nB)\)。
- 对于 \(r\),\(l\) 所在块不变,复杂度为 \(O(B)\),\(l\) 所在块变,最坏移动 \(O(n)\) 次,复杂度为 \(O(\frac {n^2} B)\)。
- 总复杂度 \(O(\dfrac {n^3} {B^2} + nB + \dfrac {n^2} B)\),大约取到 \(B = n^{\frac 2 3}\) 时最小,为 \(O(n^{\frac 5 3})\)。
3.3 回滚莫队
当维护的信息在区间增加与区间减小其中之一无法实现的情况下(如区间最大值),此时我们就需要 回滚莫队。
虽然不能够缩短或伸长,但增加或撤销操作也都是可以实现的。
具体操作(以不能缩短为例):
- 首先我们需要对序列 分块,然后以 \(l\) 所在块为第一关键字排序,以 \(r\) 为第二关键字排序。
- 如果 \(l,r\) 在同一块内,则暴力查询(类似分块)。
- 如果 \(l,r\) 不在同一块,设 \(l\) 所在块为 \(T\),则将 \(l\) 伸缩为 \(R[t]+1\),将 \(r\) 伸缩为 \(R[t]\)。
- \(r\) 点在块内只增加,而 \(l\) 有可能会是乱序的,则我们需要在完成该次询问后撤销回去,使 \(l\) 重回 \(R[t]+1\),形象的:可以再加一个变量 \(l_1\) 代替 \(l\) 进行该询问的增加,而 \(l\) 本身不变,即完成了 "撤销"。
类似的,若信息不能增加,则可以使 \(r\) 为第二关键字降序,在不同块内将 \(l,r\) 设为 \(L[t],n\) 的 大区间即可。
3.4 树上莫队
莫队只能在序列里使用,我们考虑把树的路径转化为序列。
欧拉序:在 \(dfs\) 每次到达某个点时,在序列中加入该点。离开某个点时,再加入一次,得到长为 \(2n\) 的序列。
我们称 \(in[x]\) 为 \(x\) 在该序列第一次的位置, \(out[x]\) 为 \(x\) 在该序列第二次的位置。
则 \(u \rightarrow v\) 的路径就是:
- 若 \(u\) 是 \(v\) 的祖先,则是 \(in[u]\) 到 \(in[v]\)。
- 若 \(u,v\) 互不为祖先,则是 \(out[u]\) 到 \(in[v]\) 再加上 \(lca(u,v)\)。
这样可以把询问也改到序列上。
则我们只需要在转化后的序列跑普通莫队即可,当然带修也可以。
注意长度为 \(2n\)。
3.5 莫队二次离线
咕咕咕
3.6 例题
I P1494 [国家集训队] 小 Z 的袜子 [cogs 1775]
给定序列,求在 \([l,r]\) 内随机抽取两个数相等的概率。
显然总概率为 \(\dbinom {r-l+1} 2\)。若当前数为 \(x\),考虑增点,则需要一个桶 \(c\) 记录 ,增加的概率就是 \(c[x]\)。考虑减点,则减少的概率就是 \(c[x]-1\)。
只需按 \(l\) 所在块为第一关键字升序排序,\(r\) 为第二关键字升序排序即可。
在区间伸缩时,必须先 伸长 再 缩短。
II P1903 [国家集训队] 数颜色 / 维护队列 [cogs 1901]
给定序列,求 \([l,r]\) 内不同数字的个数,带修改。
数据伸缩是简单的。考虑修改,维护时间轴 \(t\) 一起伸缩,可以先删除原有数据,再添加修改数据即可。
排序时以 \(l\) 所在块为第一关键字升序排序,\(r\) 所在块为第二关键字升序排序,以 \(t\) 为第三关键字升序排序。
III P4396 [AHOI2013] 作业[cogs 1822]
区间 \([l,r]\) 内在 \([a,b]\) 范围内的个数以及在 \([a,b]\) 中不同的数的个数。
可以离线,考虑莫队,先排序询问。
然后考虑指针伸缩,可以想到维护一个值域 \(BIT\),前缀和查询。复杂度 \(O(n\sqrt{n}log(n))\)。
\(BIT\) 其实是 \(log(n)\) 的修改与查询,总应该是 \(O(n\sqrt{n}log(n)+mlog(n))\)。
可以考虑平衡复杂度,利用值域分块的 \(O(1) - O(\sqrt{n})\)。
分析复杂度:
- 莫队 \(O(mB + \dfrac {n^2} B)\),\(B = \dfrac n {\sqrt{m}}\) 时最优,为 \(O(n\sqrt{m})\)。
- 值域分块 \(O(m\sqrt{M})\)。
总复杂度 \(O(n\sqrt{m} + m\sqrt{M})\)。
IV Machine Learning
注意该题是求每个数出现次数的 \(mex\)。
我们考虑如何求 \(mex\),根据 \(mex\) 定义可知,若结果为 \(x\) 则至少有 \(1+2+...+x\) 个数,得 \(n\) 个数的最大 \(mex\) 大约为 \(\sqrt{n}\),可暴力求解。
需要两个桶分别记录数组 \(a\) 的次数与记录数组 \(a\) 的桶各数出现的次数,信息维护是简单的。
套用上题,维护 \(t\),先删除原有数据,再添加修改后数据。
复杂度 \(O(q\sqrt{n})\)。
V 歴史の研究
求 \([l,r]\) 内最大 \(x \times c[x]\),其中 \(c[x]\) 表示 \([l,r]\) 内 \(x\) 出现的次数。
可以发现,该信息增加是容易的,但是删除时却很困难(不知道次大值),需要用到 回滚莫队。
首先我们把序列分块,然后根据 \(l\) 所在块为第一关键字升序排序,以 \(r\) 为第二关键字升序排序。
考虑询问,若 \(l,r\) 在同一块内则直接暴力查询(注意是独立查询),否则从 \(l\) 所在块 \(T\) 开始,将 \(l,r\) 定为 \(R[T]+1,R[t]\),回滚 \(l\),\(r\) 单增即可。
VI COT2 - Count on a tree II
只是把区间操作改到了树上路径。
利用上述方法,将树上路径改为序列区间,将区间询问改为序列上区间(加上 \(lca\))。
区间伸缩与例 II 相同。
然后在序列上跑普通莫队即可。
VII P4074 [WC2013] 糖果公园
树上带修。
首先考虑将树上路径改为序列区间,拍成欧拉序,求一下 \(lca\) 即可。
考虑区间伸缩,若增加一个点 \(x\),则 \(sum\) 增加 \(w[cnt[c[x]]+1] * v[c[x]]\),若删除点,则 \(sum\) 减少 \(w[cnt[c[x]]] * v[c[x]]\),是 \(O(1)\) 的。
然后可以直接在序列上跑 带修莫队 即可。
参考文章:
「分块」数列分块入门1 – 9 by hzwer
分块相关杂谈
『回滚莫队及其简单运用』(强烈推荐)