LibreOJ 6277. 数列分块入门 1 题解

题目连接:https://loj.ac/problem/6277html

题目描述

给出一个长为 \(n\) 的数列,以及 \(n\) 个操做,操做涉及区间加法,单点查值。c++

输入格式

第一行输入一个数字 \(n\)
第二行输入 \(n\) 个数字,第 \(i\) 个数字为 \(a_i\),以空格隔开。
接下来输入 \(n\) 行询问,每行输入四个数字 \(opt\)\(l\)\(r\)\(c\),以空格隔开。
\(opt=0\),表示将位于 \([l,r]\) 之间的数字都加 \(c\)
\(opt=1\),表示询问 \(a_r\) 的值( \(l\)\(c\) 忽略)。算法

输出格式

对于每次询问,输出一行一个数字表示答案。数组

样例输入

4
1 2 2 3
0 1 3 1
1 0 1 0
0 1 2 2
1 0 2 0

样例输出

2
5

数据范围与提示

对于 \(100%\) 的数据,\(1 \le n \le 50000, -2^{31} \le others,ans \le 2^{31}-1\)spa

解题思路

本题涉及的算法:数列分块
数列分块,就是把一个长度为 \(n\) 的数组,拆分红一个个连续的长度为 \(\lfloor \sqrt{n} \rfloor\) 的小块(若是 \(n\) 不能被 \(\lfloor \sqrt{n} \rfloor\) 整除,则最后一个分块的长度为 \(n\) mod \(\lfloor \sqrt{n} \rfloor\))。
而后咱们这里设 \(m = \sqrt{n}\),那么咱们能够定义数组中的第 \(i\) 个元素 \(a_i\) 所属的分块为 \(\lfloor \frac{i-1}{m} \rfloor + 1\)(即:\(a_1,a_2, \cdots ,a_m\) 属于第 \(1\) 个分块,\(a_{m+1},a_{m+2}, \cdots ,a_{2m}\) 属于第 \(2\) 个分块,……)。
为了入门方便起见,咱们定义一个数组 \(p[i]\) 表示 \(a_i\) 所属的分组编号。code

scanf("%d", &n);
m = sqrt(n);
for (int i = 1; i <= n; i ++) p[i] = (i-1)/m + 1;
for (int i = 1; i <= n; i ++) scanf("%d", &a[i]);

实际上,全部的分块都是这样:把一个数列分红几块,而后对它们进行批量处理。
通常来讲,咱们直接把块大小设为 \(\sqrt{n}\),但实际上,有时候咱们要根据数据范围、具体复杂度来肯定块大小。htm

更新操做

咱们来分析一下这里的更新操做。
由于咱们本题只涉及一种类型的更新操做——给区间 \([l,r]\) 范围内的每个数增长一个值 \(c\)
这些数一定是属于连续的块 \(p[l], p[l]+1, \cdots , p[r]\) 内的。
而且咱们能够发现:当块的数量 \(\gt 2\) 时,除了 \(p[l]\)\(p[r]\) 这两块可能存在“部分元素须要更新”的状况,其他全部的分块(\(p[l]+1, p[l]+2, \cdots , p[r]-1\))都是将整块元素都增长了 \(c\) 的。blog

对于编号为 \(k\) 的分块,咱们能够知道属于这个分块的元素的编号从 \(m \times (k-1)+1\)\(m \times k\)
若是咱们的更新操做面临着将一整块的元素都更新 \(c\)(即每一个元素都增长\(c\)),那么咱们能够采起以下朴素方法:get

for (int i = m*(k-1)+1; i <= m*k; i ++)
    a[i] += c;

这种方法的时间复杂度是 \(O(m) = O( \sqrt{n} )\)it

但其实咱们不须要对一整块当中的每个元素都加 \(c\) ,由于他们都加上 \(c\) 了,因此我干脆标记这个分块有个总体的增量 \(c\) 便可。
咱们能够开一个大小为 \(\sqrt{n}\) 的数组 \(v\),其中 \(v[i]\) 用于表示第 \(i\) 个分块的总体更新量。
那么,当我须要对编号为 \(k\) 的那个块进行总体的更新操做,我能够执行以下代码:

v[k] += c;

因此,咱们能够将区间 \([l,r]\) 总体增长 \(c\) 的操做拆分以下:
首先,若是 \(a[l]\)\(a[r]\) 属于同一个分块(那么只有一个不完整的分块),我仍是朴素地从 \(a[l]\)\(a[r]\) 遍历并将每一个元素加上 \(c\)

if (p[l] == p[r]) { // 说明在同一个分块,直接更新
    for (int i = l; i <= r; i ++) a[i] += c;
    return;
}

不然,说明从 \(a[l]\)\(a[r]\) 至少有两个分块。
咱们把问题拆分红三步走:

  1. 更新最左边的那个分块;
  2. 更新最右边的那个分块;
  3. 更新中间的那些分块(若是有的话)。

step.1 更新最左边的那个分块

