题目链接: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} \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\) 所属的分组编号。

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 \times (k-1)+1\) 到 \(m \times 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 \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})\)。

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

LibreOJ 6277. 数列分块入门 1 题解的更多相关文章

  1. LibreOJ 6277 数列分块入门 1(分块)

    题解:感谢hzwer学长和loj让本蒟蒻能够找到如此合适的入门题做. 这是一道非常标准的分块模板题,本来用打标记的线段树不知道要写多少行,但是分块只有这么几行,极其高妙. 代码如下: #include ...

  2. LibreOJ 6277. 数列分块入门 1

    题目链接:https://loj.ac/problem/6277 参考博客:https://www.cnblogs.com/stxy-ferryman/p/8547731.html 两个操作,区间增加 ...

  3. LibreOJ 6278. 数列分块入门 2 题解

    题目链接:https://loj.ac/problem/6278 题目描述 给出一个长为 \(n\) 的数列,以及 \(n\) 个操作,操作涉及区间加法,询问区间内小于某个值 \(x\) 的元素个数. ...

  4. LibreOJ 6277. 数列分块入门 2

    题目链接:https://loj.ac/problem/6278 参考博客:https://blog.csdn.net/qq_36038511/article/details/79725027 这题我 ...

  5. LOJ #6277. 数列分块入门 1-分块(区间加法、单点查询)

    #6277. 数列分块入门 1 内存限制:256 MiB时间限制:100 ms标准输入输出 题目类型:传统评测方式:文本比较 上传者: hzwer 提交提交记录统计测试数据讨论 2   题目描述 给出 ...

  6. LOJ——#6277. 数列分块入门 1

    ~~推荐播客~~ 「分块」数列分块入门1 – 9 by hzwer 浅谈基础根号算法——分块 博主蒟蒻,有缘人可直接观摩以上大佬的博客... #6277. 数列分块入门 1 题目大意: 给出一个长为 ...

  7. LibreOJ6279. 数列分块入门 3 题解

    题目链接:https://loj.ac/problem/6279 题目描述 给出一个长为 \(n\) 的数列,以及 \(n\) 个操作,操作涉及区间加法,询问区间内小于某个值 \(x\) 的前驱(比其 ...

  8. LibreOJ 6282 数列分块入门 6(在线插入在线查询)

    题解:还是分块,将每个块存入vector,然后在插入的时候就是sqrt(n)级的重构,如果块太大了,暴力将这个块拆开. 代码如下: #include<cmath> #include< ...

  9. LibreOJ 6280 数列分块入门 4(分块区间加区间求和)

    题解:分块的区间求和比起线段树来说实在是太好写了(当然,复杂度也高)但这也是没办法的事情嘛.总之50000的数据跑了75ms左右还是挺优越的. 比起单点询问来说,区间询问和也没有复杂多少,多开一个su ...

随机推荐

  1. oracle函数 INSTRB(C1,C2[,I[,J]])

    [功能]在一个字符串中搜索指定的字符,返回发现指定的字符的位置; [说明]多字节符(汉字.全角符等),按2个字符计算 [参数] C1    被搜索的字符串 C2    希望搜索的字符串 I     搜 ...

  2. Redis源码解析:03字典

    字典是一种用于保存键值对(key value pair)的抽象数据结构.在字典中,一个键和一个值进行关联,就是所谓的键值对.字典中的每个键都是独一无二的,可以根据键查找.更新值,或者删除整个键值对等等 ...

  3. python实现以立春为起点n为周期任意日期所在的日期区间

    python实现以立春为起点n为周期任意日期所在的日期区间 需求 话不多说,直接上具体需求. ''' 以每年的立春作为起始点,每N天为一个单元,任给一个日期,返回该日期所在单元的起始和结束日期.例如: ...

  4. 远程监控JVM

    设置tomcat中catalina.sh设置JAVA_OPTS= JAVA_OPTS="-server -Xms595M -Xmx595M -Xmn223M -XX:SurvivorRati ...

  5. python selenium 基础框架

    base_page.py # coding=utf-8 import time from selenium.common.exceptions import NoSuchElementExceptio ...

  6. iptables 通讯端口转接(Port Forwarding)

    是一种特殊的DNAT操作,其作用是让一部电脑(通常是防火牆)担任其它电脑的代理伺服器(proxy).防火牆接收外界网络接传给它自己的包,然后改写包的目的地位址或目的端口,使其像是要送到內部网路其它电脑 ...

  7. Ubuntu 14.04 使用ntfs-config解决开机自动挂载NTFS分区的方法

    先安装: sudo apt-get install ntfs-3g ntfs-config 再配置一下: sudo ntfs-config 然后就会弹出来一个对话框,选择你需要挂载的分区,点应用,再选 ...

  8. SQLSTATE[HY000] [2002] 错误

    http://www.thinkphp.cn/topic/36194.html 使用tp框架 3.2.3 ,在windows上跑的时候没有任何问题,但是部署到linux系统和是哪个,就会报这个错,不知 ...

  9. java io流与序列化反序列化

    java的io是实现输入和输出的基础,可以方便的实现数据的输入和输出操作. 序列化 (Serialization)是将对象的状态信息转换为可以存储或传输的形式的过程.在序列化期间,对象将其当前状态写入 ...

  10. 140种Python标准库、第三方库和外部工具

    导读:Python数据工具箱涵盖从数据源到数据可视化的完整流程中涉及到的常用库.函数和外部工具.其中既有Python内置函数和标准库,又有第三方库和工具. 这些库可用于文件读写.网络抓取和解析.数据连 ...