【模板】线段树 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行左右,代码量要少。
线段树的思路比较难理解。
分块的思路就是暴力,十分容易理解,分块更好。