首先咱们来分析最左边的分块,即 \(a[l]\) 所属的分块:

  • 若是 \(l\) mod \(m \ne 1\),说明 \(a[l]\) 不是他所在的分块的第一个元素,那么我仍是须要从 \(a[l]\) 开始从前日后更新全部和 \(a[l]\) 属于同一个分块的元素(即:将全部知足条件 \(i \ge l\)\(p[i] = p[l]\)\(a[i]\) 加上 \(c\));
  • 不然(即 \(l\) mod \(m = 1\)),说明 \(a[l]\) 是他所在的分块的第一个元素,那么咱们只要整块更新便可:\(v[p[l]] += c\)
if (l % m != 1) {    // 说明l不是分块p[l]的第一个元素
    for (int i = l; p[i]==p[l]; i ++)
        a[i] += c;
}
else v[p[l]] += c;

step.2 更新最右边的那个分块

接下来咱们来分析最右边的分块,即 \(a[r]\) 所属的分块:

  • 若是 \(r\) mod \(m = 0\),说明 \(a[r]\) 不是他所在的分块的最后一个元素,那么咱们须要从 \(a[r]\) 开始从后往前更新全部和 \(a[r]\) 属于同一个分块的元素(即:将全部知足条件 \(i \le r\)\(p[i] = p[r]\)\(a[i]\) 加上 \(c\));
  • 不然(即 \(r\) mod \(m = 0\)),说明 \(a[r]\) 是他所在的分块的最后一个元素,那么咱们只须要整块更新便可:\(v[p[r]] += c\)
if (r % m != 0) { // 说明r不是分块p[r]的最后一个元素
    for (int i = r; p[i]==p[r]; i --)
        a[i] += c;
}
else v[p[r]] += c;

3. 更新中间的那些分块(若是有的话)

在前两步当中,咱们已经更新完了最左边的分块(\(a[l]\)所属的分块)及最右边的分块(\(a[r]\)所属的分块),那么剩下来的就是中间的那些分块(即编号为\(p[l]+1, p[l]+2, \cdots , p[r]-1\)的那些分块),这些分块都是整块更新的,全部对于这些分块,咱们直接将更新量 \(c\) 加到其总体更新量当中便可。

for (int i = p[l]+1; i < p[r]; i ++)
    v[i] += c;

查询操做

若是咱们如今要查询 \(a[i]\) 对应的值,那么他应该对应两部分:

  1. \(a[i]\) 自己的值;
  2. \(a[i]\) 所属的分块 \(p[i]\) 的总体更新量 \(v[p[i]]\)

因此 \(a[i]\) 的实际值为 \(a[i] + v[p[i]]\)

这样,咱们就分析玩了数列分块对应的更新和查询这两种操做。
完整实现代码以下:

#include <bits/stdc++.h>
using namespace std;
const int maxn = 50050;
int n, m, a[maxn], p[maxn], v[300], op, l, r, c;
void add(int l, int r, int c) {
    if (p[l] == p[r]) { // 说明在同一个分块,直接更新
        for (int i = l; i <= r; i ++) a[i] += c;
        return;
    }
    if (l % m != 1) {    // 说明l不是分块p[l]的第一个元素
        for (int i = l; p[i]==p[l]; i ++)
            a[i] += c;
    }
    else v[p[l]] += c;
    if (r % m != 0) { // 说明r不是分块p[r]的最后一个元素
        for (int i = r; p[i]==p[r]; i --)
            a[i] += c;
    }
    else v[p[r]] += c;
    for (int i = p[l]+1; i < p[r]; i ++)
        v[i] += c;
}
int main() {
    scanf("%d", &n);
    m = sqrt(n);
    for (int i = 1; i <= n; i ++) p[i] = (i-1)/m + 1;
    for (int i = 1; i <= n; i ++) scanf("%d", a+i);
    for (int i = 0; i < n; i ++) {
        scanf("%d%d%d%d", &op, &l, &r, &c);
        if (op == 0) add(l, r, c);
        else printf("%d\n", a[r] + v[p[r]]);
    }
    return 0;
}

时间复杂度分析

更新

更新最左边的那个分块:
由于每一个分块的元素不超过 \(\sqrt{n}\) 因此操做次数不会超过 \(\sqrt{n}\)

更新最右边的那个分块:
由于每一个分块的元素不超过 \(\sqrt{n}\) 因此操做次数不会超过 \(\sqrt{n}\)

更新中间的那些分块:
由于分块个数不会超过 \(\sqrt{n}+1\) 因此中间那些分块的数量不会超过 \(\sqrt{n}\)

因此更新一次的时间复杂度为 \(O( \sqrt{n} ) + O( \sqrt{n} ) + O( \sqrt{n} ) = O( \sqrt{n} )\)

查询

查询直接返回 \(a[i] + v[p[i]]\) ,因此查询的时间复杂度为 \(O(1)\)

综上所述,由于一共有 \(n\) 次操做,因此该算法的时间复杂度为 \(O(n \sqrt{n})\)

参考连接:http://www.javashuo.com/article/p-giqzkvug-he.html

相关文章
相关标签/搜索