块状数组

发布于

一、块状数组的引入

这一节课中,我们还是来解决区间类的问题,不过这一次,我们会把整个序列划分为若干个小块,对整块整体操作,对零散的块单独处理。这就是分块的思想。

假设序列的长度为 n,操作的数量为q。块状数组就是把原序列划分成了B块,每块的大小为⌊n/B⌋,在询问的时候,对于整块的区间整体处理,对于零散的块单独处理,时间复杂度为,O(B+⌊n/B⌋),其中 B sqrt(n) 时候复杂度最优。

分块实质上就是三层的树,每个非叶子结点的节点有sqrt(n)个子节点。

我们为了能够整体处理完整的区间,所以在需要根据题目的要求,在第二层的块上,维护一些信息,最常见的有处理区间加法的lazy标记等等。

我们来看一个例子,我们如果想要对某一段区间进行在线操作(一边修改一边查询),每

次操作对某个区间lr加上c,然后再进行查询区间[1r]的和,我们可以怎么做呢?

二、块状数组的代码实现

1.    确定每一个块的大小,以及块的左端点和右端点,同时明确每一个点属于哪一个区间。

int L[N], R[N], belong[N];
//L[i] 表示第i个点的左端点
//R[i] 表示第i个点的右端点
//belong[i] 表示i点属于belong[i]这个快

void init(){
    len = sqrt(n);    //每个块的长度
    num = (n - 1) / len + 1;    //一共有num个块
    //对于每一个快,确定左右边界
    for(int i = 1; i <= num; i++){
        L[i] = R[i - 1] + 1;
        R[i] = i * len;
    }
    R[num] = n;
    //对于每一个i,明确她属于哪一个块
    for(int i = 1; i <= n; i++){
        belong[i] = (i - 1) / len + 1;
    }
}

2.把每个数读取进来,并且求出块内元素和。

long long sum[N], lazy[N];
//sum[i] 表示第i个块的块内元素和
//lazy[i] 表示第i个块的块标记

cin >> n;
init();
for(int i = 1; i <= n; i++){
    cin >> a[i];
    //统计块内元素和
    sum[belong[i]] += a[i];
}

3.对于修改操作,我们首先需要确定的是需要修改的区间[lr]的左右端点,分别是在哪个块中。

其次按照我们之前所说的原则,零散的块单独处理修改原数组的值,更新块内元素和。完整的块整体处理,更新块标记和块内元素和。

void update(int l, int r, int c){
    //l属于bl这个块,r属于br这个块
    int bl = belong[l], br = belong[r];
    //在同一个块中,直接暴力处理
    for(int bl == br){
        //修改元素
        for(int i = 1; i <= r; i++){
            a[i] += c;
        }
        //更新块内元素和
        sum[bl] += (r - l + 1)  c;
        return;
    }
    //[l, r[bl] 零散的块,修改元素,更新块内元素和
    //[bl + 1, br - 1] 完整的块 更新块标记,更新块内元素和
    //[l[br], r] 零散的块,修改元素,更新块内元素和
    for(int i = 1; i <= R[bl]; i++){
        a[i] += c;
    }
    sum[bl] += (R[bl] - l + 1)  c;
    
    for(int i = bl + 1; i <= br - 1; i++){
        lazy[i] += c;
        sum[i] += len  c;
    }
    
    for(int i = L[br]; i <= r; i++){
        a[i] += c;
    }
    sum[br] += (r - l[br] + 1)  c;
}

5.对于查询操作,我们同样需要先确定的是需要修改的区间[l,r]的左右端点,分别是在哪个块中。之后对于零散的块单独处理,除了要加上原数组的值,还要加上块标记。完整的块整体处理,只需要加上块内元素和即可。

long long query(int l, int r){
    //l属于bl这个块,r属于br这个块
    int bl = belong[l], br = belong[r];
    //在同一个块中,直接暴力处理
    long long res = 0;
    if(bl == br){
        for(int i = 1; i <= r; i++){
            res += a[i];
        }
        res += (r - l + 1)  lazy[bl];
        return res;
    }
    ////[l, r[bl] 零散的块,暴力累加,再加上块标记
    //[bl + 1, br - 1] 完整的块 返回块内元素和
    //[l[br], r] 零散的块,暴力累加,再加上块标记
    for(int i = 1; i <= R[bl]; i++){
        res += a[i];
    }
    res += (R[bl] - l + 1)  lazy[bl];
    
    for(int i = bl + 1; i <= r; i++){
        res += sum[i];
    }
    
    for(int i = L[br]; i <= r; i++){
        res += a[i];
    }
    res += (r - L[br] + 1) * lazy[br];
    return res;
}

三、例题讲解-【模板】块状数组区间修改、区间查询

题目描述

这是一道模板题。

