更新于 2026/6/29 19:53:02 作者

command_block

stO lxl Orz\text{stO lxl Orz}

您将在此看到 OIer 分块的最低水平。

0. 分块概论

在数据结构问题中,我们需要维护一些离散的信息单位,单位的数目会影响处理的复杂度。

如果信息支持不受限制的合并与批量处理,那么就可以使用线段树等区间数据结构。

当然,有时并不存在高效的信息合并化简方式,而需要在“批量”和“零散”之间找到平衡点,这就是分块的核心思想。

这一类问题的性质更弱,所以问题形式变化多端,“整散”关系多种多样,具有传统数据结构难以比拟的灵活性。

1. 基础数列分块

在这一节中,我们将认识一些经典的数列分块问题,初步熟悉分块的形式和思想。

  • 数列分块的形式

下面统一约定数列长度为 nn ,块大小为 BB

如图,在分块中有如下概念 :

  • 末尾小块 : 因为整个数列划分不整而得到的一个较小的块,可能带来多余的细节。有时可以人为地增长数列将小块变成正常大小的块。

  • 整块 : 被操作区间完整覆盖的块。总块数是 O(n/B)O(n/B) 的。

  • 散块 : 被操作区间覆盖了一部分的块。散块包含的元素数目是 O(B)O(B) 的。

找出所有的 整块 和 散块内元素 是容易的。

在这个问题中,信息能够高效合并,这使得分块没有太大的优势。

但是,从这一类简单问题入门分块比较易于初学者理解。

  • 区间查询 (一般先分析查询,搞清楚需要维护什么,再考虑如何维护)

    注意到,每个元素对答案的贡献是独立的。我们可以对每个 块 / 散块 分别计算贡献。

    (大多数分块都会满足这个性质,因为块间贡献一般很难考虑)

    对每个整块记下总和,这样容易 O(1)O(1) 计算贡献。对于散块,暴力求和。

  • 区间修改

    对于散块,暴力加。

    对于整块,记录加法标记。

    这样,总和 == 块内元素总和 ++ 标记 ×\times 块大小。

    当取单个元素时,要加上对应块的加法标记。

两者的复杂度均为 O(n/B+B)O(n/B+B) ,令 B=nB=\sqrt{n} 得到最优复杂度 O(qn)O(q\sqrt{n})

思考我们这样做的本质,不难发现,其实是将线段树这一 “二叉树” 改成了 “n\sqrt{n} 叉树” 。

上面的做法对应标记永久化,其实也可以将标记下推,即每次暴力清空两个散块的标记,复杂度不变。

评测记录 | 代码Link

  • Δ\color{green}\bf\Delta P2801 教主的魔法

    题意 : 区间加,查询区间大于等于 kk 的数的个数。

  • 区间查询

    这里贡献仍然独立。

    考虑在每个块内维护有序序列,这样只需要一次二分即可计算块内贡献。

    对于散块,暴力遍历即可。

    若块大小为 BB ,则复杂度为 O((n/B)logB+B)O((n/B)\log B+B)

  • 区间加

    对于整块,块内的相对顺序不变,只需记录加法标记。注意考虑加法标记对二分比较的影响。

    对于散块,需要重构有序序列,若加完后暴力排序,复杂度是 O(BlogB)O(B\log B)

    在旧的有序序列中提取加和未被加的两部分,各是有序序列,归并即可做到 O(B)O(B)

    复杂度 O((n/B)+B)O((n/B)+B)

B=nlognB=\sqrt{n\log n} 得到最优复杂度 O(qnlogn)O(q\sqrt{n\log n})

复杂度分析技巧 : 分块的主要思想是平衡,若复杂度由两部分组成,直接令两者相等,即可解得最优块大小。

(本题数据非常弱,错误做法可能可以通过)

类似上一题,对每个块维护有序序列,查询时整块二分,散块暴力。

复杂度 O(qnlogn)O(q\sqrt{n\log n})

评测记录

