分块与根号算法入门

update:

  • 2024.4.13:完工,修改与整理
  • 2025.3.4: 修 markdown,移除一些大分块至分块进阶,加入树分块

0. 根号算法

一些无法以 polylog 复杂度实现的题,又不能暴力通过,这时根号算法就是一个不错的选择。

wcnm 联合省选 2025 D1T2。

1. 整除分块

这一部分较为简单。

1.1 概念与解法

整除分块是要求形如 i=1nf(i)ni 的式子,我们要求 f(n)前缀和是易求的。

暴力是 O(n) 的,不太好。

我们考虑把 ni 相同的 i 一起计算,可以发现 ni 最多只有 2×n 种数字。

证明,分类讨论:

  • in,只有 n 个数,ni 显然只有 n 个。
  • i>n,因为 ni<n,也只有 n 个。

所以我们这样计算复杂度为 O(n)

若有 l,我们考虑求出最大的 r,使 nl=nr

我们令 k=nl=nrnrnknnr=r

rmax=nnl,一个细节是优势需要与 nmin

这样只需累加 (S(r)S(l1))×(rl+1) 即可,其中 S(n)=i=1nf(i)

1.2 拓展

1.2.1 多维整除分块

即求

i=1nf(i)j=1mnji

只需令 r=minj=1m(njnjl)

使所有 njl=njr,复杂度 O(nj)

1.2.2 常用式子

i=1ni2=n(n+1)(2n+1)6i=1ni3=(n(n+1)2)2nm=n+m1mnmodi=ni×ni

1.3 例题

I P2424 约数和

拆分问题则答案为 S(r)S(l1),考虑求 S(n)

求前 n 个数的约数和,我们考虑枚举约数 i,可知前 n 个数中有 ni 个数是 i 的倍数。

则每个约数 i 的贡献为 i×ni
答案就是 i=1ni×ni

#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] 余数求和

i=1nkmodi=i=1nki×ki=k×ni=1nki=k×ni=1min(n,k)ki

直接求即可。

代码

IIIP2260 [清华集训2012] 模积和

i=1nj=1m(nmodi)×(mmodj),ij

则原式可写为i=1nj=1m(nmodi)(mmodj)i=1n(nmodi)(mmodi)
先考虑减号前的式子。

i=1nj=1m(nmodi)(mmodj)=(n2i=1nini)(m2j=1mjnj)

都可直接解决。减号后。

i=1n(nmodi)×(mmodi)=i=1n(nini)×(mimi)=i=1n(nmnimimni+i2nimi)

相当于二维整除分块。复杂度 O(n)

注意 19940417 不是质数,需要别的方法求 26 的逆元。

代码

2. 分块

2.1 算法简介

分块是一种思想,实质是将通过将一个序列分成几块,对于块内 预处理,或者说 整体修改暴力重构

对于一个序列,将每块假设有 B 个元素,则一共有 nB 块,在修改时遇到 整块 就打标记,遇到 散块 就暴力重构,这样每次暴力重构的复杂度最多为 O(B),打标记的复杂度最多为 O(nB),总复杂度为 O(nB+B),利用均值不等式容易得到最优复杂度为 O(n)

当然,每道题的操作不同,整块散块 的复杂度也会不同,B 的最优复杂度也会不同,具体问题需要具体分析。

分块的优点是容易思考,比较暴力以及可以维护一些 复杂的信息

2.2 其他分块

2.2.1 值域分块

类似于值域线段树,当值域较小时可用分块来维护值域,值域分块可以用来 平衡复杂度

在单点修改,区间查询中。

值域分块可以 O(1) 修改, O(n) 查询,即正常单点改,区间暴力查。

也可以 O(n) 修改,O(1) 查询,即维护 块内前缀和,差分查询。

这样在某些题中可以取得更优的复杂度,详见莫队例 III

2.2.2 块状链表

可插入的序列,设块长为 B

根号重构: 当一个块内添加新元素后达到了 2B 时,将该块拆为两大小为 B 的块。

这样最多分裂 O(nB) 次,且每块大小都在 [B,2B] 间。

2.2.3 树分块

对于一棵树,我们设一个阈值为 B,考虑在树上选择不超过 nB关键点,使得每个关键点到其最近祖先关键点距离不超过 B

一个简单的方法是随机撒点,期望下是正确的。

严谨的,我们每次选择深度最深的非关键点,考虑其 1B 级祖先是否是关键点,若都不是,则我们将其 B 级祖先设为关键点,这样每次我们可以排除至少 B 个点,关键点数量即为 nB 的。

