写在前面
在遇到数据结构题时,我们经常因那些复杂的数据结构的巨大码量而感到头疼。
而此时有一个更简便,易想且易写,而且思路更清晰的方法----分块。
分块主要的思想便是平衡复杂度,现在我们由浅入深的来了解这个算法。
我们先考虑把分块分为两个部分–静态分块和动态分块。
静态分块:放一些关键点,预处理关键点到关键点的信息来加速查询的,不能支持修改。
动态分块:是把序列分为一些块,每块维护一些信息,可以支持修改。
一.序列分块
序列分块应该是最常见的一种。分块,顾名思义,我们考虑将要处理的序列分成
一个个块来进行处理。(设分成
s
s
s个块)
如图,如果我们要对中间这部分进行处理,我们会怎么做。
首先,我们会对两边的散块进行处理,复杂度为 O ( n / s ) O(n/s) O(n/s)。
接下来我们要处理中间的块,我们可以整块整块的处理,复杂度为 O ( s ) O(s) O(s)。
显然在 s s s取 O ( n ) O(\sqrt{n}) O(n)时最优,复杂度为 O ( n ) O(\sqrt{n}) O(n)。
现在问题在于我们如何快速地处理整块,举一个区间查+区间改的例子。
我们可以对于每一个块都维护一个块内的和,已经打一个tag记录块。
-
对于散块,下传标记再操作,操作完后,我们重新计算这个块的和。
-
对于整块,我们直接对和和tag进行处理,并不需要一个一个地取操作。
分析完后我们得到了对于序列分块的基本步骤一般是:
修改:对散块重构,对整块打tag。
查询:对散块直接暴力查,整块一块一块统计。
先考虑四种序列分块的基本操作
-
O ( 1 ) O( 1 ) O(1)单点修改, O ( n ) O( \sqrt{n} ) O(n)区间和
-
O ( n ) O( \sqrt{n} ) O(n)单点修改, O ( 1 ) O(1) O(1)区间和
-
O ( 1 ) O( 1 ) O(1)区间加, O ( n ) O( \sqrt{n} ) O(n)查单点
-
O ( n ) O( \sqrt{n} ) O(n)区间加, O ( 1 ) O(1) O(1)查单点
用上面的方法都很容易实现了。
然后我们就可以解决一些问题了。
先来一个比较简单的问题:
经典问题
- 维护一个序列
- 1.区间加
- 2.查询区间小于 x x x的数个数
分块,维护每块的排序后的数组
每次区间加的时候,整块可以打一个标记,零散块可以重构。
每次查询的时候,整块查询小于 x x x的数,这个整块的标记为 y y y(也就是说这一块所有数都加了 y y y)
则等价于查整块的排序后的数组里面小于
x
−
y
x-y
x−y的数的个数,这个可以二分。
零散块就直接暴力查询块内在查询区间内的数是否满足条件
设有 s s s个块
查询复杂度:
整块
O
(
l
o
g
n
x
)
∗
x
O( log{\frac{n}{x}} ) * x
O(logxn)∗x,零散块
O
(
n
x
)
O( \frac{n}{x} )
O(xn)
修改复杂度:
整块
O
(
1
)
O( 1 )
O(1),零散块
O
(
n
x
)
O( \frac{n}{x} )
O(xn) (重构的时候用归并)
按照根号平衡算一算可以发现:
总复杂度
O
(
m
n
l
o
g
n
)
O( m \sqrt{ nlogn } )
O(mnlogn)
此时块大小为
n
l
o
g
n
\sqrt{ nlogn }
nlogn
P3373 【模板】线段树 2
已知一个数列,你需要进行下面三种操作:
-
将某区间每一个数乘上 x x x
-
将某区间每一个数加上 x x x
-
求出某区间每一个数的和(模p意义下,p在最开始给定)
1 ≤ n ≤ 100000 , m ≤ 100000 1 \leq n \leq 100000,m \leq 100000 1≤n≤100000,m≤100000
这道题是线段树模板,但是分块也可以做。
Sol:
考虑我们的对于一个块维护哪些东西。
显然要维护一个区间和,但是tag如何取维护呢,我们维护两个tag,taga
和tagc
,显然在先乘后除意义下这两者更容易维护。
-
对于所有操作,散块的处理都是暴力重构。
-
整块:
操作一:给taga
tagc
sum
都乘上 x x x。
操作二:给taga
加上 x x x,sum
加上 l e n × x len \times x len×x。
操作三:直接查sum
。
实测分块不比线段树慢多少(评测记录)
上面的题目都属于动态分块,下面我们来看一道静态分块的题目。
bzoj2906颜色
给定一个长度为 N N N的颜色序列 C C C,对于该序列中的任意一个元素 C i C_{i} Ci,都有 1 ≤ C i ≤ M 1 \leq Ci \leq M 1≤Ci≤M。
对于一种颜色 C o l o r K ColorK ColorK来说,区间 [ L , R ] [L,R] [L,R]内的权值定义为这种颜色在该区间中出现的次数的平方,即区间 [ L , R ] [L,R] [L,R]内中满足 C i = C o l o r K C_{i}=ColorK Ci=ColorK的元素个数的平方。
接下来给出 Q Q Q个询问,询问区间 [ L , R ] [L,R] [L,R]内颜色 [ a , b ] [a,b] [a,b]的权值总和。
1 ≤ N , Q ≤ 50000 , M ≤ 20000 1 \leq N,Q \leq 50000,M \leq 20000 1≤N,Q≤50000,M≤20000
看到数据范围我们就能大概确定是分块。
Sol:
首先我们可以用差分把询问转化为问区间 [ L , R ] [L,R] [L,R]内颜色 [ 1 , x ] [1,x] [1,x]的权值总和
考虑把整个序列分成 s s s个块。
但我们发现分完块并不好用上文所说的套路直接进行处理,因为我们并不好合并两个块。
那既然不好合并两个块,我们为什么不直接把 i i i到 j j j块的答案全部算出来呢。
即考虑预处理出一个数组
f
i
,
j
,
k
f_{i,j,k}
fi,j,k表示第
i
i
i到第
j
j
j个块直接颜色
[
1
,
k
]
[1,k]
[1,k]的答案。
显然这部分复杂度是
O
(
s
2
×
M
)
O(s^2 \times M)
O(s2×M)。
再考虑如何加入散块中的数,对于一个数
x
x
x,直接加上贡献
2
×
c
n
t
x
+
1
2 \times cnt_{x}+1
2×cntx+1就好了。
复杂度
O
(
n
s
×
Q
)
O(\frac{n}{s} \times Q)
O(sn×Q)
总复杂度 O ( s 2 × M + n s × Q ) O(s^2 \times M+\frac{n}{s} \times Q) O(s2×M+sn×Q)。
s s s取 M 1 3 M^{ \frac{1}{3}} M31能够通过
P4168 [Violet]蒲公英
和上道题差不多,太经典了,题解肯定比我讲得好。
这两道题告诉我们在静态分块的题目中预处理出块间的东东或许是个好方法。
P3206 [HNOI2010]城市建设
给定一张图 n n n个点 m m m条边,每条边有一个权值。
q q q组询问,每组修改一条边的权值并询问图上最小生成树的权值和。
1 ≤ n ≤ 2 × 1 0 4 1\leq n\leq 2\times 10^4 1≤n≤2×104, 1 ≤ m , q ≤ 5 × 1 0 4 1\leq m,q\leq 5\times 10^4 1≤m,q≤5×104
Sol:
神仙题。
我们考虑现在的图和原图的差别将整张图分为两种边:静态边和动态边
-
动态边:和原图权值不一样的边(设有 g g g条)
-
静态边:和原图权值一样的边
考虑求MST,我们的步骤:
- 先将动态边权值设为-INF跑一次最小生成树,那么树上边一定是必选的。
- 再将动态边权值设为INF跑一次最小生成树,那么不在树上边一定是不选的。
这样跑一次之后我们就会发现我们静态边中不确定是否选择的边就只剩下了 O ( g ) O(g) O(g)条。
我们考虑对询问序列进行分块。(设分成 s s s块)
显然一个块内动态边只有
O
(
n
s
)
O( \frac{n}{s})
O(sn)条。
对于每一块我们考虑先在开头按我们的方法算两次MST。
然后在整个块当中就只有 O ( n s ) O( \frac{n}{s}) O(sn)条不确定的边,然后每次暴力并在这些边里面修改跑一次kruskal次即可。
块头求MST的总复杂度是
O
(
s
×
m
l
o
g
m
)
O( s \times mlogm )
O(s×mlogm)
每次求答案总复杂度
O
(
q
×
n
s
l
o
g
n
)
O(q \times \frac{n}{s}logn)
O(q×snlogn)
实测在块长为
10
×
n
\sqrt{10 \times n}
10×n时比较好,可以通过。
评测记录
二.莫队
我们先来了解几种基础的莫队。
1.普通莫队
我们用一道模板题来引入普通莫队。
如果我们直接暴力的话,我们每次询问的复杂度都是
O
(
n
)
O(n)
O(n),显然是不可以接受的。
但是我们发现每次询问我们都做了很多相同的事情,那我们能不能把所有询问一起做来化简这个过程呢。所以我们得离线。
我们发现其实如果知道了 [ l , r ] [l,r] [l,r]可以 O ( 1 ) O(1) O(1)知道 [ l , r + 1 ] [l,r+1] [l,r+1], [ l − 1 , r ] [l-1,r] [l−1,r], [ l + 1 , r ] [l+1,r] [l+1,r], [ l , r − 1 ] [l,r-1] [l,r−1]。
考虑把我们的询问放到平面上,我们是可以
O
(
1
)
O(1)
O(1)上下左右动的。
大概长这样
我们是从原点,要经过所有点,尽量走的短,我们看一下可以怎么走。
考虑将x轴分成
n
\sqrt{n}
n个块,然后在块内,我们从下往上走。块间直接从上面下来就好了。
如何用代码来实现呢,实际上我们只需要把询问排序然后一个个往后跑就可以了。
bool operator<(const node &x)const
{
if(x.l/unit!=l/unit)return l<x.l;
return (l/unit)&1?r<x.r:r>x.r;
}
往后跑直接4个循环就好了,注意顺序!就先扩展
while(l>q[i].l)add(a[--l]);
while(r<q[i].r)add(a[++r]);
while(l<q[i].l)del(a[l++]);
while(r>q[i].r)del(a[r--]);
现在我们来计算一下我们这种方法的复杂度。
考虑左右的移动:每次移动都是
O
(
n
)
O( \sqrt{n} )
O(n)的,移动
O
(
n
)
O(n)
O(n)次,总复杂
O
(
n
n
)
O( n \sqrt{n})
O(nn)
考虑上下的移动:单块块间的上下移动是
O
(
n
)
O(n)
O(n),总复杂度
O
(
n
n
)
O( n \sqrt{n})
O(nn) ,块间的上下移动
O
(
n
)
O(n)
O(n),总复杂度也是
O
(
n
n
)
O( n \sqrt{n})
O(nn)。
故总复杂度 O ( n n ) O( n \sqrt{n}) O(nn)。
我们发现对于块间的上下移动,我们每次上去了都要一下去又跑下来,不是很好。我们可以对于奇数的块从下往上,偶数的块从上往下,从而减少常数。
2.带修莫队
现在这道题带修改了,我们任何处理呢。
直接加一个时间维就好了。
我们先把询问拍平到l,r维的平面上,因为我们还有一维t,所以我们把两位都分块。
图好像画错了,画出了l>r的点,但是我懒得改了
假设我们把两维都分成了 s s s个块,在块内我们还是按照原来直接从小到大的动,我们看复杂度会是多少。
t维的移动:
O
(
s
2
n
)
O(s^2n)
O(s2n)。
l,r维的移动:
O
(
n
2
s
)
O( \frac{n^2}{s})
O(sn2)。
在 s s s等于 n 1 3 n^{ \frac{1}{3}} n31时复杂度为 O ( n 5 3 ) O(n^{ \frac{5}{3}}) O(n35)。
3.回滚莫队
我们发现对于这道题,我们知道 [ l , r ] [l,r] [l,r]之后我们 O ( 1 ) O(1) O(1)只能知道 [ l − 1 , r ] [l-1,r] [l−1,r]和 [ l , r + 1 ] [l,r+1] [l,r+1]。
我们就不能那样四个循环的做了。怎么办呢?
我们操作的过程大概是这样
这样做我们可以在l维只往左,而r轴只往上,大概代码长这样。
while(r<q[pos].r)
{
r++;
加入r的操作;
}
while(l>q[pos].l)
{
l--;
加入l的操作;
}
还原到原来的l
这样我们就可以做到不删除只加入了,具体实现看代码。
好了,讲完了几个比较常见的莫队,我们来看一些莫队的题目。
查询区间 [ l , r ] [l,r] [l,r]中值在 [ a , b ] [a,b] [a,b]内的不同数个数
n ≤ 1 e 5 , m ≤ 1 e 6 n \leq 1e5 , m \leq 1e6 n≤1e5,m≤1e6
sol:
首先可以跑个莫队,维护一个树状数组。
复杂度O( n m l o g n n \sqrt{m}logn nmlogn )
能不能做到更优呢?
我们注意到: 我们在树状数组上的修改次数为 O ( n m ) O(n \sqrt{m}) O(nm),而查询次数只有 O ( m ) O(m) O(m)。
于是我们可以使用序列分块中提到的 O ( n ) O( \sqrt{n}) O(n)修改, O ( 1 ) O(1) O(1)查询来处理。
这样我们可以做到 O ( m n ) O( m \sqrt{n} ) O(mn)。
给一个树,
n
n
n 个点,有点权,初始根是
1
1
1
m
m
m 个操作,每次操作:
- 将树根换为 x x x
- 给出两个点 x x x, y y y,从 x x x 的子树中选每一个点, y y y的子树中选每一个点,如果两个点点权相等, a n s + + ans++ ans++,求 a n s ans ans
Sol:
肯定是按照DFS序转换为区间查询。
两个区间不好维护,但是这个信息具有可减性,可以考虑差分。
按照DFS序转换为区间查询,然后考虑差分。
[
l
1
,
r
1
]
−
[
l
2
,
r
2
]
[l1,r1] - [l2,r2]
[l1,r1]−[l2,r2]的询问可以差分为:
F
(
l
1
,
r
1
,
l
2
,
r
2
)
=
F
(
1
,
r
1
,
1
,
r
2
)
−
F
(
1
,
l
1
−
1
,
1
,
r
2
)
−
F
(
1
,
r
1
,
1
,
l
2
−
1
)
+
F
(
1
,
l
1
−
1
,
1
,
l
2
−
1
)
F(l1,r1,l2,r2)=F(1,r1,1,r2)-F(1,l1-1,1,r2)-F(1,r1,1,l2-1)+F(1,l1-1,1,l2-1)
F(l1,r1,l2,r2)=F(1,r1,1,r2)−F(1,l1−1,1,r2)−F(1,r1,1,l2−1)+F(1,l1−1,1,l2−1)
这样都变成了前缀的区间,就可以在这个上面跑莫队了。
4.二次离线莫队
对于大部分题目,伸缩区间的时间复杂度为 O ( 1 ) O(1) O(1)。如果无法在线性时间内伸缩区间,我们可使用莫队二次离线优化时间复杂度。
考虑为什么无法快速伸缩区间:新增的位置对整个区间的贡献和区间内每个数都有关,需要用数据结构维护。通常这样的信息是 可减 的。因此,恰当地差分可以将贡献的形式写得更加整洁,从而通过再次离线求解。
通过扫描线,再次将更新答案的过程离线处理,降低时间复杂度。假设更新答案的复杂度为 O ( k ) O(k) O(k),它将莫队的复杂度从 O ( n k n ) O(nk\sqrt n) O(nkn)降到了 O ( n k + n n ) O(nk +n\sqrt n) O(nk+nn),大大简化了计算。
这道题我们就可以使用这种方法。
我们先模拟一次莫队的计算,然后把要伸缩的东东离线下来。
比如我们在扩展 [ l , r ] [l,r] [l,r]到 [ l , r + 1 ] [l,r+1] [l,r+1]时,我们的贡献就是 r + 1 r+1 r+1对 [ l , r ] [l,r] [l,r]的贡献,差分一下就是 r + 1 r+1 r+1对 [ 1 , r ] [1,r] [1,r]的贡献- r + 1 r+1 r+1对 [ 1 , l − 1 ] [1,l-1] [1,l−1]的贡献。
离线下来之后我们要计算的就是 O ( n n ) O(n \sqrt{n}) O(nn)个这样的贡献,直接从前往后扫,利用异或运算的交换律,开一个桶t,t[i]表示当前前缀中与i异或有k个数位为1的数有多少个。
总复杂是 O ( n n + ( 14 k ) n ) O(n \sqrt{n}+ \binom{14}{k}n) O(nn+(k14)n)。但是空间复杂度是 O ( n n ) O(n \sqrt{n}) O(nn)会炸掉。
考虑一次移动时,这些贡献的共性,首先前面一坨的贡献统统是 r + 1 r+1 r+1对 [ 1 , r ] [1,r] [1,r]的形式可以预处理出来。
而后面的那一坨在一次移动时,实际上 r + 1 r+1 r+1对 [ 1 , l − 1 ] [1,l-1] [1,l−1]的 l − 1 l-1 l−1并未变化,可以一次移动统一记录这样空间复杂度就是 O ( n ) O(n) O(n)可以通过。