对每个块维护有序序列,查询时二分转化成 P2801。复杂度为 O(qnlognlogw)O(q\sqrt{n\log n}\log w) ,无法通过。

仔细分析查询时的操作 : 整块二分,散块暴力。

根据信息论,整块二分难以优化。但对散块进行 O(logw)O(\log w) 次暴力有些浪费。

可以事先将散块归并,这样对散块的查询无需暴力遍历,而可以一次二分搞定。

这样,单次询问的复杂度改进为 O((n/B)logBlogw+B)O\big((n/B)\log B\log w+B \big)

若将 logw\log w 视为与 logn\log n 同阶,则 令 B=nlognB=\sqrt{n}\log n 可得复杂度为 O(qnlogn)O(q\sqrt{n}\log n),卡常后可以通过。

注意,散块部分的常数较大,应适当调小块大小。

评测记录

: 觉得不过瘾可以做 P3380 【模板】二逼平衡树(树套树)

  • Δ\color{green}\bf\Delta P3203 [HNOI2010]弹飞绵羊

    题意 : 维护一个 nn 个点的有向图,每个点都有且仅有一条出边,且指向比自己编号大的点。

    支持修改某个点的出边,查询从某个点出发几步会到达 nn 号点。

“行动步数”问题和经典的求算贡献问题有很大不同。

不难发现,若没有修改,容易 O(n)O(n) 求出各个点的答案。

对于一个块,预处理其每个元素走出该块所需步数,以及到达的点。

这样,可以 O(1)O(1) 跳过一个块。查询复杂度改进为 O(n/B+B)O(n/B+B)

在修改时,也只需 O(B)O(B) 重构一个块。

B=nB=\sqrt{n} 可得复杂度为 O(qn)O(q\sqrt{n})

评测记录

  • Δ\color{green}\bf\Delta Loj#6546. 简单的数列题

    题意 : 给定两个长度为 nn 的数列 A,BA,B ,支持下列操作:

    • 将数列 AA 中区间 [l,r][l,r] 内所有数加上 ww ;
    • 交换 BxB_xByB_y ;
    • maxi[l,r]{Ai×Bi}\max\limits_{i\in[l,r]}\{A_i\times B_i\} .

操作二可以直接把涉及到的两个块重构。

区间加时,散块可以暴力重构,但是整块只能打标记。

每个位置的贡献可以看做关于标记的一次函数,于是维护凸壳即可。

散块重构时利用归并可以 O(B)O(B) 建立凸壳。

查询时,由于标记只会递增,线性扫即可。

B=nB=\sqrt{n} 可得复杂度为 O(qn)O(q\sqrt{n})

  • Δ\color{green}\bf\Delta P4135 作诗

    题意 : 查询区间内出现偶数次的数(称为好数)的个数,强制在线。值域在 O(n)O(n) 级别。

此时贡献是不独立的,对每个块分别考虑并不方便。

利用桶记录每个数的出现次数(奇偶性),即可增量维护好数的个数。

s[i,j]s[i,j] 为块 [i...j][i...j] 中好数个数,可以 O(n/B)O(n/B) 次从各个块开始增量维护得到,复杂度为 O(n2/B)O(n^2/B)

o[i][x]o[i][x] 表示前 ii 个块中 xx 的出现次数(奇偶性),通过差分,可以得到任意块区间中某个数的出现次数。

查询时,若 [l,r][l,r] 包含的整块区间为 [L,R][L,R] ,则先取出 s[L,R]s[L,R]

然后考虑散块的影响,仍然用增量法计算即可。

总复杂度为 O(nB+n2/B)O(nB+n^2/B) ,令 BnB\sqrt{n} 可得复杂度为 O((n+q)n)O\big((n+q)\sqrt{n}\big)

若要节省空间,可以用 bitset 记录 oo ,则空间可以做到 O(nn/w)O(n\sqrt{n}/w)

可以发现,本题思想类似于回滚莫队。实际上,静态分块的功能是莫队的子集,所以强制在线时分块才有优越性。

基本操作和上一题非常类似。