这样对于一个链询问,我们可以分成四个长度 <B 的散块,以及个数不超过 nB 个的整块,复杂度是正确的。

2.3 经典问题

区间修改,区间查询。

对于修改,散块 暴力改,整块 打标记。

对于询问,散块 暴力加,整块 直接加和,注意加上标记。

#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];
    ll ans = 0;
    if(p == q){
        for(int i = l;i <= r;i++)ans += a[i] + la[p];
        return ans;
    }
    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 教主的魔法

区间加,区间询问 x 的数的个数。

发现 c 的值很大,考虑分块,预处理时将每块内排序,假设每块有 B 个。

对于修改,整块 中,打上标记即可,复杂度 O(nB)散块 中,直接修改,暴力重构即可,复杂度 O(Blogn)

对于询问,整块 中,块内是重构后排好序的,二分查找即可,复杂度 O(nBlogn)散块 中,暴力判断即可,复杂度 O(B)

总复杂度 O(nlogn+mBlogn+nmlognB),取 B=n 得复杂度为 O(nlogn+mnlogn)

代码

III P2464 [SDOI2008] 郁闷的小 J

虽然分块不优,但是可以做个示例。

我们分块后,维护每个块内的 哈希表,询问对于 散块 暴力判断,整块 则询问哈希表,修改为哈希表复杂度。

若用 map,复杂度 O(mB+nmlognB),若仅仅取 B=n,则 TLE 80pts 3.64s。

若平衡复杂度取 B=nlogn,则仅跑 1.56s,若换成更快的哈希表,如 gp_hash_table,仅需跑 600ms。

可以直观的感受平衡复杂度的思想。

代码

IV P4168 [Violet] 蒲公英

区间查询众数,输出最小众数,强制在线。

a 的值域很大,先离散化下。

考虑预处理,设 s[i][j] 为块 i 到块 j 的众数,设 cnt[i][j] 为数 ij 块出现的次数,这样可以差分。

都可 O(nB) 求出。

考虑询问:

  • 对于整块,直接提出 s 数组内众数即可。
  • 对于散块,在桶内暴力统计,暴力比较 整块与散块 中个数和。

时间复杂度 O(nn),空间复杂度 O(nn)

代码

但是仅仅这样还是不 毒瘤,lxl 卡了卡内存,变成了

IV P5048 [Ynoi2019 模拟赛] Yuno loves sqrt technology III

只能要 O(n) 的内存。首先把 s 变为区间众数的次数。

发现是 cnt 的问题,考虑优化。可以建立一个 vector 存每个数出现的下标,这样求区间 [l,r]i 出现的次数只需要两次二分即可。

这样空间复杂度降到了 O(n),不过时间复杂度变为了 O(nnlogn)

这样就行了吗?不行!

本题必须严格根号复杂度才可过,继续考虑优化。

在询问中设当前整块的答案为 ans=s[p+1][q1],若左部分散块的元素 x 在范围内的个数大于 ans,则在 vector 中一定有 ve[x][di+ans] 的值小于等于 r(这里 di 指 每个元素在相应vector里的下标),直接暴力 ans++。右部分同理。

d 数组可以预处理,又因为散块中的数最多有 2n 个,所以复杂度合理。

时间复杂度为严格 O(nn),空间复杂度为 O(n)

代码

V P6177 Count on a tree II/【模板】树分块

树分块,选定阈值 B,找不超过 nB关键点,使其与其最近关键点祖先距离不超过 B,用 2.2.3 的方法,复杂度 O(nB)

然后考虑求答案,首先可以离散化,n 很小,考虑 bitset,我们预处理任意在一条到根的链上的 一对关键点 之间的 bitset 数组,复杂度 O(n3B2ω)

对于一个询问 xy,我们将其分为 xlcaylca 两个等价段。

我们暴力跳到一个关键点,记录 bitset,然后向上接着跳关键点,最后再跳到 lca,复杂度 O(nmω+mB+nmB)

总复杂度 O(nB+n3B2ω+nmω+mB+nmB),取 B=n,得最优复杂度为 O(n2+nmω+(n+m)n)

代码

3. 莫队

莫队————优雅的暴力

3.1 算法简介

莫队是一种 离线 算法,资瓷修改。

主要思想:对询问分块(相当于对 l 分块),将询问在以 l 所在块为第一关键字排序,再以 r 为第二关键字升序。同时维护两个指针 lr,根据询问伸缩区间,这样时间复杂度就为 O(nn×W)W 表示指针移动的复杂度。

