分块
1. 简介
对于区间信息的整理,我们往往会用树状数组或者线段树,但在维护一些较为复杂的信息时,例如不满足区间可加、可减时就显得非常的吃力,代码实现起来也不那么简单、直观。
分块算法的基本思想是,通过适当的划分,预处理一些信息保存下来,用空间换取时间,从而达到时空平衡。
分块的思想是把数据分成若干块,为了时间复杂度的均摊,块的大小通常设置为 sqrt(n)
,以块为单位维护区间的值。
对于单点更新,时间复杂度 O(1)
区间更新,若区间不跨越块,直接暴力处理,时间复杂度 O(sqrt(n))
,若跨越块,则不能凑成一个整块的暴力处理,整块的直接更新,时间复杂度 O(sqrt(n))
。
区间查询,与区间更新一样,为O(sqrt(n))
。
2. 算法流程
2.1 建立分块 build
首先定义块操作的基本要素:
-
块的大小:用
block
表示。 -
块的数量:用
num
表示。 -
块的左右边界:用数组st[],en[],表示,st[i],en[i]就是第i块的起点与终点。
st[i] = (i − 1) ∗ block + 1; en[i] = i ∗ block
-
每个元素所属的块:belong[i]表示第i个元素属于哪个块。
void build(){
block = sqrt(n); //块的大小:每块有block个元素。
num = n / block; //块的数量:共分为t块
if (n % block)num++; // sqrt(n)的结果不是整数,最后加一小块
for (int i = 1; i <= num; i++){ //遍历块
st[i] = (i - 1) * block + 1;
ed[i] = i * block;
}
ed[num] = n; // sqrt(n)的结果不是整数,最后一块较小
for (int i = 1; i <= n; i++) //遍历所有元素的位置
belong[i] = (i - 1) / block + 1;
}
分块可能会有剩余的“小块”,对小块的处理是算法的关键。
2.2 区间修改 change
首先建立一个sum数组
来辅助区间求和。O(n)
for (int i = 1; i <= num; i++) //遍历所有的块
for (int j = st[i]; j <= ed[i]; j++) //遍历块i内的所有元素
sum[i] += a[j];
以[L,R]每个数加上一个d为例。
- [L,R]在某一块中,即一个块的“碎片”,将a[L],a[L+1] …… a[R]全部加上一个d。此时sum[]已经失效需要维护,
sum[i] = sum[i] + d * (R - L + 1)
。 - [L,R]跨越了多个块,对于完全被包含的块更新
add[i] += d
,对于两端未完全包含的块同上处理。
void change(int L, int R, int d){
int p = belong[L], q = belong[R];
if (p == q) { //情况1,计算次数是n / num
for (int i = L; i <= R; i++) a[i] += d;
sum[p] += d * (R - L + 1);
}
else{ //情况2
for (int i = p + 1; i <= q - 1; i++)
add[i] += d; //整块,有m=(q-1)-(p+1)+1个。计算m次
for (int i = L; i <= ed[p]; i++)
a[i] += d; //整块前面的碎片。计算n/num次
sum[p] += d * (ed[p] - L + 1);
for (int i = st[q]; i <= R; i++)
a[i] += d; //整块后面的碎片。计算n/num次
sum[q] += d * (R - st[q] + 1);
}
}
2.3 区间查询 ask
以求[L,R]的和为例。
- [L, R]在某个i块之内。暴力加每个数,最后加上add[i],答案是
ans = a[L] + a[L+1] + … + a[R] + (R - L + 1)*add[i]
。计算次数约为n / num
。 - [L, R]跨越了多个块。在被[L, R]完全包含的那些块内(设有k个块),
ans += sum[i] + add[i]*len[i]
,其中len[i]
是第i
段的长度,等于n / num
。对于不能完全包含的那些碎片,按情况1处理,然后与ans
累加。
long long ask(int L, int R){
int p = belong[L], q = belong[R];
long long ans = 0;
if (p == q){ //情况1
for (int i = L; i <= R; i++) ans += a[i];
ans += add[p] * (R - L + 1);
}
else{ //情况2
for (int i = p + 1; i <= q - 1; i++)
ans += sum[i] + add[i] * (ed[i] - st[i] + 1); //整块
for (int i = L; i <= ed[p]; i++)
ans += a[i]; //整块前面的碎片
ans += add[p] * (ed[p] - L + 1);
for (int i = st[q]; i <= R; i++)
ans += a[i]; //整块后面的碎片
ans += add[q] * (R - st[q] + 1);
}
return ans;
}
3. 例题讲解
题目描述
如题,已知一个数列,你需要进行下面两种操作:
将某区间每一个数加上 k。
求出某区间每一个数的和。
输入格式
第一行包含两个整数 n, m,分别表示该数列数字的个数和操作的总个数。
第二行包含 n 个用空格分隔的整数,其中第 i 个数字表示数列第 i 项的初始值。
接下来 m 行每行包含 3 或 4 个整数,表示一个操作,具体如下:
1 x y k:将区间 [x, y] 内每个数加上 k 。
2 x y:输出区间 [x, y] 内每个数的和。
【参考代码】
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const ll maxn = 1e5 + 7;
int n, m;
int a[maxn], block, num, sum[maxn] = {0}, add[maxn] = {0};
int st[maxn], ed[maxn], belong[maxn];
void build()
{
block = sqrt(n);
num = n / block;
if (n % block) num++;
for (int i = 1; i <= num; i++){
st[i] = (i - 1) * block + 1;
ed[i] = i * block;
}
ed[num] = n;
for (int i = 1; i <= n; i++)
belong[i] = (i - 1) / block + 1;
}
void change(int l, int r, int d)
{
int p = belong[l], q = belong[r];
if (p == q){
for (int i = l; i <= r; i++) a[i] += d;
sum[p] += d * (r - l + 1);
}
else{
for (int i = p + 1; i <= q - 1; i++)
add[i] += d;
for (int i = l; i <= ed[p]; i++)
a[i] += d;
sum[p] += d * (ed[p] - l + 1);
for (int i = st[q]; i <= r; i++)
a[i] += d;
sum[q] += d * (r - st[q] + 1);
}
}
ll ask(int l, int r)
{
int p = belong[l], q = belong[r];
ll ans = 0;
if (p == q){
for (int i = l; i <= r; i++) ans += a[i];
ans += add[p] * (r - l + 1);
}
else{
for (int i = p + 1; i <= q - 1; i++)
ans += sum[i] + add[i] * (ed[i] - st[i] + 1);
for (int i = l; i <= ed[p]; i++)
ans += a[i];
ans += add[p] * (ed[p] - l + 1);
for (int i = st[q]; i <= r; i++)
ans += a[i];
ans += add[q] * (r - st[q] + 1);
}
return ans;
}
signed main()
{
scanf("%d%d", &n, &m);
build();
for (int i = 1; i <= n; i++) scanf("%d", &a[i]);
for (int i = 1; i <= num; i++)
for (int j = st[i]; j <= ed[i]; j++)
sum[i] += a[j];
while (m--)
{
int op, x, y, k;
scanf("%d%d%d", &op, &x, &y);
if (op == 1) {
scanf("%d", &k);
change(x, y, k);
}
else printf("%lld\n", ask(x, y));
}
return 0;
}
//__asm__(R"(xxxxxxxxxxxx)");
题目序号 | 题目出处 | 题目难度 |
---|---|---|
1 | A Simple Problem with Integers | ⭐ |
2 | 蒲公英 | ⭐⭐⭐ |
3 | 磁力 | ⭐⭐⭐ |
4 | 小Z的袜子(hose) | ⭐⭐ |
5 | 开关 | ⭐⭐⭐ |
6 | 守墓人 | ⭐⭐ |
7 | Balanced Lineup G | ⭐⭐⭐ |
8 | XOR的艺术 | ⭐⭐⭐ |
9 | 质量检测 | ⭐⭐⭐ |