贡献不独立。

用桶记录每个数的出现次数(奇偶性),即可增量维护众数。

s[i,j]s[i,j] 为块 [i...j][i...j] 中的众数,利用增量求解。

o[i][x]o[i][x] 表示前 ii 个块中 xx 的出现次数。

查询时,若 [l,r][l,r] 包含的整块区间为 [L,R][L,R] ,则先取出 s[L,R]s[L,R]

然后考虑散块的影响,仍然用增量法计算即可。(注意散块对出现次数也有贡献)

时空复杂度均为 O(nn)O(n\sqrt{n})

在上一题中,空间的瓶颈是 oo 数组。观察其用途 : 判定散块中某个数在询问区间中的出现次数是否 >k>k

我们可以用 vector 记录下同类数的所有出现位置。

当判定散块中数 uu 出现次数是否 >k>k 时,假设 uu 是询问区间内同类数的最靠左出现(左侧散块,右侧散块类似),则检查 uu 后面的第 kk 个同类数是否在询问区间外即可。

空间复杂度改进为 O(n)O(n) ,时间常数也减小了。

在逆序对问题中,每一对数都有贡献,贡献模式是二维的。

询问时,答案的贡献成分如下图 :

绿色是整块内部的贡献,橙色是散块内部的贡献,红色是整块和散块之间的贡献,蓝色是散块之间的贡献。

考虑对每个块内前后缀都预处理内部逆序对数,容易使用树状数组做到 O(nlogn)O(n\log n),这样就解决了橙色部分。

考虑归并排序的过程,不难两个区间之间的逆序对可以将这两个区间归并计算。

于是,预处理块内有序序列,蓝色部分就可以(先取出有序散块)归并计算。

然后预处理 s[i,j]s[i,j] 表示块 i...ji...j 内部的逆序对数。

考虑差分,有 $s[i,j]=s[i+1,j]+s[i,j-1]-s[i+1,j-1]+(\text{块}i,j\text{之间的贡献})$

两块间贡献仍然可以 O(n)O(\sqrt{n}) 归并计算,则预处理复杂度为 O(nn)O(n\sqrt{n})。绿色部分完成。

接下来预处理 o[i][x]o[i][x] 表示前 ii 个块中 x\leq x 的数的个数,然后可以计算红色部分(对每个散块元素分别统计贡献)。

复杂度为 O(nn)O(n\sqrt{n})。此做法常数较大。

还有另一个常数较小的做法,预处理 f[i][j]f[i][j] 表示 [1...i][1...i] 中的数与第 jj 个块之间的贡献,利用桶(值域前缀和)不难做到 O(nn)O(n\sqrt{n})

这样,计算两块之间的贡献时,直接差分即可(不需要常数巨大的归并)。

计算红色部分时,可以转而枚举整块,然后差分。

蓝色部分依然需要归并计算。

2. 经典分块技巧

一些 基于分块的技巧 和 分块需要的技巧。

  • Δ\color{blue}\bf\Delta 块状数组

某些分块可以视作三层的 n\sqrt{n} 叉树,如图。

块状数组查询和修改的复杂度一优一劣,用于查询和修改数目不同的问题,以平衡复杂度。

  • O(1)O(1) 单点修改,O(n)O(\sqrt{n}) 查询区间和。

    维护块和,修改时只需修改对应元素以及块和,查询时和数列分快相同。

  • O(n)O(\sqrt{n}) 单点修改,O(1)O(1) 查询前缀和。

    维护块内前缀和,块和的前缀和。查询时整块正缀和加上块内前缀和。

    修改时暴力 O(n)O(\sqrt{n}) 分别重新计算块内前缀和和块和的前缀和。

