分块
分块思想
一、性质与证明
分块,故名思义,是将一个区间分成几个块,然后对于每个询问,整合一个或多个(甚至全部区间)的信息,但这种分块和整合是有技巧性的,否则很难有效地降低时间复杂度。
先来看一道例题:
老方有一个长度为 n
的序列,被她的学生拜托完成以下三个操作:
- 修改某位置的元素值
- 将一段区间的元素加上或减去一个值
- 求一个区间内的最大最小值
共有 m
个这样的操作,保证 n, m <= 50000
考虑分块。
我们考虑将整个序列划分为 sqrt(n)
块,因为这样可以使得总查询时间最短。
证明如下:
设这个序列的完整分块数是 C
块,每一个完整个分块都包含 S
个元素,那么显然可能存在一种情况即在区间 [l, r]
的两端包含有不完整的分块。
如图,蓝色部分即为不完整的分块。
可以发现该序列内有 C
个完整分块和两个不完整分块,且这两个不完整分块内元素数量之和小于 2S
,则对于一个序列而言,我们要进行的查询总数的极限为 C + 2S
,则时间复杂度为 O(C + 2S)
,也就相当于 O(max{C, S})
.
因此,由于 C * S + 2 * S >= r - l + 1
,则可以近似的看作当且仅当 S == C
时,时间复杂度可以取得最小上界, 此时 C == S == sqrt(n)
.
二、具体实现
初始化
考虑用 belong[]
来表示每个点与分块之间的关系。
为了便于阅读,除最终整合实现的代码外,其余代码块一律省略头文件。
且在 vscode 中,如出现 "end"不明确
这一类的报错,可以在相应变量名前加 ```::`` 即区域标识符。
#define MAXN 50000 + 5
int n, a[MAXN];
int S, C = 0, st[MAXN], end[MAXN]; // st[]:每个块的左边界,end[]:每个块的右边界
int belong[MAXN]; // 记录每个元素与所分块的关系
void init() {
S = int(sqrt(double(n)));
for(int i = 1; i <= n; i += S) {
st[++C] = i;
end[C] = min(n, i + S - 1);
}
/*
也可以写成:C = n / S; if(n % S) C++;
*/
for(int i = 1; i <= C; i++) {
for(int j = st[i]; j <= end[i]; j++) {
belong[j] = i;
/*
我们还可以在这里进行其他一系列区间操作:
sum[i] += a[j];
maxx[i] = max(maxx[i], a[j]);
*/
}
}
}
接下来是区间修改和查询操作。似乎每个数据结构都绕不过这几个
修改操作
考虑对于原序列的单点 x
修改除了会影响到 a[x]
,还会影响到所在分块的和、最大值等系列区间信息。实际上很好实现。
inline void updata(int x, int y) {
int home = belong[x];
a[x] += y;
sum[home] += y;
return;
}
查询操作
考虑分治,分两种情况考虑:a. [x, y] 在一个块内;b.[x, y]不在一个块内。
a. [x, y] 在一个块内
如图所示,要查询区间[x, y]的信息,非常简单,直接暴力即可。
if(belong[x] == belong[y]) {
int ans = 0;
for(int i = x; i <= y; i++) {
ans += a[i];
}
return ans;
}
b.[x, y]不在一个块内
如图所示,我们发现,区间[x, y]的和即就是 [x, 绿色块1尾] + 蓝色块 + [绿色块2头,y]
。
所以我们可以将绿色部分暴力,蓝色部分直接查询。
if(belong[x] != belong[y]) {
for(int i = x; i <= ::end[belong[x]]; i++) {
ans += a[i];
}
for(int i = belong[x] + 1; i <= belong[y] - 1; i++) {
ans += sum[i];
}
for(int i = st[belong[y]]; i <= y; i++) {
ans += a[i];
}
return ans;
}
这样便可以完美的解决查询问题。
三、相应习题
实际上,这两道题都是板子题,只要大家看懂了上面的知识,这两道题乱杀。
四、后话
老方很开心,因为你们帮她解决了一个大难题.......(逃)
其实分块的时间复杂度为 O(sqrt(n) * m)
,慢于树状数组和线段树,同学们可以在掌握分块思想后去学习树状数组和线段树。
注意,分块是种思想!