分块

分块算法被称为“优雅的暴力”,它是一种通过将数据分成若干个大小相等的“块”,从而平衡修改查询时间复杂度的算法思想。

在信息学竞赛中,如果线段树、树状数组等高级数据结构难以实现,分块往往可以作为一种“保底”甚至首选方案。

核心思想:根号平衡

分块的核心在于一个简单的数学观察:如果将长度为 \(n\) 的数组分成若干块,每块的大小设为 \(B\),那么总块数就是 \(\dfrac{n}{B}\)

当进行区间操作时,包括两种处理:

  1. 整块处理:直接修改/查询该块维护的标记,效率极高。
  2. 散块处理:左右端点可能不完整覆盖整块,由于散块内部元素最多只有 \(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),维护:

  1. 前缀频率数组 \(s_{v,b}\),记录数值 \(v\) 在前 \(b\) 个块中出现的总次数。
  2. 块间众数数组 \(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;
}
posted @ 2026-05-04 15:42  RonChen  阅读(6)  评论(0)    收藏  举报