当值域大小为 O(n)O(n) 时,可以用类似权值线段树的形式维护 “权值 n\sqrt{n} 叉树”。这个技巧被称为值域分块

  • O(1)O(1) 插入一个数,O(n)O(\sqrt{n}) 查询第 kk 小。

    维护块和。查询时先逐块确定,然后进入块内逐个确定。

  • O(n)O(\sqrt{n}) 插入一个数,O(1)O(1) 查询第 kk 小。(较为困难)

    首先求出答案在那个值域块内,再求答案。

    对每个 kk 维护第 kk 大值在哪个值域块中,会形成 O(n)O(\sqrt{n}) 个段。

    一次修改后,每个段的端点会移动,总复杂度是 O(n)O(\sqrt{n}) 的。

    对于每个值域块维护一个有序序列,利用插入排序可以做到 O(n)O(\sqrt{n})

    再维护每个块前面的数的数目,将 kk 减去之后直接取对应下标即可。

  • 例题 : CC Chef and Churu

    题意 : 给定一个长度为 nn 的序列和 mm 个区间。

    有两种操作:

    • 把序列中的第 xx 个数改为 yy
    • 求第 xx 个区间到第 yy 个区间的和 (的和)

将区间分块。维护 c[k][i]c[k][i] 表示在第 kk 块中第 ii 个位置被覆盖了多少次。

这样,在单点修改之后,能 O(n)O(\sqrt{n}) 地计算出各个块的和的变化。

查询时,对于散块暴力 O(n)O(\sqrt{n}) 次区间求和,使用 O(n)O(\sqrt{n}) 修改 O(1)O(1) 求和的分块即可。

  • 配合莫队使用 : 莫队小记

  • 可持久化块状数组

将上述三层 n\sqrt{n} 叉树可持久化即可。可以搞出“块状主席树”之类的东西。

例题 : P5574 [CmdOI2019]任务分配问题O(nn+nklogn)O(n\sqrt{n}+nk\log n)

  • Δ\color{blue}\bf\Delta 块状链表

解决一系列带插入的序列问题。(在带插入的序列上维护分块)

将某个元素插入某个块时,将该块重构。若某个块的大小超过 2B2B (或其他自定常数)则分裂为两个大小为 BB 的块。

这样,分裂次数是 O(n/B)O(n/B) 的,且能保证各个块的大小在 [B,2B][B,2B] 之间。

代码实现细节较多,具体见例题。

在远古时期这题的时限是 1s\texttt{1s} ,故几乎只有实现较好的分块能通过。

先考虑带单点修改区间K小值,若沿袭 P5356 的做法,复杂度为 O(qnlogn)O(q\sqrt{n}\log n) ,无法接受。

注意到本题的特殊性 : 值域较小(认为与 nn 同阶)。这启发我们使用值域分块。

维护序列块的前缀权值块状数组,差分能得到任意一端块的权值块状数组。再将散块 O(B)O(B) 建立一个权值块状数组。

在线性组合后的权值块状数组上,模仿主席树二分来爬,复杂度是 O(n)O(\sqrt{n}) 的。

在单点修改时,只会有 O(n/B)O(n/B)O(1)O(1) 的权值块状数组更新。

B=nB=\sqrt{n} 即可做到 O(nn)O(n\sqrt{n})

接下来考虑插入,只需多考虑分裂操作。

评测记录 | 代码Link

题意 : 维护一个序列,支持(在指定元素后面)插入元素,询问两个元素的先后顺序。

显然可以使用平衡树维护,回答询问时,求出两者的排名并比较。这样能做到 O(logn)O(\log n) 修改 O(logn)O(\log n) 查询。

考虑给每个元素一个标号,是的先后顺序和标号的大小顺序相对应。

给根一个值域区间,如 [0,1][0,1][0,2601][0,2^{60}-1],将点 uu 的权值设为自己区间的中点 mid=(u.l+u.r)/2mid=(u.l+u.r)/2,并令左右儿子的值域区间分别为 [u.l,mid],[mid,u.r][u.l,mid],[mid,u.r]

显然,这样得到的权值也满足左小右大的性质。若平衡树的最大高度为 hh ,则权值精度要求是 O(2h)O(2^h) 的。

使用重量平衡树(如替罪羊树)来维护这个序列,这样能够保证我们每次操作更新(并重新标记权值)的子树大小是 O(logn)O(\log n) 级别的。

