分块
分块算法被称为“优雅的暴力”,它是一种通过将数据分成若干个大小相等的“块”,从而平衡修改和查询时间复杂度的算法思想。
在信息学竞赛中,如果线段树、树状数组等高级数据结构难以实现,分块往往可以作为一种“保底”甚至首选方案。
核心思想:根号平衡
分块的核心在于一个简单的数学观察:如果将长度为 \(n\) 的数组分成若干块,每块的大小设为 \(B\),那么总块数就是 \(\dfrac{n}{B}\)。
当进行区间操作时,包括两种处理:
- 整块处理:直接修改/查询该块维护的标记,效率极高。
- 散块处理:左右端点可能不完整覆盖整块,由于散块内部元素最多只有 \(B\) 个,可以直接暴力遍历。
为了让“总块数”和“单块大小”达到平衡,通常取 \(B \approx \sqrt{n}\)。这样,单次操作的时间复杂度就被控制在 \(O(\sqrt{n})\)。
算法流程(以区间加法、区间求和为例)
A. 预处理
计算每个元素所属的块 ID,预处理每个块的统计信息(如块内元素总和 sum[block_id])。
B. 区间修改
要修改区间 \([L,R]\):
- 中间的整块:直接在块标记
add[block_id]上累加。 - 两端的散块:暴力修改原数组
a[i],并更新该块的sum[block_id]。
C. 区间查询
要查询区间 \([L,R]\):
- 中间的整块:直接调用预存的
sum[block_id]。 - 两端的散块:暴力累加原数组中的值,并加上该块的
add[block_id]带来的增量。
例题:P4168 [Violet] 蒲公英
给定一个长度为 \(n \ (n \le 40000)\) 的序列 \(a_i \ (a_i \le 10^9)\),支持 \(m \ (m \le 50000)\) 次在线查询:在区间 \([l,r]\) 内出现次数最多的数(众数)是谁?若有多个众数,输出编号最小的一个。
区间众数是一个经典的非信息可加性问题,即无法通过两个子区间的众数简单合并出大区间的众数。由于题目要求在线查询,通常使用分块算法来解决。
首先对原始数据进行离散化,将 \(10^9\) 范围的编号映射到 \([1,n]\) 范围内。
将序列分为约 \(\sqrt{n}\) 个块(本题中可取块大小为 200),维护:
- 前缀频率数组 \(s_{v,b}\),记录数值 \(v\) 在前 \(b\) 个块中出现的总次数。
- 块间众数数组 \(f_{i,j}\),记录从第 \(i\) 块到第 \(j\) 块这一段完整块区域内的众数。
预处理的时间复杂度为 \(O(n \sqrt{n})\)。
对于查询区间 \([l,r]\):如果 \(l,r\) 在同一块或相邻块,直接暴力统计区间内每个数的出现次数,找出众数;如果跨度较大,设中间完整块区域为 \([b_l + 1, b_r - 1]\),整个区间的众数只可能产生于三个来源,分别是中间完整块的众数 \(f_{b_l+1, b_r-1}\),左端散块中的数值,右端散块中的数值。
以中间块众数作为初始候选者,利用 \(s\) 数组计算其在中间块的频率。扫描左右两个散块,利用一个辅助数组 \(c\) 统计散块中每个数的频率。对于散块中的每个数,其全区间频率等于 \(c_v + (s_{v,b_r-1} - s_{v,b_l})\)。比较得出最终众数后,再次扫描散块元素将 \(c\) 数组归零(避免全量清零)。
这样每次查询的时间复杂度为 \(O(\sqrt{n})\),算法整体的时间复杂度为 \(O((n+m) \sqrt{n})\)。
参考代码
#include <cstdio>
#include <algorithm>
using namespace std;
const int N = 40005;
const int B = 200;
int n, a[N], num[N], ori[N], pos[N], bg[N], ed[N], pre[N][B], c[N], mode[B][B];
void init() {
// 1. 离散化
sort(num + 1, num + n + 1);
int sz = unique(num + 1, num + n + 1) - (num + 1);
for (int i = 1; i <= n; i++) {
int val = lower_bound(num + 1, num + sz + 1, a[i]) - num;
ori[val] = a[i];
a[i] = val;
}
// 2. 分块
int cnt = (n + B - 1) / B;
for (int i = 1; i <= n; i++) pos[i] = (i - 1) / B + 1;
for (int i = 1; i <= cnt; i++) {
bg[i] = (i - 1) * B + 1;
ed[i] = min(i * B, n);
}
// 3. 预处理前缀次数 pre[val][block]
for (int i = 1; i <= n; i++) pre[a[i]][pos[i]]++;
for (int i = 1; i <= sz; i++) {
for (int j = 1; j <= cnt; j++)
pre[i][j] += pre[i][j - 1];
}
// 4. 预处理块间众数 mode[i][j]
for (int i = 1; i <= cnt; i++) {
for (int v = 1; v <= sz; v++) c[v] = 0;
int maxc = 0, cur = 0;
for (int j = i; j <= cnt; j++) {
for (int k = bg[j]; k <= ed[j]; k++) {
c[a[k]]++;
if (c[a[k]] > maxc || (c[a[k]] == maxc && a[k] < cur)) {
maxc = c[a[k]];
cur = a[k];
}
}
mode[i][j] = cur;
}
}
for (int v = 1; v <= sz; v++) c[v] = 0;
}
int query(int l, int r) {
int bl = pos[l], br = pos[r], res = 0, maxc = 0;
if (br - bl <= 1) {
for (int i = l; i <= r; i++) {
int v = a[i];
c[v]++;
if (c[v] > maxc || (c[v] == maxc && v < res)) {
maxc = c[v];
res = v;
}
}
for (int i = l; i <= r; i++) c[a[i]] = 0;
} else {
// 初始候选者:中间完整块的众数
res = mode[bl + 1][br - 1];
maxc = pre[res][br - 1] - pre[res][bl];
// 统计散块元素在散块中的频率
for (int i = l; i <= ed[bl]; i++) c[a[i]]++;
for (int i = bg[br]; i <= r; i++) c[a[i]]++;
maxc += c[res]; // 加上初始众数在散块中的贡献
// 检查散块中的所有数
auto check = [&](int v) {
int tot = c[v] + (pre[v][br - 1] - pre[v][bl]);
if (tot > maxc || (tot == maxc && v < res)) {
maxc = tot;
res = v;
}
};
for (int i = l; i <= ed[bl]; i++) check(a[i]);
for (int i = bg[br]; i <= r; i++) check(a[i]);
// 复位 c 数组
for (int i = l; i <= ed[bl]; i++) c[a[i]] = 0;
for (int i = bg[br]; i <= r; i++) c[a[i]] = 0;
}
return ori[res];
}
int main()
{
int m; scanf("%d%d", &n, &m);
for (int i = 1; i <= n; i++) {
scanf("%d", &a[i]);
num[i] = a[i];
}
init();
int x = 0;
while (m--) {
int l0, r0; scanf("%d%d", &l0, &r0);
int l = (l0 + x - 1) % n + 1;
int r = (r0 + x - 1) % n + 1;
if (l > r) swap(l, r);
x = query(l, r);
printf("%d\n", x);
}
return 0;
}

浙公网安备 33010602011771号