题解:P13976 数列分块入门 1
lcfollower
·
·
题解
修改
闲话
:::info[无关内容]
哎这题是数列分块 1 的板子为啥黄?哦原来是可以用 BIT 做。但是好像评难度的跟不上时代,BIT 板子已经绿了。
我以前数列分块都是在团队里交的,数据范围也和原题一样,然后我那个团给数列分块 9 题都评了红,我们底下就流传了切红题等于切数列分块。
后来知道了 loj 就交了几道到 loj 上。
然后盼了好久,终于等到了这一天——洛谷竟然也上传数列分块入门系列 9 题了(终于没有红了)!
:::
前置知识
-
暴力。
-
线段树的懒标记思想。
-
会基础的时间复杂度分析。
引入
序列分块是什么?
它是一种算法,可以把它当作优雅的暴力。
对于这道题,我们对于操作 0 有个朴素的暴力:
for(int i = l ; i <= r ; i ++) a[i] += c;
这样操作 1 就可以直接输出 a_r,最坏时间复杂度为 \mathcal O(Qn),其中 Q 为操作次数,在本题中就是 n,下文还是会突出 Q 而非 n。
那么我们能不能把 \mathcal O(n) 的修改给减少一点时间呢?
现在我们可以把序列抽相成若干个长度为 1 的块,每次 a[i] += c 都是在修改一个块的元素。
那么我们能不能把序列重新抽象成由若干个长度为 B 的块组成,这样一次暴力的时间复杂度是否能减少呢?
抽象过后的序列
举个例子,比如 a = [1 ,4 ,2 ,3 ,5 ,3],此时取 B = 3,那么就可以抽象为 a = [[1 ,4 ,2] ,[3 ,5 ,3]],取 B = 4 可以抽象为 a = [[1 ,4 ,2 ,3] ,[5 ,3]](注意这里最后一块长度不满但是没什么大关系)。
操作 0
我们先抛开预处理,直接来看操作怎么解决。
现在我们有个长度为 10 的序列 a = [1 ,1 ,4 ,5 ,1 ,4 ,1 ,9 ,1 ,9],现在取 B = 3。
相同颜色的框内的数均在同一个块中。
现在我们要将 a_{[2\dots 8]} 加上 5。
我们发现,修改的区间有一些块是完整的修改块,又有一些块是只修改一部分的修改块。我们把完整的修改块称为整块,修改一部分的块成为零散块。容易发现零散块最多只有 2 个,分别位于修改区间的左和右端点。因此我们把左边的零散块称为左零散块,同理右边的为右零散块。
比如上图的整块为蓝块,左零散块为红块,右零散块为粉块。
此时,我们发现整块可以直接 \mathcal O(1) 修改。具体地,就是和线段树一样的懒标记,把这个块的懒标记增加 5。
我们记 belong_i 为节点 i 所属的块,这个在下面的建块里可以预处理。为了方便代码书写,如果修改区间不存在整块,我们也把最左边和最后边的整块当作零散块。
//[x ,y] 为修改区间,val 为该修改操作增加的值。
//tag[i] 为块 i 的加法懒标记。
#define up(i, x, y) for(int i = x ; i <= y ; i ++)
up(i, belong[x] + 1, belong[y] - 1) tag[i] += /*5*/ val;
//由于最左、最右块我们都看作了零散块,因此最左和最右的两个块的懒标记不能修改,因此 belong[x] + 1 <= i <= belong[y] - 1。
容易计算得整个序列有 \lceil \frac{n}{B}\rceil 个块,那么对于修改整块,最多修改 \max\{0 ,\lceil\frac{n}{B}\rceil - 2\} 个整块,一次修改时间复杂度为 \mathcal O(1),因此时间复杂度为 \mathcal O(\frac{n}{B})。
现在还有左右两个零散块没有更新过,此时我们不能打上标记,那怎么修改呢?
我们想到一种很暴力的做法:直接暴力给每个值增加 5。
现在我们记 l_i 为块 i 的左端点编号,r_i 为块 i 的右端点编号,这个我们可以在下面的建块预处理得到。
那么左边零散块需要暴力修改的位置为 [x ,r_{belong_x}],右零散块需要暴力修改的位置为 [l_{belong_y} ,y]。
//很板的暴力。
up(i, x, r[belong[x]]) a[i] += val;
up(i, l[belong[y]], y) a[i] += val;
明显,一次循环最多修改 B 个值,那么两个循环最多修改 2B 个值,时间复杂度为 \mathcal O(B),那么操作的 0 时间复杂度就为了 \mathcal O(B + \frac{n}{B})。
现在还有一个特殊情况,如果修改区间内,没有任何整块呢?
既然零散块不能直接打标记(标记是给整块打的),那么能不能直接暴力呢?
因为一个块的长度最长为 B,我们可以暴力修改,时间复杂度为 \mathcal O(B),这样做当且仅当 belong_x = belong_y。
if(belong[x] == belong[y]) {
up(i, x, y) a[i] += val;//暴力做。
return ; //要么回溯写 return,要么不写下面写 else。
} /*else*/
提一嘴给加法打懒标记的正确性很显然,做过线段树 1 的应该显然。
操作 1
这个是单点查询,我们直接输出 a_r + tag_{belong_r} 即可,即一开始的序列值,暴力修改的序列值,以及懒标记的值的和,前两个就是 a_r,后面的为 tag_{belong_r}。
那么如果是区间查询呢?
和区间修改一样的,我们需要额外维护一个 sum_i 表示块 i 的总和,这个只对于原序列,预处理完就不更改了。
那么查询时:
修改还是一模一样。
建块
好了,这两个操作都要把块建出来才行。
我们首先计算块的数量 num = \lceil \frac{n}{B}\rceil = \lfloor\frac{n - 1}{B}\rfloor + 1 = \lceil \frac{n + B - 1}{B}\rceil,下取整 C++ 里是有个整除的所以尽量用下取整,它方便。
然后对于块 i(1\le i\le num),我们能得到 r_i = i\times len,这个显然有 i 个长度为 len 的块那么右端点就是 i\times len。然后可以计算得到 l_i = (i - 1) \times len + 1 = r_{i - 1} + 1,即块 i 的左端点为块 i - 1 的右端点右边一个位置。
注意可能 r_{num} > n,因为最后一个块的长度可能不满 B,因此循环完写上 r[num] = n 即可。
然后对于下标 i 所属块的编号 belong_i(1\le i\le n),显然的就是 \lceil \frac{i}{B}\rceil,同样地,转化为下取整计算即可。
len = B /*待定*/ ,num = (n - 1) / len + 1;
//len 即为 B,num 为块的数量。
up(i, 1, num){
l[i] = (i - 1) * len + 1;
r[i] = i * len;
//计算块的左右端点。
}
r[num] = n;//不要忘了。
up(i, 1, n) belong[i] = (i - 1) / len + 1; //计算 i 所属的块的编号。
建块的时间复杂度为 \mathcal O(\frac{n}{B} + n),显然最坏情况下为 \mathcal O(n),下文就不讨论这个时间复杂度了。
当然可能有的麻烦点的建块时间复杂度为 \mathcal O(nB),比如序列分块入门 9。
---
好了现在要取值了,取多少好呢?
注意到时间复杂度瓶颈肯定在 $Q$ 次操作。
然后操作 $0$ 的时间复杂度为 $\mathcal O(\frac{n}{B} + B)$,操作 $1$ 的时间复杂度为 $\mathcal O(1)$。
假设 $Q - 1$ 次操作 $0$,$1$ 次操作 $1$,最坏时间复杂度为 $\mathcal O(Q(\frac{n}{B} + B))$。
因此我们想要 $Q(B + \frac{n}{B})$ 尽可能小,分离出常数 $Q$ 就是要 $B + \frac{n}{B}$ 尽可能小。
此时只要取 $B = \sqrt{n}$,时间复杂度为 $\mathcal O(\frac{n}{\sqrt{n}} + \sqrt{n}) = \mathcal O(\sqrt{n} + \sqrt{n}) = \mathcal O(\sqrt{n})$,容易发现此时最优。
因此本题的时间复杂度为 $\mathcal O(Q\sqrt{n})$,可以通过。
然后有的题比较卡常,可能要调整 $B$ 的长度:
比如在常数上,记整块修改一次的常数为 $c1$,零散块修改一次的常数为 $c2$,其余相等。
- 如果 $c1$ 远大于 $c2$,那么肯定是要 $\frac{n}{B}$ 尽可能小,此时我们应该适当增加 $B$。
- 反之我们要适当减少 $B$。
不同的题可能有不同的最优 $B$,一般把时间复杂度表示出来解个最优的,或根据常数,各种操作的执行次数等来定。
一般不卡常的话取 $\sqrt{n}$ 基本上就可以了,或者可以修改你整个代码的常数大的地方,比如减少循环次数和空间大小等。
代码
---
一年前的代码了,可能有点丑,跟上面的有点不一样,我现在尽量加点空格。
```cpp
#include <bits/stdc++.h>
using namespace std;
#define int long long
#define rep(i, x, y) for(int i = x;i <= y;i ++)
#define pr printf
inline int read() {
int s = 0, w = 1;
char c = getchar();
while (!isdigit(c)) {
if (c == '-') w = -1;
c = getchar();
}
while (isdigit(c)) {
s = (s << 1) + (s << 3) + (c ^ 48);
c = getchar();
}
return s * w;
}
const int N = 3e5 + 10 , M = 620;
int n , len , num , a[N] , belong[N] , l[M] , r[M] , add[M];
inline void build(){
len = sqrt(n);
num = (n - 1) / len + 1;
rep(i, 1, n) belong[i] = (i - 1) / len + 1;
rep(i, 1, num) l[i] = (i - 1) * len + 1 , r[i] = i * len;
r[num] = n;
}
inline void modify(int x , int y , int val){
if(belong[x] == belong[y]) rep(i, x, y) a[i] += val;
else{
rep(i, belong[x]+1, belong[y]-1) add[i] += val;
rep(i, x, r[belong[x]]) a[i] += val;
rep(i, l[belong[y]], y) a[i] += val;
}
}
signed main() {
n = read();
int Q = n;
rep(i, 1, n) a[i] = read();
build () ;
while(Q --){
int op = read(), x = read(), y = read(), val = read();
if(!op) modify(x, y, val);
else pr("%lld\n", a[y] + add[belong[y]]);
}
return 0;
}
```