证明:因为在一块内 r 为升序,最坏复杂度为 O(n)l 只在块内,最坏复杂度为 O(B2),总 O(n2B+nB),取 B=n 复杂度最优。得证。

  • 奇偶性排序:当 l 在奇数块内 r 以降序排序,否则内 r 以升序排序,会使 r 呈类似折线形,可有效减少常数。

3.2 带修莫队

因为是离线,莫队一般不资瓷修改,但我们可以暴力增加一个 时间维,同 l,r 指针一起伸缩。

询问变成了三维,排序也要多以个关键字,类似普通莫队,我们以 O(n23)为块长。
我们分成了 n13 块,第一关键字 l 所在块排序,第二关键字以 r 所在块排序,第三关键字是时间。
这样复杂度是 O(n35)
证明:设 n,m,q 同阶。

  • l,r 所在块不变,t 是单增的,复杂度为 O(n),总 O(n3B2)
  • 对于 l,在块内移动最多 O(B) 次,总 O(nB)
  • 对于 rl 所在块不变,复杂度为 O(B)l 所在块变,最坏移动 O(n) 次,复杂度为 O(n2B)
  • 总复杂度 O(n3B2+nB+n2B),大约取到 B=n23 时最小,为 O(n53)

3.3 回滚莫队

当维护的信息在区间增加与区间减小其中之一无法实现的情况下(如区间最大值),此时我们就需要 回滚莫队
虽然不能够缩短或伸长,但增加或撤销操作也都是可以实现的。
具体操作(以不能缩短为例):

  • 首先我们需要对序列 分块,然后以 l 所在块为第一关键字排序,以 r 为第二关键字排序。
  • 如果 l,r同一块内,则暴力查询(类似分块)。
  • 如果 l,r 不在同一块,设 l 所在块为 T,则将 l 伸缩为 R[t]+1,将 r 伸缩为 R[t]
  • r 点在块内只增加,而 l 有可能会是乱序的,则我们需要在完成该次询问后撤销回去,使 l 重回 R[t]+1,形象的:可以再加一个变量 l1 代替 l 进行该询问的增加,而 l 本身不变,即完成了 "撤销"

类似的,若信息不能增加,则可以使 r 为第二关键字降序,在不同块内将 l,r 设为 L[t],n大区间即可。

3.4 树上莫队

莫队只能在序列里使用,我们考虑把树的路径转化为序列。

欧拉序:在 dfs 每次到达某个点时,在序列中加入该点。离开某个点时,再加入一次,得到长为 2n 的序列。
我们称 in[x]x 在该序列第一次的位置, out[x]x 在该序列第二次的位置。
uv 的路径就是:

  • uv 的祖先,则是 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] 内随机抽取两个数相等的概率。

显然总概率为 (rl+12)。若当前数为 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(nnlog(n))
BIT 其实是 log(n) 的修改与查询,总应该是 O(nnlog(n)+mlog(n))

可以考虑平衡复杂度,利用值域分块的 O(1)O(n)
分析复杂度:

  • 莫队 O(mB+n2B)B=nm 时最优,为 O(nm)
  • 值域分块 O(mM)

总复杂度 O(nm+mM)

代码

IV Machine Learning
注意该题是求每个数出现次数mex
我们考虑如何求 mex,根据 mex 定义可知,若结果为 x 则至少有 1+2+...+x 个数,得 n 个数的最大 mex 大约为 n,可暴力求解。

需要两个桶分别记录数组 a 的次数与记录数组 a 的桶各数出现的次数,信息维护是简单的。
套用上题,维护 t,先删除原有数据,再添加修改后数据。
复杂度 O(qn)

代码

V 歴史の研究
[l,r] 内最大 x×c[x],其中 c[x] 表示 [l,r]x 出现的次数。

可以发现,该信息增加是容易的,但是删除时却很困难(不知道次大值),需要用到 回滚莫队
首先我们把序列分块,然后根据 l 所在块为第一关键字升序排序,以 r 为第二关键字升序排序。

考虑询问,若 l,r 在同一块内则直接暴力查询(注意是独立查询),否则从 l 所在块 T 开始,将 l,r 定为 R[T]+1,R[t]回滚 lr 单增即可。

代码

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
分块相关杂谈
『回滚莫队及其简单运用』(强烈推荐)

posted @   oXUo  阅读(194)  评论(2编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!
网站统计
点击右上角即可分享
微信分享提示