分块
分块算法被称为“优雅的暴力”,它是一种通过将数据分成若干个大小相等的“块”,从而平衡修改和查询时间复杂度的算法思想。
在信息学竞赛中,如果线段树、树状数组等高级数据结构难以实现,分块往往可以作为一种“保底”甚至首选方案。
核心思想:根号平衡
分块的核心在于一个简单的数学观察:如果将长度为 \(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 + 5], c[N], mode[B + 5][B + 5];
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;
}
例题:P10590 磁力块
平面上有 \(N \ (N \le 250000)\) 块磁石,每块磁石有坐标 \((x,y)\)、质量 \(m\)、磁力 \(p\) 和吸引半径 \(r\)。你手持一块磁石 \(L\) 位于 \((x_0,y_0)\) 保持不动,若磁石 \(A\) 满足以下条件,则可以吸引磁石 \(B\):
- \(\text{distance}(L, B) \le r_A\)
- \(m_B \le p_A\)
吸引过来的磁石可以作为新的吸引源继续吸引其他磁石,求最多能获得的磁石数量。
这是一个具有连锁反应的搜索问题,每获得一块新的磁石,它都可能吸引更多的磁石,这符合 BFS 的过程。
初始将磁石 \(L\) 加入队列,每次从队列中取出一个吸引源 \(A\),在剩余磁石中寻找满足条件的磁石 \(B\) 并加入队列。难点在于直接 BFS 的时间复杂度是 \(O(N^2)\),需要优化“寻找符合条件磁石”的过程。
要寻找同时满足距离限制(\(d \le r_A\))和质量限制(\(m \le p_A\))的磁石,这是一个二维偏序问题的变体,可以使用分块算法优化。
将所有磁石按到起点的距离平方 \(d^2\)进行升序排序,将排序后的序列分成约 \(\sqrt{N}\) 个块,记录每个块的最大距离平方 \(d^2_i\),在每个块内部,额外维护一份按 质量 \(m\) 升序排序的副本。
对于一个吸引源 \(A\),其磁力为 \(p\),吸引半径平方为 \(r^2\),遍历所有块:
- 完全覆盖块(\(d^2_i \le r^2\)):该块内所有磁石的距离都达标,只需在按质量排序的副本中,利用一个块内指针线性扫描 \(m \le p\) 的磁石。由于队列中的磁力不一定单调,但每个磁石只需被吸引一次,所以块内指针可以只增不减。
- 部分覆盖块(\(d^2_i \gt r^2\)):该块只有部分磁石距离达标,由于块是按距离排序的,只需遍历该块的前半部分,检查 \(d^2 \le r^2\) 且 \(m \le p\) 的磁石。一旦遇到 \(d^2 \gt r^2\),则该块及后续块的距离均不达标,直接结束。
参考代码
#include <cstdio>
#include <algorithm>
#include <cmath>
#include <queue>
using namespace std;
using ll = long long;
const int N = 250005;
const int B = 505;
struct Manget {
int m, p, id;
ll r2, d2;
};
Manget a[N], b[N];
bool used[N];
int bg[B], ed[B], ptr[B];
ll d2[B];
// 按距离平方排序
bool cmp_d2(const Manget& x, const Manget& y) {
if (x.d2 != y.d2) return x.d2 < y.d2;
return x.id < y.id;
}
// 按质量排序
bool cmp_m(const Manget& x, const Manget& y) {
if (x.m != y.m) return x.m < y.m;
return x.id < y.id;
}
int main()
{
int x0, y0, pl, rl, n;
// 读取起点坐标、初始磁力、半径和磁石数量
scanf("%d%d%d%d%d", &x0, &y0, &pl, &rl, &n);
ll rl2 = 1ll * rl * rl;
for (int i = 1; i <= n; i++) {
int x, y, m, p, r;
scanf("%d%d%d%d%d", &x, &y, &m, &p, &r);
a[i].m = m; a[i].p = p; a[i].id = i;
a[i].r2 = 1ll * r * r;
// 计算到起点的距离平方,避免开根号带来的精度问题
a[i].d2 = 1ll * (x - x0) * (x - x0) + 1ll * (y - y0) * (y - y0);
}
// 按距离排序并重新编号,确保 id 对应 a 数组下标
sort(a + 1, a + n + 1, cmp_d2);
for (int i = 1; i <= n; i++) a[i].id = i;
// 分块预处理
int sz = sqrt(n);
int num = (n + sz - 1) / sz;
for (int i = 1; i <= num; i++) {
bg[i] = (i - 1) * sz + 1;
ed[i] = min(i * sz, n);
d2[i] = a[ed[i]].d2;
ptr[i] = bg[i];
for (int j = bg[i]; j <= ed[i]; j++) b[j] = a[j];
// 块内按质量排序
sort(b + bg[i], b + ed[i] + 1, cmp_m);
}
// 放入初始磁石
Manget start;
start.p = pl; start.r2 = rl2;
queue<Manget> q;
q.push(start);
int cnt = 0;
while (!q.empty()) {
Manget cur = q.front(); q.pop();
int p = cur.p;
ll r2 = cur.r2;
for (int i = 1; i <= num; i++) {
if (d2[i] <= r2) {
// 完全在吸引半径内的块,按质量顺序寻找符合条件的磁石
while (ptr[i] <= ed[i] &&b[ptr[i]].m <= p) {
int id = b[ptr[i]].id;
if (!used[id]) {
used[id] = true;
q.push(a[id]);
cnt++;
}
ptr[i]++;
}
} else {
// 部分在吸引半径内的块,遍历该块中距离符合条件的磁石
for (int j = bg[i]; j <= ed[i]; j++) {
if (a[j].d2 > r2) break; // 距离超过半径,停止
int id = a[j].id;
if (!used[id] && a[j].m <= p) {
used[id] = true;
q.push(a[j]);
cnt++;
}
}
break; // 后续块的最大距离一定更大,直接结束块遍历
}
}
}
printf("%d\n", cnt);
return 0;
}
除了分块算法外,也可以使用线段树。
采用 BFS,将每个新获得的磁石作为吸引源存入队列。在每一轮 BFS 中,给定磁力 \(p\) 和半径 \(r\),目标是快速找到所有满足 \(m_B \le p\) 且 \(\text{distance}(L,B) \le r\) 的磁石 \(B\)。
可以利用其中一个维度(如质量)来确定搜索区间,利用另一个维度(如距离)在线段树内进行剪枝。
计算所有磁石到起点的距离平方 \(d^2\),将磁石按质量 \(m\) 从小到大排序。
在排序后的序列上建立线段树,每个节点维护该区间内所有磁石的 \(d^2\) 最小值。
对于当前吸引源 \(A(p,r)\),利用二分查找在质量排序数组中找到最大的位置 \(k\),使得 \(1 \dots k\) 的所有磁石都满足 \(m \le p\)。在区间 \([1,k]\) 内搜索所有满足 \(d^2 \le r^2\) 的磁石:如果当前节点的 \(d^2\) 最小值大于 \(r^2\),说明该子树内没有任何磁石满足距离条件,直接剪枝返回;否则,递归进入左右子树寻找;找到满足条件的叶子节点后,将其加入 BFS 队列。为了防止磁石被重复吸引,每当取出一块磁石,需要将其在线段树中的值修改为 \(\infty\),并向上更新。
每块磁石只会被提取并设为 \(\infty\) 一次,而根据剪枝逻辑,只有该区间内包含满足条件的磁石时才会向下递归,因此总体的时间复杂度为 \(O(n \log n)\)。
参考代码
#include <cstdio>
#include <algorithm>
#include <queue>
using namespace std;
using ll = long long;
const int N = 250005;
const ll INF = 4e18; // 足够大的值,表示磁石已移除
struct Magnet {
int m, p;
ll r2, d2;
};
Magnet a[N];
ll tr[N * 4];
queue<Magnet> q;
int ans;
bool used[N];
// 用于线段树的排序:按质量排序
bool cmp(const Magnet& x, const Magnet& y) {
if (x.m != y.m) return x.m < y.m;
return x.d2 < y.d2;
}
void pushup(int p) {
tr[p] = min(tr[p * 2], tr[p * 2 + 1]);
}
void build(int p, int l, int r) {
if (l == r) {
tr[p] = a[l].d2;
return;
}
int mid = (l + r) >> 1;
build(p * 2, l, mid);
build(p * 2 + 1, mid + 1, r);
pushup(p);
}
// 在区间 [x, y] 中寻找所有 d2 <= r2 的磁石,存入队列
void update(int p, int l, int r, int x, int y, ll r2) {
// 剪枝:如果当前区间最小值已经大于 r2,则无需继续
if (tr[p] > r2) return;
if (l == r) {
if (!used[l]) {
used[l] = true;
q.push(a[l]);
ans++;
tr[p] = INF; // 移除磁石
}
return;
}
int mid = (l + r) >> 1;
if (x <= mid) update(p * 2, l, mid, x, y, r2);
if (y > mid) update(p * 2 + 1, mid + 1, r, x, y, r2);
pushup(p);
}
int main()
{
int x0, y0, pl, rl, n;
scanf("%d%d%d%d%d", &x0, &y0, &pl, &rl, &n);
for (int i = 1; i <= n; i++) {
int x, y, m, p, r;
scanf("%d%d%d%d%d", &x, &y, &m, &p, &r);
a[i].m = m; a[i].p = p;
a[i].r2 = 1ll * r * r;
a[i].d2 = 1ll * (x - x0) * (x - x0) + 1ll * (y - y0) * (y - y0);
}
sort(a + 1, a + n + 1, cmp); // 按质量排序
build(1, 1, n);
Magnet start; start.p = pl; start.r2 = 1ll * rl * rl;
q.push(start);
ans = 0;
while (!q.empty()) {
Magnet cur = q.front(); q.pop();
int p = cur.p; ll r2 = cur.r2;
// 1. 二分找到质量在范围内的右边界
int l = 1, r = n, k = 0;
while (l <= r) {
int mid = (l + r) >> 1;
if (a[mid].m <= p) {
k = mid; l = mid + 1;
} else {
r = mid - 1;
}
}
// 2. 在 [1, k] 范围内通过线段树提取所有满足距离条件的磁石
if (k > 0) update(1, 1, n, 1, k, r2);
}
printf("%d\n", ans);
return 0;
}

浙公网安备 33010602011771号