而且,替罪羊树的树高严格 log1/αn\leq \log_{1/\alpha} n (一般不超过 3logn3\log n),故对精度的要求也较低(低于 O(n3)O(n^3)),使用 doublelong long 即可满足。

比较先后顺序时,只需比较标号即可,这样即可做到(均摊) O(logn)O(\log n) 修改 O(1)O(1) 查询。

  • 均摊 O(1)O(1)

将序列分成块状链表的形式,每一块的大小在 [logn,2logn][\log n,2\log n] 之间,适时分裂。

块外用替罪羊树维护。共产生 O(n/logn)O(n/\log n) 次分裂,故复杂度为 O(n)O(n)

比较时,先比较所在块的先后顺序,再比较块内的先后顺序。

块内使用链表维护,插入的元素的权值设置为前驱和后继权值的平均数,这样所需的值域也并不大。

  • 严格 O(1)O(1) : 先咕咕。

  • Δ\color{blue}\bf\Delta 四毛子 : P3793 由乃救爷爷

    题意 : 静态序列,查询区间 min\min

该问题的一个经典做法是 ST\rm ST 表,但需要 O(nlogn)O(n\log n) 的预处理时间。

考虑将原序列分为 O(logn)O(\log n) 一块,每一块预处理前/后缀 min\min ,整块的 min\min 上建立 ST\rm ST 表。

这样,若查询的区间端点异块,则使用散块前/后缀 min\min 拼上一段整块的 min\min 就能做到 O(1)O(1) 查询。

上面的复杂度是 O(n)O(1)O(n)-O(1) 的。

若区间端点同块,只能暴力,但是题目中一般不会出现这种情况。这样,我们就得到了一个简易的 O(n)O(1) rmqO(n)-O(1)\ \rm rmq 替代品。

递归套用本算法可以得到 O(nα(n))O(α(n))O(n\alpha (n))-O(\alpha (n)) 的做法,但不实用。

3. 根号分治 & 自然根号

有时,根号复杂度会产生于一些非常自然的问题中。

  • Δ\color{blue}\bf\Delta 根号分治

例 : 有若干数和为 nn ,则最多有 n/an/a 个数字大于 aa

若对于某个数可以 O(x)O(x) 处理,那我们就以 O(xa)O(xa) 的复杂度保证了其余数都 a\leq a

  • 例题① : 「Be Surrounded

    题意 : 维护一个 nn 个点 mm 条边的无向图,支持下列两种操作 :

    • 将点 uu 的权值 +y+y
    • 查询与 uu 相连的点的权值和

点度数总和是 O(m)O(m) 的,对度数进行根号分治。

对于度数 m\geq \sqrt{m} 的点,称为大点,反之称为小点。

在查询小点时暴力。修改时主动给周围的大点贡献,由于大点数目是 O(m)O(\sqrt{m}) 的,一次修改也不会超过 O(m)O(\sqrt{m})

总复杂度 O(qm)O(q\sqrt{m})

  • 例题② : P3396 哈希冲突

    题意 : 维护一个长为 nn 的序列 AA,支持下列操作 :

    • 单点修改
    • 查询 i=1n[imodx=y]Ai\sum\limits_{i=1}^n[i\bmod x=y] A_i

考虑 xnx\geq \sqrt{n} 的询问,显然可以 O(n)O(\sqrt{n}) 计算。

对于剩下每个询问 (x,y)(x,y) ,都维护其答案。在单点修改时,对于每个 xx 都有一个 yy 的答案改变,复杂度也是 O(n)O(\sqrt{n})

显然,最差情况是 n=1+2+3+...+mn=1+2+3+...+m ,和是 O(m2)O(m^2) 级别的,故 m=O(n)m=O(\sqrt{n})

  • Δ\color{blue}\bf\Delta 复杂度与 min\min 相关的询问

    mm 个集合,大小分别为 S1,S2...SmS_1,S_2...S_m。设 n=i=1mSin=\sum_{i=1}^mS_i

    qq 次对子询问,处理询问 (x,y)(x,y) 的复杂度为 O(min(Sx,Sy))O\big(\min(S_x,S_y)\big)

    若将询问记忆化,则总复杂度为 O(nq)O(n\sqrt{q})

