例题
题目描述
给出一个长为的序列,以及个操作,操作涉及区间加法,单点查值。
输入格式
第一行输入一个数字。
第二行输入个数字,第个数字为,以空格隔开。
接下来输入次询问,每行输入四个数字、、、,以空格隔开。
若,表示将位于的区间的数字都加。
若,表示询问的值(和忽略)。
输出格式
对于每次询问,输出一行一个数字表示答案。
样例
样例输入
4
1 2 2 3
0 1 3 1
1 0 1 0
0 1 2 2
1 0 2 0
样例输出
2
5
数据范围与提示
对于的数据,。
题目分析
暴力
此方法较为直接,代码较短,但是由于其时间复杂度过高,难免会,在此不多做讲解,直接贴代码:
#include <bits/stdc++.h>
using namespace std;
int a[50005];
int main() {
int n, opt, l, r, c;
scanf("%d", &n);
for (int i=1; i<=n; i++) {
scanf("%d", &a[i]);
}
for (int i=1; i<=n; i++) {
scanf("%d%d%d%d", &opt, &l, &r, &c);
if (opt == 0) {
for (int j=l; j<=r; j++) {
a[j] += c;
}
}
else /*if (opt == 1)*/ {
printf("%d\n", a[r]);
}
}
return 0;
}
进入正题——分块(AC)
首先我们来了解一下分块思想:
分块的基本思想是,通过对原数据的适当划分,并在划分后的每一个块上预处理部分信息,从而较一般的暴力算法取得更优的时间复杂度。
分块的时间复杂度主要取决于分块的块长,一般可以通过均值不等式求出某个问题下的最优块长,以及相应的时间复杂度。
分块是一种很灵活的思想,相较于树状数组和线段树,分块的优点是通用性更好,可以维护很多树状数组和线段树无法维护的信息。
当然,分块的缺点是渐进意义的复杂度,相较于线段树和树状数组不够好。
不过在大多数问题上,分块仍然是解决这些问题的一个不错选择。
——OI Wiki
那么我们该如何实现呢?
对于暴力来说,区间修改和查询所需要的时间复杂度都是,我们可以通过前缀和的方法将查询的时间复杂度降低到,也可以用差分将区间修改的复杂度降低到。对于这种既要进行区间修改,也要进行查询的问题,如果要直接修改数组中的数值,的时间复杂度是无法避免的,于是我们引入了“懒标记(lazy tag)”。
分块的实现
第1段 | 第2段 | 第3段 | 第4段 | 第5段 |
如上图,我们先预处理出每一个“块”的区间和,对于每一个“块”,我们对它添加一个标记(初始值为0),如果整个块的数值都被加上一个数,那么我们就将这个数加到标记上,在需要查询的时候再将标记的数值与数组中的数值相加,这样就大大减少了区间修改的时间复杂度。同样地,对于查询区间和,我们也可以用整块的区间和进行运算,同样减少了运算的时间复杂度。一举两得!(大喜)
我们将原数组每个元素作为一个块(指原数组的长度),本人是使用结构体进行分块,当然也可以使用多个数组,代码如下:
struct node {
int l, r;
ll sum, lt;
} block[M];
区间修改和查询的思路也比较简单,可以结合代码中的注释进行理解,基本思路已经明确了,完整代码如下:
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int M = 50005; // maxium num of n
int n, len, opt, l, r, c, tail;
/*
* n: length of array 'a';
* len: length of a block ([sqrt n])
* opt, l, r, c: input variable from the problem
*/
int a[M], id[M];
/*
* a: main array
* id: the location(block) of each item in array 'a'
*/
struct node {
int l, r;
ll sum, lt;
/*
* sum: sum of items from [l, r] in array 'a'
* lt: lazy tag (+x)
*/
} block[M];
void change(int ll, int rr, int cc) {
int L = id[ll], R = id[rr];
if (L == R) { // change the items directly if [ll, rr] are in a same block
for(int i=ll; i<=rr; i++) {
a[i] += cc;
block[L].sum += cc;
}
}
else {
for (int i=L+1; i<=R-1; i++) { // add lazy tags of whole blocks first
block[i].lt += cc;
}
for (int i=ll; i<=block[L].r; i++) { // change the items directly in parts of a block
a[i] += cc;
block[L].sum += cc;
}
for (int i=block[R].l; i<=rr; i++) {
a[i] += cc;
block[R].sum += cc;
}
}
}
ll ask(int rr) {
return (ll)a[rr] + block[id[rr]].lt;
}
int main() {
//freopen("sample.in", "r", stdin);
scanf("%d", &n);
len = (int)sqrt(n);
for (int i=1; i<=n; i++) {
scanf("%d", &a[i]);
id[i] = (i-1)/len+1;
block[id[i]].sum += a[i]; // preprocess the sum of each block
if ((i-1)%len == 0) {
block[tail].r = i-1;
block[++tail].l = i;
}
}
for (int i=1; i<=n; i++) {
scanf("%d%d%d%d", &opt, &l, &r, &c);
if (opt == 0) {
change(l, r, c);
}
else /*(opt == 1)*/ {
printf("%lld\n", ask(r));
}
}
return 0;
}
怎么样,是不是很简单?
再啰嗦一句,分块的区间查询可以利用完整的块的区间和与区间两边多余的元素数值进行相加,原理与上面代码中的change函数类似,代码如下:
long long ask(int ll, int rr) {
long long ans = 0L;
int L = id[ll], R = id[rr];
if (L == R) {
for(int i=ll; i<=rr; i++) {
ans += (long long)a[i];
}
}
else {
for (int i=L+1; i<=R-1; i++) {
ans += (long long)block[i].sum;
}
for (int i=ll; i<=block[L].r; i++) {
ans += (long long)a[i];
}
for (int i=block[R].l; i<=rr; i++) {
ans += (long long)a[i];
}
}
return ans;
}
感谢完整地读完我这篇枯燥的博客,还请点个赞再走叭~