分块&莫队学习笔记
分块与莫队算法学习笔记
前言
注:本文章经过ai修改
分块和莫队都属于“优雅的暴力”算法,与DFS、BFS这类遍历算法的核心区别的是:分块和莫队通过合理划分区间、优化遍历顺序,让暴力操作的复杂度大幅降低,甚至在部分场景下,复杂度优于线段树、树状数组等数据结构。
两者的核心优势的是通用性强、写法灵活,能处理一些线段树难以维护的操作;缺点是渐近复杂度略逊于线段树,但在大部分题目中足以通过,且代码实现更简洁易懂。
一、分块算法
分块的核心思想是“化整为零”:将一个长度为n的数组,划分成若干个长度相近的“块”,对每个块进行预处理,后续的区间查询、区间修改操作,通过“块内暴力、块间批量处理”的方式,将时间复杂度从暴力的O(n²)优化到O(√n),实现高效操作。
分块的时间复杂度由块长决定,通常通过均值不等式可求得最优块长(一般取√n),不同题目可根据需求微调块长以达到更优效率。相较于线段树、树状数组,分块的通用性更强,可维护一些特殊操作(如区间众数、区间第k大等)。
1.1 模板题示例
以 T348695 一个简单的整数问题2 为例,讲解分块的核心实现(区间加法、区间求和)。
1.2 核心原理与预处理
设块长为l,原数组为a,每个块的和为sum,pos[i]表示第i个元素所属的块编号,L[i]、R[i]分别表示第i个块的左右区间边界。
数组划分示意:
\(\underbrace{a_1,a_2,a_3,...,a_l}_{sum_1},\underbrace{a_{l+1},a_{l+2},...,a_{2l}}_{sum_2},\underbrace{a_{2l+1},a_{2l+2},...,a_{3l}}_{sum_3},...,\underbrace{a_{(l-1)*l+1},...,a_n}_{sum_{\lceil n/l \rceil}}\)
预处理(init函数):划分块、记录每个元素的块编号、计算每个块的初始和。
1.3 核心代码实现
预处理函数(init)
void init(){
int cnt = sqrt(n); // 最优块长,通常取√n
for(int i = 1 ; i <= cnt ; i ++){
L[i] = (i - 1) * cnt + 1;
R[i] = i * cnt;
}
// 处理最后一个不完整的块
if(R[cnt] < n){
cnt ++;
L[cnt] = R[cnt - 1] + 1;
R[cnt] = n;
}
// 给每个元素分配块编号,计算每个块的初始和
for(int i = 1 ; i <= cnt ; i ++){
for(int j = L[i] ; j <= R[i] ; j ++){
pos[j] = i;
sum[i] += a[j];
}
}
}
区间修改(区间[l, r]加c)
思路:判断l和r是否在同一个块内,若在则暴力修改;若不在,则批量修改中间的块(打懒标记),再暴力修改两端的块。
void modify(int l, int r, int c){
int p = pos[l], q = pos[r];
// 同一块,暴力修改
if(p == q){
for(int i = l ; i <= r ; i ++){
a[i] += c;
}
sum[p] += (r - l + 1) * c;
return ;
}
// 批量修改中间的块(打懒标记)
for(int i = p + 1 ; i < q ; i ++){
lz[i] += c;
}
// 暴力修改左端点所在块
for(int i = l ; i <= R[p] ; i ++){
a[i] += c;
}
sum[p] += (R[p] - l + 1) * c;
// 暴力修改右端点所在块
for(int i = L[q] ; i <= r ; i ++){
a[i] += c;
}
sum[q] += (r - L[q] + 1) * c;
}
区间查询(区间[l, r]求和)
思路:与区间修改一致,同块暴力求和,不同块则批量计算中间块的和(需加上懒标记的贡献),再暴力计算两端块的和。
int query(int l, int r){
int ans = 0;
int p = pos[l], q = pos[r];
// 同一块,暴力求和
if(p == q){
for(int i = l ; i <= r ; i ++){
ans += a[i];
}
ans += (r - l + 1) * lz[p];
return ans;
}
// 批量计算中间块的和(加上懒标记贡献)
for(int i = p + 1 ; i < q ; i ++){
ans += sum[i] + (R[i] - L[i] + 1) * lz[i];
}
// 暴力计算左端点所在块的和
for(int i = l ; i <= R[p] ; i ++){
ans += a[i];
}
ans += (R[p] - l + 1) * lz[p];
// 暴力计算右端点所在块的和
for(int i = L[q] ; i <= r ; i ++){
ans += a[i];
}
ans += (r - L[q] + 1) * lz[q];
return ans;
}
完整参考代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N = 1e5 + 10;
typedef long long LL;
typedef pair<int, int> PII;
int n, m;
int a[N];
int L[N], R[N];
int pos[N], sum[N];
int lz[N];
void init(){
int cnt = sqrt(n);
for(int i = 1 ; i <= cnt ; i ++){
L[i] = (i - 1) * cnt + 1;
R[i] = i * cnt;
}
if(R[cnt] < n){
cnt ++;
L[cnt] = R[cnt - 1] + 1;
R[cnt] = n;
}
for(int i = 1 ; i <= cnt ; i ++){
for(int j = L[i] ; j <= R[i] ; j ++){
pos[j] = i;
sum[i] += a[j];
}
}
}
void modify(int l, int r, int c){
int p = pos[l], q = pos[r];
if(p == q){
for(int i = l ; i <= r ; i ++){
a[i] += c;
}
sum[p] += (r - l + 1) * c;
return ;
}
for(int i = p + 1 ; i < q ; i ++){
lz[i] += c;
}
for(int i = l ; i <= R[p] ; i ++){
a[i] += c;
}
sum[p] += (R[p] - l + 1) * c;
for(int i = L[q] ; i <= r ; i ++){
a[i] += c;
}
sum[q] += (r - L[q] + 1) * c;
}
int query(int l, int r){
int ans = 0;
int p = pos[l], q = pos[r];
if(p == q){
for(int i = l ; i <= r ; i ++){
ans += a[i];
}
ans += (r - l + 1) * lz[p];
return ans;
}
for(int i = p + 1 ; i < q ; i ++){
ans += sum[i] + (R[i] - L[i] + 1) * lz[i];
}
for(int i = l ; i <= R[p] ; i ++){
ans += a[i];
}
ans += (R[p] - l + 1) * lz[p];
for(int i = L[q] ; i <= r ; i ++){
ans += a[i];
}
ans += (r - L[q] + 1) * lz[q];
return ans;
}
signed main(){
cin>>n>>m;
for(int i = 1 ; i <= n ; i ++){
cin>>a[i];
}
init();
while(m --){
char op;
int l, r, x;
cin>>op>>l>>r;
if(op == 'C'){
cin>>x;
modify(l, r, x);
}
else if(op == 'Q'){
cout<<query(l, r)<<'\n';
}
}
return 0;
}
二、莫队算法
莫队算法是一种基于分块思想的离线算法,核心用于解决区间查询类问题(如区间众数、区间不同元素个数、区间和等),尤其适用于“询问次数多、无法用线段树高效维护”的场景。
莫队的核心思想:将所有查询按照一定规则排序,通过“移动左右指针”的方式,从一个查询的区间转移到另一个查询的区间,每次转移时更新答案,最终实现所有查询的高效求解,时间复杂度约为O((n√n))。
2.1 模板题示例
以 【模板】莫队 / 小B的询问 为例,讲解莫队的核心实现(区间查询[L, R]内,每个数出现次数的平方和)。
2.2 核心原理
1. 分块排序
将数组分块(块长通常取√n),对所有查询进行排序,排序规则:
-
按左端点所在块的编号升序排序;
-
若左端点所在块编号相同,则按右端点升序排序(偶数块可按右端点降序,优化常数,即“奇偶排序”)。
排序的目的是减少左右指针的移动次数,从而降低时间复杂度。
2. 指针移动与答案更新
维护两个指针(l、r),初始均为0,以及一个答案变量ans、一个计数数组cnt(记录当前区间内每个数的出现次数)。
对于每个查询[L, R],通过移动l、r指针,将当前区间调整为[L, R],每次移动指针时,更新cnt数组和ans:
-
指针右移(r++):ans -= cnt[a[r]] * cnt[a[r]],cnt[a[r]]++,ans += cnt[a[r]] * cnt[a[r]];
-
指针左移(r--):ans -= cnt[a[r]] * cnt[a[r]],cnt[a[r]]--,ans += cnt[a[r]] * cnt[a[r]];
-
指针右移(l++):ans -= cnt[a[l]] * cnt[a[l]],cnt[a[l]]--,ans += cnt[a[l]] * cnt[a[l]];
-
指针左移(l--):ans -= cnt[a[l]] * cnt[a[l]],cnt[a[l]]++,ans += cnt[a[l]] * cnt[a[l]]。
核心逻辑:每次添加/删除一个元素时,先减去该元素原来出现次数的平方(避免重复计算),更新出现次数后,再加上新的平方值,保证ans始终是当前区间的答案。
2.3 核心代码实现
1. 结构体定义(存储查询)
// 存储每个查询的左端点、右端点、查询编号(用于离线输出答案)
struct Query{
int l, r, id;
}q[N];
2. 排序函数
int block; // 块长,取√n
// 莫队排序规则
bool cmp(Query a, Query b){
// 左端点所在块不同,按块编号升序
if(pos[a.l] != pos[b.l]) return pos[a.l] < pos[b.l];
// 左端点所在块相同,按右端点升序(偶数块可降序优化)
return a.r < b.r;
}
3. 指针移动与答案更新
int ans = 0; // 当前区间的答案
int cnt[N]; // 记录每个数的出现次数
int res[N]; // 存储每个查询的答案(按查询编号输出)
// 移动指针,更新答案
void add(int x){
ans -= cnt[x] * cnt[x];
cnt[x]++;
ans += cnt[x] * cnt[x];
}
void del(int x){
ans -= cnt[x] * cnt[x];
cnt[x]--;
ans += cnt[x] * cnt[x];
}
// 处理所有查询
void solve(){
int l = 1, r = 0; // 初始指针位置
for(int i = 1; i <= m; i++){
int ql = q[i].l, qr = q[i].r;
// 移动右指针,扩大区间
while(r < qr) add(a[++r]);
// 移动左指针,缩小区间
while(r > qr) del(a[r--]);
// 移动左指针,扩大区间
while(l > ql) add(a[--l]);
// 移动左指针,缩小区间
while(l < ql) del(a[l++]);
// 记录当前查询的答案
res[q[i].id] = ans;
}
}
4. 完整参考代码
#include<bits/stdc++.h>
using namespace std;
const int N = 5e4 + 10, M = 5e4 + 10;
int n, m;
int a[N];
int pos[N]; // 每个元素所属的块编号
// 存储查询
struct Query{
int l, r, id;
}q[M];
// 莫队排序规则
int block;
bool cmp(Query a, Query b){
if(pos[a.l] != pos[b.l]) return pos[a.l] < pos[b.l];
return a.r < b.r;
}
int ans = 0;
int cnt[N];
int res[M];
// 添加元素x,更新答案
void add(int x){
ans -= cnt[x] * cnt[x];
cnt[x]++;
ans += cnt[x] * cnt[x];
}
// 删除元素x,更新答案
void del(int x){
ans -= cnt[x] * cnt[x];
cnt[x]--;
ans += cnt[x] * cnt[x];
}
int main(){
cin >> n >> m;
block = sqrt(n); // 块长取√n
// 初始化每个元素的块编号
for(int i = 1; i <= n; i++){
cin >> a[i];
pos[i] = i / block;
}
// 读入所有查询,记录查询编号
for(int i = 1; i <= m; i++){
cin >> q[i].l >> q[i].r;
q[i].id = i;
}
// 对查询排序
sort(q + 1, q + 1 + m, cmp);
// 处理所有查询
int l = 1, r = 0;
for(int i = 1; i <= m; i++){
int ql = q[i].l, qr = q[i].r;
while(r < qr) add(a[++r]);
while(r > qr) del(a[r--]);
while(l > ql) add(a[--l]);
while(l < ql) del(a[l++]);
res[q[i].id] = ans;
}
// 按查询编号输出答案
for(int i = 1; i <= m; i++){
cout << res[i] << endl;
}
return 0;
}
2.4 莫队的优化与拓展
-
奇偶排序:左端点所在块为奇数时,右端点升序;为偶数时,右端点降序,减少指针移动次数;
-
带修莫队:新增一个时间维度,处理区间修改+区间查询的问题,块长取n(2/3),复杂度O(n(5/3));
-
树上莫队:将树转化为线性序列(如欧拉序),再用莫队处理区间查询,适用于树上路径查询问题。
三、相关练习题(按知识点分类)
1. 分块练习题
-
1. 一个简单的整数问题2(模板题)
-
2. 弹飞绵羊(分块维护动态区间)
-
3. 蒲公英(区间众数,分块预处理)
-
4. 教主的魔法(区间更新+区间查询第k大)
2. 莫队练习题
-
1. 【模板】莫队 / 小B的询问(模板题)
-
2. 小Z的袜子(经典莫队,区间概率问题)
-
3. 小清新人渣的本愿(莫队+容斥,区间不同元素相关)
-
4. Rmq Problem / mex(莫队维护区间mex)

浙公网安备 33010602011771号