关于树状数组
简介:树状数组一般用来维护一个序列的前缀和。在求前缀和的时间复杂度为O(logn),支持单点修改,区间修改,区间求和。
不妨用T标识树状数组,a表示原数组。对于T[i],T[lowbit(i)]是T[i]的父亲节点,父亲节点维护着其子树的和。
inline int lowbit(int x){return x&(-x);}
对于建树,每次只需要更新其父亲节点即可。时间复杂度O(n)
void build(int a[],int T[])
{
for(int i=1;i<=n;i++)
{
T[i]+=a[i];
int father=i+lowbit(i);
if(father<=n)T[father]+=a[i];
}
}
一维树状数组
每个T[i],维护原数组中前lowbit(i)个元素的和(包括a[i]本身)。
1、单点修改,区间求和
(1)、单点修改除了修改该点以外还要修改维护该点的节点。
void modify(int index,int value)
{
while(index<=n)T[index]+=value,index+=lowbit(index);
}
(2)、由于树状数组维护的是原数组的前缀和,所以求[l,r]的和sum。有sum=prefix($r$)-prefix($l-1$)。
int sum(int l,int r)
{
int ans=0;
while(r>0)ans+=T[r],r-=lowbit(r);
l--;
while(l>0)ans-=T[l],l-=lowbit(l);
return ans;
}
2、区间修改,区间求和
此时为了方便区间修改,我们需要用树状数组维护原数组a的差分数组b。
(1)、区间修改,例如在修改区间[l,r]加上x。
因为b[i]=a[i]-a[i-1]。
所以有b[l]=a[l]+x-a[l-1],b[r+1]=b[r+1]-(b[r]+x)=b[r+1]-x;
这样区间修改就可以转化成两个点的单点修改,方法同上。
(2)、区间求和。
∑i=1xai=∑i=1x∑j=1ibj=∑i=1xbi⋅(x−i+1)=(x+1)⋅∑i=1xbi−∑i=1xi⋅bi\begin{aligned}
\sum_{i=1}^xa_i&=\sum_{i=1}^x\sum_{j=1}^ib_j
\\&=\sum_{i=1}^xb_i\cdot(x-i+1)\\
&=(x+1)\cdot\sum_{i=1}^xb_i-\sum_{i=1}^xi\cdot b_i
\end{aligned}i=1∑xai=i=1∑xj=1∑ibj=i=1∑xbi⋅(x−i+1)=(x+1)⋅i=1∑xbi−i=1∑xi⋅bi
可以得出,想要通过差分数组进行快速区间求和,还需要维护i∗bii*b_ii∗bi这个数组。
int sum(int r)
{
int t=r;
int ans=0;
while(r>0){
ans+=T1[r];//差分数组
ans-=T2[r];//i*差分数组
r-=lowbit(r);
}
return ans;
}
二维维树状数组
要注意二维树状数组T[n][m]并不是n个长度维m的树状数组,二维树状数组无论在以行还是以列来看都要展现出树状数组的性质特点。
T[x][y]维护的是右下角为(x,y),行数为 lowbit(x),列数为 lowbit(y) 的矩形区间的区间和。
1、单点修改,区间求和。
void modify(int x,int y,int v)//T[1~n][1~m]
{
while(x<=n)
{
int t=y;
while(t<=m)T[x][t]+=v,t+=lowbit(t);
x+=lowbit(x)
}
}
void sum1(int x,int y)//求从[1,1]到[x,y]之间的矩形区间的和
{
int ans=0;
while(x>0)
{
int t=y;
while(t>0)ans+=T[x][t],t-=lowbit(t);
x-=lowbit(x);
}
return ans;
}
void sum(int a,int b,int c,int d)//请画图
{
return sum1(c,d)-sum1(c,b-1)-sum1(a-1,d)+sum1(a-1,b-1);
}
2、区间修改,区间求和。(请先好好理解一维的情况)
同一维数组的思想一样,为了方便区间修改,我们需要维护的是原数组的差分数组。
(1)、区间修改,例如在区间(x1,y1)到(x2,y2)之间的矩形区间加上x。
对于数组a的差分数组b。
由二维前缀和的知识可得,b[i][j]=a[i][j]−a[i−1][j]−a[i][j−1]+a[i−1][j−1]b[i][j]=a[i][j]-a[i-1][j]-a[i][j-1]+a[i-1][j-1]b[i][j]=a[i][j]−a[i−1][j]−a[i][j−1]+a[i−1][j−1]
因为a[x1][y1]+=x,所以b[x1][y2]+=x;
因为a[x2][y2]+=x,所以b[x2+1][y2+1]+=x;
因为a[x2][y1]+=x,所以b[x2+1][y1]-=x;
因为a[x1][y2]+=x,,所以b[x2][y2+1]-=x;
其他b[x][y]要么没被影响到,要么影响两两抵消。
所以此时的区间修改就转化成了对4个点的修改。
(2)、区间求和。
∑i=1x∑j=1yaij=∑i=1x∑j=1y(∑u=1i∑v=1jbuv)=∑i=1x∑j=1ybij(x−i+1)(y−j+1)=∑i=1x∑j=1y(x+1)(y+1)bij−(x+1)jbij−(y+1)ibij+ijbij\begin{aligned}
\sum_{i=1}^x\sum_{j=1}^ya_{ij}&=\sum_{i=1}^x\sum_{j=1}^y
\left ( \sum_{u=1}^i\sum_{v=1}^jb_{uv}\right )\\
&=\sum_{i=1}^x\sum_{j=1}^yb_{ij}(x-i+1)(y-j+1)\\
&=\sum_{i=1}^x\sum_{j=1}^y(x+1)(y+1)b_{ij}-(x+1)jb_{ij}
-(y+1)ib_{ij}+ijb_{ij}
\end{aligned}i=1∑xj=1∑yaij=i=1∑xj=1∑y(u=1∑iv=1∑jbuv)=i=1∑xj=1∑ybij(x−i+1)(y−j+1)=i=1∑xj=1∑y(x+1)(y+1)bij−(x+1)jbij−(y+1)ibij+ijbij
所以我们一共要维护四个数组,分别是
T1=bij,T2=i∗bij,T3=j∗bij,T4=i∗j∗bijT1=b_{ij},T2=i*b_{ij},T3=j*b_{ij},T4=i*j*b_{ij}T1=bij,T2=i∗bij,T3=j∗bij,T4=i∗j∗bij
在修改时,需要同时对4个数组进行修改。
void add(int x,int y,ll k)//分别维护四个树状数组
{
if(x<=0||y<=0)return ;
ll v1=k,v2=x*k,v3=y*k,v4=x*y*k;
while(x<=n)
{
ll temp=y;
while(temp<=m)
{
t1[x][temp]+=v1;
t2[x][temp]+=v2;
t3[x][temp]+=v3;
t4[x][temp]+=v4;
temp+=lowbit(temp);
}
x+=lowbit(x);
}
return ;
}
void add1(ll a,ll b,ll c,ll d,ll k)//给(a,b)与(c,d)之间的矩阵加x
{
add(a,b,k);
add(c+1,b,-k);
add(a,d+1,-k);
add(c+1,d+1,k);
}
ll sum(ll (*a)[2100],int x,int y)
{
ll ans=0;
while(x>0)
{
int t2=y;
while(t2>0)ans+=a[x][t2],t2-=lowbit(t2);
x-=lowbit(x);
}
return ans;
}
ll sum1(int x,int y)//求从(1,1)到(x,y)的矩阵和
{
if(x<=0||y<=0)return 0;
ll ans=0;
ans+=(x+1)*(y+1)*sum(t1,x,y);
ans-=(x+1)*sum(t3,x,y);
ans+=sum(t4,x,y);
ans-=(y+1)*sum(t2,x,y);
return ans;
}
ll sum2(int a,int b,int c,int d)//求以(a,b)为左上端点,(c,d)为右下端点的矩阵和
{
return sum1(c,d)-sum1(a-1,d)-sum1(c,b-1)+sum1(a-1,b-1);
}