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


    题目链接:https://loj.ac/problem/6277

    题目描述

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

    输入格式

    第一行输入一个数字 (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)

    解题思路

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

    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}),但实际上,有时候我们要根据数据范围、具体复杂度来确定块大小。

    更新操作

    我们来分析一下这里的更新操作。
    因为我们本题只涉及一种类型的更新操作——给区间 ([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) 的。

    对于编号为 (k) 的分块,我们可以知道属于这个分块的元素的编号从 (m imes (k-1)+1)(m imes k)
    如果我们的更新操作面临着将一整块的元素都更新 (c)(即每个元素都增加(c)),那么我们可以采取如下朴素方法:

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

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

    但其实我们不需要对一整块当中的每一个元素都加 (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 e 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
    ", 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}))

    参考链接:https://www.cnblogs.com/louhancheng/p/10051136.html

  • 相关阅读:
    Git引用
    如何查看Git对象
    Git是如何存储对象的
    图形化的Git
    git中找回丢失的对象
    Git的Patch功能
    ES查看配置和查看全部配置
    增删改查
    Elasticsearch增、删、改、查操作深入详解
    ES博客链接
  • 原文地址:https://www.cnblogs.com/quanjun/p/12111576.html
Copyright © 2020-2023  润新知