1、树状数组
(1) 树状数组原理
树状数组到底干了件什么事情?前缀和,就单单是前缀和那么简单,并没有那么复杂。
来看看单点修改区间查询,区间查询就是查询区间中数字的和,例如数组:int f[6]={0,1,2,3,4,5},那么求1~3的区间和就是f[1]+f[2]+f[3]=1+2+3=6,4~5的区间和就是f[4]+f[5]=4+5=9。
如果不需要修改,先对数组做前缀和,再用差分就可以实现多次O(1)查询了,例如数组:int f[6]={0,1,2,3,4,5},前缀和数组就是pre[6]={0,1,3,6,10,15},求1~3的区间和就是pre[3]-pre[0]=6-0=6,4~5的区间和就是pre[5]-pre[3]=15-6=9。
假如需要修改,那么每次修改之后都需要做前缀和处理,需要O(n)的时间复杂度。也就是说,单纯用前缀和解决这个问题,对于每次查询的复杂度是O(1),每次修改的复杂度是O(n),当然也可以逆过来做,每次修改用O(1)的时间直接修改,用O(n)的时间暴力查询。对于解决实际问题,它们并不是一个理想的算法。那么线段树算法就诞生了。
线段树对于单点修改的复杂度是O(log n),区间查询的时间复杂度是O(log n),大大减低了复杂度。那么它是如何实现的呢?
对于一个数组int f[10]={0,1,2,3,4,5,6,7,8,9},那么经过改造为树状数组将变为
int g[10]={0,1,3,3,10,5,11,7,36,9}。
那么这个数组怎么变来的呢?实际上:
g[0]=f[0];
g[1]=f[1]
g[2]=f[1]+f[2]
g[3]=f[3]
g[4]=f[1]+f[2]+f[3]+f[4]
g[5]=f[5]
g[6]=f[5]+f[6]
g[7]=f[7]
g[8]=f[1]+f[2]+f[3]+f[4]+f[5]+f[6]+f[7]+f[8]
g[9]=f[9]
可以说是毫无规律是吧哈哈
其实这得从二进制来看的:
g[00002]=f[00002];
g[00012]=f[00012]
g[00102]=f[00012]+f[001022]
g[00112]=f[00112]
g[01002]=f[00012]+f[00102]+f[00112]+f[01002]
g[01012]=f[01012]
g[01102]=f[01012]+f[01102]
g[01112]=f[01112]
g[10002]=f[00012]+f[00102]+f[00112]+f[01002]+f[01012]+f[01102]+f[01112]+f[10002]
g[10012]=f[10012]扫描二维码关注公众号,回复: 14741287 查看本文章![]()
可以看到,上面的二进制是有联系的,所包括的数字取决最后一位,什么意思呢
假如一个数的二进制为100102,它的最后一位是10,那么所包括的值就有01,10,即:
g[100102]=f[100012]+f[100102]
假如一个数字是101002,最后一位是100那么所包括的值有001,010,011,100,即
g[101002]=f[100012]+f[100102]+f[100112]+f[101002]
如果你看懂了上面的树状数组的含义,那么就可以接着往下看了,如果没有理解,建议反复理解,第一次接触总是觉得难的,但是理解了之后也就那么一回事
上面只是构建了树状数组,想必大家会有所疑问,为什么要这样构建?如果理解了上面的树状数组的意义,那么接下来就很好理解了:
查询,是在树状数组的基础上的,那么我们接着来看查询是怎么做到的
int g[10]={0,1,3,3,10,5,11,7,36,9}
int ans[10]={0,1,3,6,10,15,21,28,36,45}
没错,查询出来的值就是前缀和,那么我们再来推一遍
实际上
ans[0]=g[0]
ans[1]=g[1]
ans[2]=g[2]
ans[3]=g[2]+g[3]
ans[4]=g[4]
ans[5]=g[4]+g[5]
ans[6]=g[4]+g[6]
ans[7]=g[4]+g[6]+g[7]
ans[8]=g[8]
ans[9]=g[8]+g[9]
同样地,从二进制看:
ans[00002]=g[00002]
ans[00012]=g[00012]
ans[00102]=g[00102]
ans[00112]=g[00102]+g[00112]
ans[01002]=g[01002]
ans[01012]=g[01002]+g[01012]
ans[01102]=g[01002]+g[01102]
ans[01112]=g[01002]+g[01102]+g[01112]
ans[10002]=g[10002]
ans[10012]=g[10002]+g[10012]
可以发现数组与二进制的位数有关
例如101010,那么对应的数字为101010,101000,100000,每次都将最低的一位1置为了0
ans[1010102]=g[1010102]+g[1010002]+g[1000002]
例如1110,那么对应的数字为1110,1100,1000,每次都将最低的一位1置为了0
ans[11102]=g[11102]+g[11002]+g[10002]
虽然这样达到了目的,那么我们就回推一下,为什么这样可以求出前缀和:
int f[10]={0,1,2,3,4,5,6,7,8,9}
int g[10]={0,1,3,3,10,5,11,7,36,9}
int ans[10]={0,1,3,6,10,15,21,28,36,45}
首先我们简单拆分一下ans数组的由来,例如
ans[01112]=g[01002]+g[01102]+g[01112]
又有:
g[01002]=f[00012]+f[00102]+f[00112]+f[01002]
g[01102]=f[01012]+f[01102]
g[01112]=f[01112]
所以:
ans[01112]=f[00012]+f[00102]+f[01002]+f[00012]+f[00102]+f[00112]+f[01002]+f[01112]
我们将上面的数字转成十进制来看:
ans[7]=g[4]+g[6]+g[7]
g[4]=f[1]+f[2]+f[3]+f[4]
g[6]=f[5]+f[6]
g[7]=f[7]
ans[7]=f[1]+f[2]+f[3]+f[4]+f[5]+f[6]+f[7]
其它的可以类似推导一下就可以完全理解树状数组啦!
(2)树状数组实现
修改对应的当然是构建我们的树状数组了!
那么我们反过来想一下,f[1]到底包括在g数组哪些数里呢?
1的二进制是00012,因为构建g数组是与最后一位1有关的,即00012、00102、01002、10002都包括00012,那么10102有没有包括00012呢?没有,它只包括10012和10102
再来看个例子:
5的二进制为001012,那么它被001012、001102、010002、100002等等数字包括
可以发现规律,被包括的总是某个数加上最低位的1来的,例如
001012+000012=001102
001102+000102=010002
010002+010002=100002
假如我们5号位置加上了1,那么6号、8号、16号位置都要加上1,这样就维护了树状数组的正确性
void add(long long i,long long v){
//修改函数
while(i<100010){
f[i]+=v;
i+=i&-i;
//i&-i可以获取i最低位的1,例如5,二进制为00101,-5二进制为11011,按位与后,二进制为00001,获取了最低位1,这样就模拟维护了的树状数组
}
}
查询就更简单理解了,每次都去掉了最低位,那么就变成i-=i&-i就可以了!
long long get(long long i){
//查询函数
long long sum=0;
while(i>=1){
sum+=f[i];
i-=i&-i;
}
return sum;
}
2、单点修改区间查询模板
思路:
上面的思路都是按照单点修改区间查询展开的,相信看完上面你已经理解这个用法了
代码:
#include<bits/stdc++.h>
using namespace std;
long long f[100010];
void add(long long i,long long v){
//修改函数
while(i<100010){
f[i]+=v;
i+=i&-i;
}
}
long long get(long long i){
//查询函数
long long sum=0;
while(i>=1){
sum+=f[i];
i-=i&-i;
}
return sum;
}
int main(){
int n,m;
cin>>n>>m;
for(int i=1;i<=n;i++){
//初始化
int v;
cin>>v;
add(i,v);
}
for(int i=0;i<m;i++){
int g,x,y;
cin>>g>>x>>y;
if(g==1){
//修改
add(x,y);
}else{
//查询
cout<<get(y)-get(x-1)<<endl;
}
}
}
3、区间修改单点查询模板
思路:
区间修改单点查询其实和单点修改区间查询一点区别都没有,树状数组的含义没有遍,依然是求前缀和,只是修改的时候做一下转变,例如区间3~5都加上1,那么只需要在3的位置加1,在6的位置减1,求它的前缀和,实际上就是求单点的值了,即先差分,再前缀和,而单点修改区间查询是先前缀和再差分
代码:
#include<bits/stdc++.h>
using namespace std;
long long f[100010];
void add(long long i,long long v){
//修改函数
while(i<100010){
f[i]+=v;
i+=i&-i;
}
}
long long get(long long i){
//查询函数
long long sum=0;
while(i>=1){
sum+=f[i];
i-=i&-i;
}
return sum;
}
int main(){
int n,m;
cin>>n>>m;
for(int i=1;i<=n;i++){
//初始化
int v;
cin>>v;
add(i,v);
add(i+1,-v);
}
for(int i=0;i<m;i++){
int g,x,y,val;
cin>>g;
if(g==1){
//修改
cin>>x>>y>>val;
add(x,val);
add(y+1,-val);
}else{
//查询
cin>>x;
cout<<get(x)<<endl;
}
}
}
4、区间修改区间查询模板
思路:
区间修改区间查询使用了两个树状数组,看下面模拟的思路大概就明白了:
假如位置3~5加上了5,第一个树状数组f2[3]+=5,f2[6]-=5,第二个树状数组维护f[3]+=2*5,f1[6]-=5*5
单点的前缀和怎么求呢?
ans代表真正的前缀和,ans1代表第一个树状数组前缀和,ans2代表第二个树状数组前缀和
ans[i]=ans1[i]*i-ans2[i]
这个式子能理解吧!例如:
情况1:查询2,在3~5左边 ans[2]=ans1[2]*2-ans2[2]=0*2-0=0;
情况2:查询3,在3~5左边界 ans[3]=ans1[3]*3-ans2[3]=5*3-10=5;
情况3:查询4,在3~5中间 ans[4]=ans1[4]*4-ans2[4]=5*4-10=10;
情况4:查询5,在3~5右边界 ans[5]=ans1[5]*5-ans2[5]=0*5-(-15)=15;
情况5:查询6,在3~5右边 ans[6]=ans1[6]*6-ans2[6]=0*6-(-15)=15;
可以看到单点的区间和已经可以求出来了,那么区间的前缀和就做一个差分就可以了,例如,求区间2~7,那么,答案就是ans[7]-ans[1]
写法有两种,两棵树分开写、或者合并写,思路都是一样的,写法不同而已
写法一:
#include<bits/stdc++.h>
using namespace std;
long long f1[1000010];
long long f2[1000010];
void add1(long long i,long long v){
//修改
while(i<1000010){
f1[i]+=v;
i+=i&-i;
}
}
long long get1(long long i){
//查询
long long sum=0;
while(i>=1){
sum+=f1[i];
i-=i&-i;
}
return sum;
}
void add2(long long i,long long v){
//修改
long long x=i;
while(i<1000010){
f2[i]+=v*(x-1);
i+=i&-i;
}
}
long long get2(long long i){
//查询
long long sum=0;
while(i>=1){
sum+=f2[i];
i-=i&-i;
}
return sum;
}
long long getSum(long long i){
//单点前缀和
return get1(i)*i-get2(i);
}
int main(){
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
int n,m;
cin>>n>>m;
for(int i=1;i<=n;i++){
long long val;
cin>>val;
add1(i,val);
add1(i+1,-val);
add2(i,val);
add2(i+1,-val);
}
for(int i=0;i<m;i++){
long long g,x,y;
cin>>g>>x>>y;
if(g==1){
long long val;
cin>>val;
add1(x,val);
add1(y+1,-val);
add2(x,val);
add2(y+1,-val);
}else{
cout<<getSum(y)-getSum(x-1)<<endl;//区间前缀和
}
}
}
写法二:
#include<bits/stdc++.h>
using namespace std;
long long f1[1000010];
long long f2[1000010];
void add(long long i,long long v){
//修改
long long x=i;
while(i<1000010){
f1[i]+=v;
f2[i]+=v*(x-1);
i+=i&-i;
}
}
long long get(long long i){
//查询
long long sum=0;
long long x=i;
while(i>=1){
sum+=f1[i]*x-f2[i];
i-=i&-i;
}
return sum;
}
int main(){
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
int n,m;
cin>>n>>m;
for(int i=1;i<=n;i++){
long long val;
cin>>val;
add(i,val);
add(i+1,-val);
}
for(int i=0;i<m;i++){
long long g,x,y;
cin>>g>>x>>y;
if(g==1){
long long val;
cin>>val;
add(x,val);
add(y+1,-val);
}else{
cout<<get(y)-get(x-1)<<endl;
}
}
}
感谢观看!