证明 : 我们选出复杂度最大的 qq 个不同的询问进行求和。

SS 从大到小排序后的结果为 SS' ,复杂度最大的 qq 个不同的询问的复杂度和为 O(i=1qSi×i)O\Big(\sum_{i=1}^{\sqrt{q}}S_i\times i\Big)

最差情况是 SSO(q)O(\sqrt{q})O(n/q)O(n/\sqrt{q}) ,此时复杂度为 O(nq)O(n\sqrt{q})

例题 : P5576 [CmdOI2019]口头禅

  • Δ\color{blue}\bf\Delta 广义 SAM 上的一个算法

    对于(多串建立的)广义 SAM ,定义节点 uusizusiz_uparentparent 树子树内串的种类数。

    (即将输入中第 ii 个串的所有终止节点加上标记 ii ,问子树内不同标记个数)

    n=iSin=\sum_i|S_i| ,则 usizu\sum_u siz_uO(nn)O(n\sqrt{n}) 级别的。

证明 : 对于某个 SiS_i ,其贡献的复杂度显然为 O(min(Si2,n))O\big(\min(|S_i|^2,n)\big)

Si>n|S_i|>\sqrt{n} 时,这样的串只有 O(n)O(\sqrt{n}) 个,就算跑满 O(n)O(n) 的复杂度,也仅为 O(nn)O(n\sqrt{n})

Sin|S_i|\leq \sqrt{n} 时,复杂度不超过 O(Si2)O(nmaxSi)=O(nn)O(\sum |S_i|^2)\leq O(n*\max|S_i|)=O(n\sqrt{n})

也存在达到此上界的构造,可见 Itst : 广义 SAM 上每个模板串包含的节点数量和的上界以及构造

例题 : [Str记录]CF204E Little Elephant and Strings

  • Δ\color{blue}\bf\Delta SAM 上 endpos 集合的等差序列划分

    SAM\rm SAM 上每个节点的 endpos\rm endpos 集合划分为若干等差序列。等差序列的总个数为 O(nn)O(n\sqrt{n})

证明 :

  • lennlen\leq \sqrt{n} 的点 : 由于 lenlen 相同的点 edp\rm edp 集合不交,每个 lenlen 的集合总大小为 O(n)O(n) ,故这部分的集合总大小仅为 O(nn)O(n\sqrt{n})

  • len>nlen>\sqrt{n} 的点 : 若有两个相邻edp\rm edp 距离不超过 len/2len/2 ,则距离必然为最小循环节。

    由此,只有 >len/2>len/2 的空位才能隔断某个等差数列,故每个点的等差数列个数至多是 O(n/len)O(n)O(n/len)\leq O(\sqrt{n}) 的。

也存在达到此上界的构造,限于篇幅故不展示。

找出这些等差序列的方法暂不介绍,有题目之后再说。

  • Δ\color{blue}\bf\Delta Border 的分布

    一个串的 Border\rm Border 可以被划分为若干等差序列,其中公差 >B>B 的等差序列总大小 O(n/B)\leq O(n/B)

Border\rm Border 并非完全随意地分布为 O(logn)O(\log n) 个等差序列。

Border\rm Border 长度按照 [20,21),[21,22),...[2k,2k+1)...[2^0,2^1),[2^1,2^2),...[2^k,2^{k+1})... 分段后,可以证明每一段内至多只有一个等差序列。

故公差 >B>B 的等差序列总大小是 O(k(n/2k)/B)=O(n/B)O(\sum_{k}(n/2^k)/B)=O(n/B)

例题 : [Str记录]Loj#6681. yww 与树上的回文串

4. Ynoi 分块选做

题解都较为繁琐,故不在同一篇文章中展示。