【模板】线段树 1
题目描述
如题,已知一个数列,你需要进行下面两种操作:
- 将某区间每一个数加上 k k k。
- 求出某区间每一个数的和。
输入格式
第一行包含两个整数 n , m n, m n,m,分别表示该数列数字的个数和操作的总个数。
第二行包含 n n n 个用空格分隔的整数,其中第 i i i 个数字表示数列第 i i i 项的初始值。
接下来 m m m 行每行包含 3 3 3 或 4 4 4 个整数,表示一个操作,具体如下:
1 x y k
:将区间 [ x , y ] [x, y] [x,y] 内每个数加上 k k k。2 x y
:输出区间 [ x , y ] [x, y] [x,y] 内每个数的和。
输出格式
输出包含若干行整数,即为所有操作 2 的结果。
样例 #1
样例输入 #1
5 5
1 5 4 2 3
2 2 4
1 2 3 2
2 3 4
1 1 5 1
2 1 4
样例输出 #1
11
8
20
提示
对于
30
%
30\%
30% 的数据:
n
≤
8
n \le 8
n≤8,
m
≤
10
m \le 10
m≤10。
对于
70
%
70\%
70% 的数据:
n
≤
10
3
n \le {10}^3
n≤103,
m
≤
10
4
m \le {10}^4
m≤104。
对于
100
%
100\%
100% 的数据:
1
≤
n
,
m
≤
10
5
1 \le n, m \le {10}^5
1≤n,m≤105。
保证任意时刻数列中所有元素的绝对值之和 ≤ 10 18 \le {10}^{18} ≤1018。
【样例解释】
#
思路
先不要看算法标签和题目名称。
通过观察可以发现,如果强行枚举,绝对会超时(不会有玄学方法能过吧 )
看题目的数据范围,
1
≤
n
,
m
≤
10
5
1 \le n, m \le {10}^5
1≤n,m≤105。
那么这道题目时间复杂度的最高是
O
(
n
n
)
O(n\sqrt n)
O(nn)
这道题目明显做法很多,线段树、树状数组可以用十分优秀的
O
(
n
log
n
)
O(n\log n)
O(nlogn)的时间复杂度通过。
但是明显线段树、树状数组这些做法太长了不好写。
因此,分块成了一种简单好写,而且时间复杂度较为优秀的思想。
分块的时间复杂度略高于线段树、树状数组,为
O
(
n
n
)
O(n\sqrt n)
O(nn),所以分块可以在这一题替代线段树。
分块原理
先考虑暴力,最简单的思路就是每一次操作进行增加就从
l
l
l枚举到
r
r
r,增加每一个数,求和就从
l
l
l枚举到
r
r
r,进行累加。(这里的
l
l
l和
r
r
r代表题目中的
x
x
x和
y
y
y)
显然时间大大滴超。
导致超时的情况是
l
l
l和
r
r
r的差值很大,接下来考虑如何优化掉这种情况。
可以将数组分成很多块。
在
l
l
l和
r
r
r的差值很大的情况下,这个区间将会覆盖很多整块。
比如
n
=
1000
n=1000
n=1000的情况下,每个块的大小为
100
100
100时,假如
l
=
10
l=10
l=10,
r
=
500
r=500
r=500时,这个区间就可以看作4个完整块以及一个不完整的块,如果可以在
O
(
1
)
O(1)
O(1)的时间将每个完整块处理,那么这次操作的时间复杂度就会降到
O
(
n
÷
10
)
O(n÷10)
O(n÷10)左右。
分块的块的大小
在面对并不在一个完整的块内的情况,只能进行暴力求解,设块的大小为
s
i
z
siz
siz,则时间复杂度为
O
(
s
i
z
)
O(siz)
O(siz)。
覆盖多个完整块的情况下,每个完整块都是
O
(
1
)
O(1)
O(1),设共有
n
u
m
num
num块,则时间复杂度为
O
(
n
u
m
)
O(num)
O(num)。
因为块数=总数÷块的大小,因此
n
u
m
=
n
/
s
i
z
,
n
u
m
∗
s
i
z
=
n
num=n/siz,num*siz=n
num=n/siz,num∗siz=n。
为了让时间复杂度尽可能低,要
n
u
m
num
num和
s
i
z
siz
siz尽可能都小,此时最好的办法就是将
s
i
z
siz
siz和
n
u
m
num
num设为
n
\sqrt n
n。
因此块的大小最好为
n
\sqrt n
n。
此时的时间复杂度为
m
n
m\sqrt n
mn其中
m
m
m为操作次数,
n
n
n为元素个数。
这道题的分块实现
所需的数组和变量
ll siz,num;//块个数和块大小
ll L[N],R[N];//每个块的左右端点
ll bel[N],su[N],add[N];//每个元素属于哪一个块,以及每个块的和、每个块每个元素都加上多少
ll n,m,a[N];//对应输入
分块初始化
void init(){
siz=sqrt(n);//最佳的分块大小
num=n/siz+(n%siz>0);//块的个数
for(int i=1;i<=num;i++){//这里预处理,其实也可以封装函数后面计算
L[i]=siz*(i-1)+1;
R[i]=min(n,siz*i);
}
for(int i=1;i<=n;i++){//这里也是预处理
bel[i]=(i-1)/siz+1;
su[bel[i]]+=a[i];
}
}
预处理可以降低常数
区间增加某个数
void upd(ll l,ll r,ll k){
ll x=bel[l],y=bel[r];
if(x==y){//在同一块内的情况
for(int i=l;i<=r;i++){
a[i]+=k;
su[x]+=k;
}
}
else{
for(int i=x+1;i<y;i++){//整块
add[i]+=k;
su[i]+=k*siz;
}
for(int i=l;i<=R[x];i++){//非完整
a[i]+=k;
su[x]+=k;
}
for(int i=L[y];i<=r;i++){//非完整
a[i]+=k;
su[y]+=k;
}
}
}
这里可以看作每一个块内所有元素都加上的某个数暂时没有加上去,而是保留到需要用的时候再进行处理。
面对不完整的块直接暴力就行。
区间求和
ll qu(ll l,ll r){
ll ans=0;//记录答案
ll x=bel[l],y=bel[r];
if(x==y){//同一个块内直接暴力就行
for(int i=l;i<=r;i++){
ans+=a[i]+add[x];//加上add是因为把之前没有加上的加上
}
}
else{
for(int i=x+1;i<y;i++){
ans+=su[i];//每个块的和之前记录了
}
for(int i=l;i<=R[x];i++){
ans+=a[i]+add[x];//这里也是一样
}
for(int i=L[y];i<=r;i++){
ans+=a[i]+add[y];//同上
}
}
return ans;
}
查找时,之前没有加上去放在add数组里的值就可以加上了,优化了时间复杂度。
可以看出分块还是一种暴力。
这里也可以看出预处理的作用。
“完整”代码
直接复制代码不是一个好习惯。
ll siz,num,L[N],R[N],bel[N],su[N],add[N];
ll n,m,a[N];
void init(){
siz=sqrt(n);
num=n/siz+(n%siz>0);
for(int i=1;i<=num;i++){
L[i]=siz*(i-1)+1;
R[i]=min(n,siz*i);
}
for(int i=1;i<=n;i++){
bel[i]=(i-1)/siz+1;
su[bel[i]]+=a[i];
}
}
void upd(ll l,ll r,ll k){
ll x=bel[l],y=bel[r];
if(x==y){
for(int i=l;i<=r;i++){
a[i]+=k;
su[x]+=k;
}
}
else{
for(int i=x+1;i<y;i++){
add[i]+=k;
su[i]+=k*siz;
}
for(int i=l;i<=R[x];i++){
a[i]+=k;
su[x]+=k;
}
for(int i=L[y];i<=r;i++){
a[i]+=k;
su[y]+=k;
}
}
}
ll qu(ll l,ll r){
ll ans=0;
ll x=bel[l],y=bel[r];
if(x==y){
for(int i=l;i<=r;i++){
ans+=a[i]+add[x];
}
}
else{
for(int i=x+1;i<y;i++){
ans+=su[i];
}
for(int i=l;i<=R[x];i++){
ans+=a[i]+add[x];
}
for(int i=L[y];i<=r;i++){
ans+=a[i]+add[y];
}
}
return ans;
}
int main(){
scanf("%lld %lld",&n,&m);
for(int i=1;i<=n;i++){
scanf("%lld",&a[i]);
}
init();
for(int i=1;i<=m;i++){
int op;
scanf("%d",&op);
if(op==1){
ll x,y,k;
scanf("%lld %lld %lld",&x,&y,&k);
upd(x,y,k);
}
else{
ll x,y;
scanf("%lld %lld",&x,&y);
ll tmp=qu(x,y);
printf("%lld\n",tmp);
}
}
return 0;
}
分块和线段树对比的优势和劣势
时间复杂度上
线段树的时间复杂度平均
O
(
n
log
n
)
O(n\log n)
O(nlogn)
分块时间复杂度
O
(
n
n
)
O(n\sqrt n)
O(nn)
分块时间复杂度较高
空间复杂度上
线段树和分块的空间复杂度均为 O ( n ) O(n) O(n),but实际上分块的空间更小,不容易被卡。
代码复杂度上
d
a
l
a
o
dalao
dalao们的线段树代码100行左右。
分块80行左右,代码量要少。
线段树的思路比较难理解。
分块的思路就是暴力,十分容易理解,分块更好。
结果
可以看出分块与线段树不相上下,面对大部分区间问题分块足以。不行就用莫队