树状数组可是个好东西
树状数组(Binary Indexed Tree(B.I.T), Fenwick Tree)是一个查询和修改复杂度都为 O ( l o g 2 n ) O(log_2n) O(log2n)的数据结构。主要用于查询任意两位之间的所有元素之和,但是每次只能修改一个元素的值;经过简单修改可以在 O ( l o g 2 n ) O(log_2n) O(log2n)的复杂度下进行范围修改,但是这时只能查询其中一个元素的值(如果加入多个辅助数组则可以实现区间修改与区间查询,当然,本文也要介绍这种方法)。
树状数组长这样:
它像是一个简化前缀和的形式;可是表示方法很奇怪,看上面的图,你能发现什么规律吗?
如果想要查找一段区间,例如 A [ 3 ] → A [ 7 ] A[3] \to A[7] A[3]→A[7]的和,我们要找到以下几块:
A [ 1 ] → A [ 7 ] A[1] \to A[7] A[1]→A[7]之和: C [ 7 ] + C [ 6 ] + C [ 4 ] C[7] + C[6] + C[4] C[7]+C[6]+C[4]
A [ 1 ] → A [ 2 ] A[1] \to A[2] A[1]→A[2]之和: C [ 2 ] C[2] C[2]
然后利用前缀和思想,相减即可得到 A [ 3 ] → A [ 7 ] A[3] \to A[7] A[3]→A[7]的和。
看上去比较难理解,放一张图:
这是怎么找到的呢?先别急,我们来看看单点的修改:
如果更新了一个节点,例如把 A [ 5 ] A[5] A[5]加上一,我们需要依次找到 C [ 5 ] C[5] C[5]的父亲节点: C [ 5 ] → C [ 6 ] → C [ 8 ] C[5] \to C[6] \to C[8] C[5]→C[6]→C[8] ,各加上1就能得到了。
那我们应该怎么找这样的节点呢?
继续看上面那张图:在他的节点更新的时候,他的二进制表示数是如何变化的呢?
看看图里的例子:
C [ ( 111 ) 2 ] → C [ ( 110 ) 2 ] → C [ ( 100 ) 2 ] C[(111)_2] \to C[(110)_2] \to C[(100)_2] C[(111)2]→C[(110)2]→C[(100)2]
再来一组?( A [ 1 ] → A [ 5 ] A[1] \to A[5] A[1]→A[5]之和)
C [ ( 101 ) 2 ] → C [ ( 100 ) 2 ] C[(101)_2] \to C[(100)_2] C[(101)2]→C[(100)2]
或许你发现了什么?( A [ 1 ] → A [ 9 ] A[1] \to A[9] A[1]→A[9]之和)
C [ ( 1001 ) 2 ] → C [ ( 1000 ) 2 ] C[(1001)_2] \to C[(1000)_2] C[(1001)2]→C[(1000)2]
重点来了
- 你或许会发现最后剩下数组下标的是2的整数次幂
- 你或许会发现末尾有一坨0
- 你或许会发现,每次变化会少掉一个1
- 你或许会发现,每次变化少掉的1总是在低位
没错,数位为1的最低位!
我们就可以把求最低位1的这个运算叫做 L o w b i t Lowbit Lowbit
$ Lowbit(k) = k & -k $
每次变化减去一个 L o w b i t ( Lowbit( Lowbit(当前节点 ) ) )
再看看单点修改(可以借助上面的图理解一下):
举个例子( A [ 5 ] A[5] A[5]的所有父节点,祖先):
$C[(101)_2] \to C[(110)_2] \to C[(1000)_2] $
或许你又发现了什么?( A [ 4 ] A[4] A[4]的所有父节点,祖先)
C [ ( 100 ) 2 ] → C [ ( 1000 ) 2 ] C[(100)_2] \to C[(1000)_2] C[(100)2]→C[(1000)2]
并没有发现什么
每次变化加上一个 L o w b i t ( Lowbit( Lowbit(当前节点 ) ) )
? ? ? ? ???? ????
(有疑问是很正常的,用代码解决)
至此我们就 讲完了
后面还有,不要走开
#include <cstdio>
#define lowbit(x) ((x)&(-x))
int tree[500010],a[500010],n;
void update(int x,int k) {
while(x<=n) {
tree[x]+=k;
x+=lowbit(x);
}
}
void build() {
for(int i=1; i<=n; i++) update(i,a[i]);
}
int query(int x) {
int sum=0;
while(x) {
sum+=tree[x];
x-=lowbit(x);
}
return sum;
}
int main() {
int m,mode,left,right;
scanf("%d%d",&n,&m);
for(int i=1; i<=n; i++) scanf("%d",&a[i]);
build();
for(int i=1; i<=m; i++) {
scanf("%d%d%d",&mode,&left,&right);
if(mode==1) update(left,right);
else printf("%d\n",query(right)-query(left-1));
}
return 0;
}
嗯没错,以上是树状数组1的题解
接下来我们来讲树状数组2与线段树1的歪解
歪解总是比正解快不是吗
树状数组常数小,代码少,容易理解,内存占用少,在求解可用前缀和加减的区间信息或从1开始的区间信息这两个方面比线段树不知道好多少倍。唯一遗憾的是没有区间修改…
但聪明的我们总有办法
我们可以当作维护两个数组(听上去就很恶心,而且这两个数组都不是原数据,而是差分的序列)
这两个数组用来干什么呢?怎么表示呢?
来揭晓答案~~
(下文中 A i A_i Ai表示原数据,假设 C 1 = A 1 C_1=A_1 C1=A1)
我们像维护正常的树状数组那样,维护以下两组数据:
C i = A i − A i − 1 C_i=A_i-A_{i-1} Ci=Ai−Ai−1
D i = C i × ( i − 1 ) D_i=C_i \times (i-1) Di=Ci×(i−1)
那么
根据差分的思想
如果我们修改 A l → A r A_l \to A_r Al→Ar的区间,统一加上 P P P,只需要:
C l + = P , C r + 1 − = P , D l + = P ∗ ( l − 1 ) , D r + 1 − = P ∗ ( r + 1 − 1 ) C_l+=P,C_{r+1}-=P,D_l+=P*(l-1),D_{r+1}-=P*(r+1-1) Cl+=P,Cr+1−=P,Dl+=P∗(l−1),Dr+1−=P∗(r+1−1)
也就是说(放心,两个式子没有差别)
C l + = P , C r + 1 − = P , D l + = P ∗ ( l − 1 ) , D r + 1 − = P ∗ r C_l+=P,C_{r+1}-=P,D_l+=P*(l-1),D_{r+1}-=P*r Cl+=P,Cr+1−=P,Dl+=P∗(l−1),Dr+1−=P∗r
难题来了!查找区间呢?
看看以下的推导(求解
A
[
1
]
→
A
[
n
]
A[1] \to A[n]
A[1]→A[n]的和):
通过基本树状数组,我们应该可以在 O ( l o g 2 n ) O(log_2 n) O(log2n)的时间内求出以下两个
E i = ∑ j = 1 i C i E_i=\sum\limits_{j=1}^iC_i Ei=j=1∑iCi
F i = ∑ j = 1 i D i F_i=\sum\limits_{j=1}^iD_i Fi=j=1∑iDi
你会发现,我们求的是C,D两数组的前缀和
然后把 F i F_i Fi变化一下
F i = ∑ j = 1 i D i = ∑ j = 1 i C i × ( i − 1 ) F_i=\sum\limits_{j=1}^iD_i=\sum\limits_{j=1}^iC_i \times (i-1) Fi=j=1∑iDi=j=1∑iCi×(i−1)
那么我们要求的 A i A_i Ai的前缀和应该是
∑ i = 1 n A i = ∑ i = 1 n ∑ j = 1 i C i = ∑ i = 1 n ( n − i + 1 ) × C i = n × ∑ i = 1 n C i − ∑ i = 1 n C i × ( i − 1 ) = n × E i − F i \sum\limits_{i=1}^nA_i=\sum\limits_{i=1}^n\sum\limits_{j=1}^iC_i=\sum\limits_{i=1}^n(n-i+1) \times C_i=n \times\sum\limits_{i=1}^nC_i-\sum\limits_{i=1}^nC_i \times (i-1)=n \times E_i - F_i i=1∑nAi=i=1∑nj=1∑iCi=i=1∑n(n−i+1)×Ci=n×i=1∑nCi−i=1∑nCi×(i−1)=n×Ei−Fi
当然也可以这样变
维护一个 G i G_i Gi来替换 F i F_i Fi,也就是
G i = C i × k G_i=C_i \times k Gi=Ci×k
∑ i = 1 n A i = ∑ i = 1 n ∑ j = 1 i C i = ∑ i = 1 n ( n − i + 1 ) × C i = ( n + 1 ) × ∑ i = 1 n C i − ∑ i = 1 n C i × i = ( n + 1 ) × E i − G i \sum\limits_{i=1}^nA_i=\sum\limits_{i=1}^n\sum\limits_{j=1}^iC_i=\sum\limits_{i=1}^n(n-i+1) \times C_i=(n+1) \times\sum\limits_{i=1}^nC_i-\sum\limits_{i=1}^nC_i \times i=(n+1) \times E_i - G_i i=1∑nAi=i=1∑nj=1∑iCi=i=1∑n(n−i+1)×Ci=(n+1)×i=1∑nCi−i=1∑nCi×i=(n+1)×Ei−Gi
有兴趣的读者可以尝试一下上面那种实现方式。
所以,再用代码解释一切
#include <cstdio>
#define M 100100
#define ll long long
#define lb(x) ((x)&(-x))
ll a[M],sum[M],sum2[M];
int n;
void one_up(int k,ll p) {
for(int i=k; i<=n; i+=lb(i)) {
sum[i]+=p;
sum2[i]+=p*(k-1);
}
}
ll one_qu(int k) {
ll ans=0;
for(int i=k; i; i-=lb(i)) ans+=k*sum[i]-sum2[i];
return ans;
}
void Update(int x,int y,ll p) {
one_up(x,p);
one_up(y+1,-p);
}
ll Query(int x,int y) {
return one_qu(y)-one_qu(x-1);
}
int main() {
int m,md,l,r;
ll p;
scanf("%d%d",&n,&m);
for(int i=1; i<=n; i++) scanf("%d",&a[i]),Update(i,i,a[i]);
for(int i=1; i<=m; i++) {
scanf("%d%d%d",&md,&l,&r);
if(md==1) scanf("%lld",&p),Update(l,r,p);
else if(md==2) printf("%lld\n",Query(l,r));
}
return 0;
}
以上是线段树1的AC代码
至于第二种做法,只要稍微改一下就好了(明明都差不多的,怎么就能叫"第二种做法"?!!)
下面展示代码片段
void one_up(int k,ll p) {
for(int i=k; i<=n; i+=lb(i)) {
sum[i]+=p;
sum2[i]+=p*k;
}
}
ll one_qu(int k) {
ll ans=0;
for(int i=k; i; i-=lb(i)) ans+=(k+1)*sum[i]-sum2[i];
return ans;
}
事实证明,这常数可是不一般的大…
所以我可以同时发三份题解吗
U p d a t e d D e c . 8 t h 21 : 54 Updated \space Dec.8^{th} \space 21:54 Updated Dec.8th 21:54刚刚发现上面的公式有个地方打错了,赶紧改