给定数列 a[1]a[2]...a[n],你需要依次进行 Q 个操作,操作有两类:

1 l r x:给定 lrx,对于所有 i ∈ [l, r],将 a[i] 加上 x

2 l r:给定 lr,求 ∑(i=l to r)a[i] 的值。

输入格式

第一行包含 2 个正整数 nQ,表示数列长度和询问个数。保证 1 ≤ n, Q ≤ 10^6

第二行 n 个整数 a[1]a[2]...a[n],表示初始数列。保证 |a[i]| ≤ 10^6

接下来 Q 行,每行一个操作,为以下两种之一:

1 l r x:对于所有 i ∈ [l, r],将 a[i] 加上 x

2 l r:输出 ∑(i=l to r)a[i] 的值。

保证 1 ≤ l ≤ r ≤ n|x| ≤ 10^6

输出格式

对于每个 2 l r 操作,输出一行,每行有一个整数,表示所求的结果。

提示

1≤n, q≤5e4
|x|≤10000
|A[i]|≤1e9

题目分析

模板题,按照讲解的块状数组代码实现做即可:

#include <bits/stdc++.h>

using namespace std;

typedef long long LL;
const int N = 5e4 + 10;
LL a[N];
int L[N], R[N], belong[N];
// L[i] 表示第 i 个点的左端点
// R[i] 表示第 i 个点的右端点
// belong[i] 表示 i 点属于 belong[i] 这个块
LL sum[N], lazy[N];
// sum[i] 表示第 i 个块的块内元素和
// lazy[i] 表示第 i 个块的块标记
int n, m, num, len;

void init() {
    len = sqrt(n);    // 每个块的长度
    num = (n - 1) / len + 1;    // 一共有 num 个块
    // 对于每一个块,确定左右边界
    for (int i = 1; i <= num; i++) {
        L[i] = R[i - 1] + 1;
        R[i] = i  len;
    }
    R[num] = n;
    // 对于每一个 i,明确它属于哪一个块
    for (int i = 1; i <= n; i++) {
        belong[i] = (i - 1) / len + 1;
    }
}

void update(int l, int r, int c) {
    // l 属于 bl 这个块,r 属于 br 这个块
    int bl = belong[l], br = belong[r];
    // 在同一个块中,直接暴力处理
    if (bl == br) {
        // 修改元素
        for (int i = l; i <= r; i++) {
            a[i] += c;
        }
        // 更新块内元素和
        sum[bl] += (r - l + 1)  c;
        return;
    }
    // [l, R[bl]] 零散的块,修改元素,更新块内元素和
    // [bl + 1, br - 1] 完整的块,更新块标记,更新块内元素和
    // [L[br], r] 零散的块,修改元素,更新块内元素和
    for (int i = l; i <= R[bl]; i++) {
        a[i] += c;
    }
    sum[bl] += (R[bl] - l + 1)  c;

    for (int i = bl + 1; i <= br - 1; i++) {
        lazy[i] += c;
        sum[i] += len  c;
    }

    for (int i = L[br]; i <= r; i++) {
        a[i] += c;
    }
    sum[br] += (r - L[br] + 1)  c;
}

long long query(int l, int r) {
    // l 属于 bl 这个块,r 属于 br 这个块
    int bl = belong[l], br = belong[r];
    // 在同一个块中,直接暴力处理
    long long res = 0;
    if (bl == br) {
        for (int i = l; i <= r; i++) {
            res += a[i];
        }
        res += (r - l + 1)  lazy[bl];
        return res;
    }
    // [l, R[bl]] 零散的块,暴力累加,再加上块标记
    // [bl + 1, br - 1] 完整的块,返回块内元素和
    // [L[br], r] 零散的块,暴力累加,再加上块标记
    for (int i = l; i <= R[bl]; i++) {
        res += a[i];
    }
    res += (R[bl] - l + 1)  lazy[bl];

    for (int i = bl + 1; i <= br - 1; i++) {
        res += sum[i];
    }

    for (int i = L[br]; i <= r; i++) {
        res += a[i];
    }
    res += (r - L[br] + 1)  lazy[br];
    return res;
}

int main() {
    ios::sync_with_stdio(0);
    cin.tie(0);
    cout.tie(0);
    cin >> n >> m;
    init();
    for (int i = 1; i <= n; i++) {
        cin >> a[i];
        // 统计块内元素和
        sum[belong[i]] += a[i];
    }
    while (m--) {
        int op, x, y, z;
        cin >> op;
        if (op == 2) {
            cin >> x >> y;
            cout << query(x, y) << "\n";
        } else {
            cin >> x >> y >> z;
            update(x, y, z);
        }
    }
    return 0;
}

浏览(3)
评论

请登录后发表观点

暂无数据