二叉索引树,也叫树状数组是一种便于数组单点修改和区间求和的数据结构
主要根据下标的lowbit值来建树
至于lowbit(x),则是(x)&(-(x)),也就是一个二进制数从右边数第一个1代表的数
#define lowbit(x) ((x)&(-(x)))
基础树状数组如下图所示
灰色结点为树状数组中的结点,不难发现,lowbit值相同的结点在同一层上,lowbit值越大越靠近根
我们用一个c数组来存储图中白色长条(最下面结点所代表的区间和就是其本身)的区间和
不难看出
c i = ∑ j = i − l o w b i t ( i ) i a j c_i=\sum_{j=i-lowbit(i)}^ia_j ci=j=i−lowbit(i)∑iaj
在有了以上理论基础后,我们来研究一下树状数组中的相关操作
当我们要修改树状数组中的其中一个结点时,我们不仅需要改这个单点的值,还要修改c数组的值(也就是白色长条所代表的值),我们发现,这个结点只有其正上方的白色长条覆盖着,所以只需要一边往上爬一边修改沿路的c就可以了,如图所示
void add(int x,int k){
while(x<=n){
c[x]+=k;//对沿路的c数组进行修改
x+=lowbit(x);//往上爬,若是这一操作无法理解的话,推荐读者打一下草
}
}
为了求一段前缀和,我们可以在图中发现我们需要往左上一直爬并加上沿路的c数组
如图所示
int query(int x){
int sum=0;
while(x>0){
sum+=c[x];//加上沿路的c数组
x-=lowbit(x);//往左上爬
}
return sum;
}
接下来讲解的就是树状数组中比较难理解的区间操作
区间操作需要的是差分
注:接下来的操作推荐读者与笔者一同推算
设 d i f 1 = a 1 , d i f 2 = a 2 − a 1 , d i f 3 = a 3 − a 2 . . . d i f i = a i − a i − 1 dif_1=a_1,dif_2=a_2-a_1,dif_3=a_3-a_2...dif_i=a_i-a_{i-1} dif1=a1,dif2=a2−a1,dif3=a3−a2...difi=ai−ai−1
特别地, a 0 = 0 a_0=0 a0=0
如果用dif表示出a来,那么可以发现 a i = ∑ j = 1 i d i f j a_i=\sum_{j=1}^idif_j ai=∑j=1idifj
所以得出前缀和
s u m i = ∑ j = 1 i ∑ k = 1 k d i f k sum_i=\sum_{j=1}^i\sum_{k=1}^kdif_k sumi=∑j=1i∑k=1kdifk
= ( d i f 1 ) + ( d i f 1 + d i f 2 ) + . . . + ( d i f 1 + d i f 2 + . . . + d i f i ) =(dif_1)+(dif_1+dif_2)+...+(dif_1+dif_2+...+dif_i) =(dif1)+(dif1+dif2)+...+(dif1+dif2+...+difi)
= i ∗ d i f 1 + ( i − 1 ) ∗ d i f 2 + . . . + 1 ∗ d i f i =i*dif_1+(i-1)*dif_2+...+1*dif_i =i∗dif1+(i−1)∗dif2+...+1∗difi
= i ∗ ( d i f 1 + d i f 2 + . . . + d i f i ) − ( 0 ∗ d i f 1 + 1 ∗ d i f 2 + . . . ( i − 1 ) ∗ d i f i ) =i*(dif_1+dif_2+...+dif_i)-(0*dif_1+1*dif_2+...(i-1)*dif_i) =i∗(dif1+dif2+...+difi)−(0∗dif1+1∗dif2+...(i−1)∗difi)
= i ∗ ∑ j = 1 i d i f j − i ∗ ∑ j = 1 i ( j − 1 ) ∗ d i f j =i*\sum_{j=1}^idif_j-i*\sum_{j=1}^i(j-1)*dif_j =i∗∑j=1idifj−i∗∑j=1i(j−1)∗difj
然后分别用树状数组T1,T2存这两项
那么在区间修改时,如何对T1,T2修改呢?举个例子
对序列 1 , 2 , 3 , 4 , 5 1,2,3,4,5 1,2,3,4,5的[2,4]进行+1操作
操作后的序列为 1 , 3 , 4 , 5 , 5 1,3,4,5,5 1,3,4,5,5
原序列的查分序列为 1 , 1 , 1 , 1 , 1 1,1,1,1,1 1,1,1,1,1
而操作后的查分序列为 1 , 2 , 1 , 1 , 0 1,2,1,1,0 1,2,1,1,0
可以看到,只需要更改第 l l l项和第 r + 1 r+1 r+1项即可,再对应到T1,T2的具体含义中,就可以进行修改操作了
区间[l,r]和
s u m r − s u m l − 1 sum_r-sum_{l-1} sumr−suml−1
= [ r ∗ ∑ j = 1 r d i f j − r ∗ ∑ j = 1 r ( j − 1 ) ∗ d i f j ] − [ ( l − 1 ) ∗ ∑ j = 1 l − 1 d i f j − ( l − 1 ) ∗ ∑ j = 1 l − 1 ( j − 1 ) ∗ d i f j ] =[r*\sum_{j=1}^rdif_j-r*\sum_{j=1}^r(j-1)*dif_j]-[(l-1)*\sum_{j=1}^{l-1}dif_j-(l-1)*\sum_{j=1}^{l-1}(j-1)*dif_j] =[r∗j=1∑rdifj−r∗j=1∑r(j−1)∗difj]−[(l−1)∗j=1∑l−1difj−(l−1)∗j=1∑l−1(j−1)∗difj]
struct BIT{
static const int M=1e5+5;
int c[M];
void add(int x,int k) { while(x<=n) c[x]+=k,x+=lowbit(x); }
int query(int x) { int sum=0;while(x>0) sum+=c[x],x-=lowbit(x);return sum; }
}T1,T2;
//区间修改
void build(int x,int dif){
T1.add(x,dif);
T2.add(x,(x-1)*dif);
}
void interval_add(int l,int r,int k){
T1.add(l,k),T1.add(r+1,-k);
T2.add(l,k*(l-1)),T2.add(r+1,-k*(r+1-1));
}
//区间查询
void interval_query(int l,int r){
return (r*T1.query(r)-(l-1)*T1.query(l-1))-(T2.query(r)-T2.query(l-1));
}
这题的数据还是比较水的,所以笔者直接暴力用前缀和过了
#include
using namespace std;
const int M=1e6+5;
#define int long long
#define lowbit(x) (x)&(-(x))
int n,m;
int c[M];
void add(int x,int k) { while(x<=n) c[x]+=k,x+=lowbit(x); }
int query(int x) { int sum=0;while(x>0) sum+=c[x],x-=lowbit(x);return sum; }
signed main()
{
cin>>n>>m;
for(int i=1,p;i<=n;i++) cin>>p,add(i,p);
while(m--){
int if_case;cin>>if_case;
int x,y,k;
switch (if_case){
case 1:cin>>x>>k;add(x,k);break;
case 2:cin>>x>>y;cout<<query(y)-query(x-1)<<endl;break;//暴力前缀和做法
}
}
return 0;
}
这个题就需要区间操作了,需要的是区间修改
我们因为T1和T2存的都是查分,所以需要加一点操作
a i = ∑ j = 1 i d i f j a_i=\sum_{j=1}^idif_j ai=∑j=1idifj
所以查询第x个数时直接输出T1的前缀和就行
理论存在,实践开始
#include
using namespace std;
#define int long long
#define lowbit(x) ((x)&(-(x)))
int n,m;
struct BIT{
static const int M=5e5+5;
int c[M];
void add(int x,int k) { while(x<=n) c[x]+=k,x+=lowbit(x); }
int query(int x) { int sum=0;while(x>0) sum+=c[x],x-=lowbit(x);return sum; }
}T1,T2;
void build(int x,int dif) { T1.add(x,dif),T2.add(x,(x-1)*dif); }
void interval_add(int l,int r,int k){
T1.add(l,k),T1.add(r+1,-k);
T2.add(l,k*(l-1)),T2.add(r+1,-k*(r+1-1));
}
int query(int x) { return T1.query(x); }
signed main()
{
cin>>n>>m;
int a,c=0;
for(int i=1;i<=n;i++){
cin>>a;
c=a-c;
build(i,c);
c=a;
}
while(m--){
int if_case;
cin>>if_case;
int x,y,k;
switch (if_case){
case 1:cin>>x>>y>>k;interval_add(x,y,k);break;
case 2:cin>>x;cout<<query(x)<<endl;break;
}
}
}
参考资料:刘汝佳·《算法竞赛入门经典训练指南》
就这样,树状数组讲解完了,笔者不多赘述
完结撒花