分块&莫队学习笔记

分块与莫队算法学习笔记

前言

注:本文章经过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),对所有查询进行排序,排序规则:

  1. 按左端点所在块的编号升序排序;

  2. 若左端点所在块编号相同,则按右端点升序排序(偶数块可按右端点降序,优化常数,即“奇偶排序”)。

排序的目的是减少左右指针的移动次数,从而降低时间复杂度。

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. 分块练习题

2. 莫队练习题

posted @ 2026-03-14 19:43  神烦doge  阅读(44)  评论(0)    收藏  举报