0x42
树状数组
若一个正整数 x x x的二进制表示为 a k − 1 a k − 2 . . . a 2 a 1 a 0 a_{k-1}a_{k-2}...a_2a_1a_0 ak−1ak−2...a2a1a0,其中等于1的位是 { a i 1 , a i 2 , . . . , a i m } \{a_{i_1},a_{i_2},...,a_{i_{m}}\} {ai1,ai2,...,aim},则正整数 x x x可以被“二进制分解”成:
x = 2 i 1 + 2 i 2 + . . . + 2 i m x=2^{i_1}+2^{i_2}+...+2^{i_m} x=2i1+2i2+...+2im
不妨设 i 1 > i 2 > . . . > i m i_1>i_2>...>i_m i1>i2>...>im,进一步地,区间 [ 1 , x ] [1,x] [1,x]可以分成 O ( l o g x ) O(logx) O(logx)个小区间:
1.长度为 2 i 1 2^{i_1} 2i1的小区间 [ 1 , 2 i 1 ] [1,2^{i_1}] [1,2i1]
2.长度为 2 i 2 2^{i_2} 2i2的小区间 [ 2 i 1 + 1 , 2 i 1 + 2 i 2 ] [2^{i_1}+1,2^{i_1}+2^{i_2}] [2i1+1,2i1+2i2]
3.长度为 2 i 3 2^{i_3} 2i3的小区间 [ 2 i 1 + 2 i 2 + 1 , 2 i 1 + 2 i 2 + 2 i 3 ] [2^{i_1}+2^{i_2}+1,2^{i_1}+2^{i_2}+2^{i_3}] [2i1+2i2+1,2i1+2i2+2i3]
……
m.长度为 2 i m 2^{i_m} 2im的小区间 [ 2 i 1 + 2 i 2 + . . . + 2 i m − 1 + 1 , 2 i 1 + 2 i 2 + . . . + 2 i m ] [2^{i_1}+2^{i_2}+...+2^{i_{m-1}}+1,2^{i_1}+2^{i_2}+...+2^{i_m}] [2i1+2i2+...+2im−1+1,2i1+2i2+...+2im]
这些小区间的共同特点是:若区间结尾为 R R R,则区间长度就等于 R R R的“二进制分解”下最小的2的次幂,即 l o w b i t ( R ) lowbit(R) lowbit(R)。例如 x = 7 = 2 2 + 2 1 + 2 0 x=7=2^2+2^1+2^0 x=7=22+21+20,区间 [ 1 , 7 ] [1,7] [1,7]可以分成 [ 1 , 4 ] [1,4] [1,4]、 [ 5 , 6 ] [5,6] [5,6]、 [ 7 , 7 ] [7,7] [7,7]三个小区间,长度分别是 l o w b i t ( 4 ) = 4 lowbit(4)=4 lowbit(4)=4、 l o w b i t ( 6 ) = 2 lowbit(6)=2 lowbit(6)=2和 l o w b i t ( 7 ) = 1 lowbit(7)=1 lowbit(7)=1。
树状数组(Binary Indexed Trees)就是一种基于上述思想的数据结构,其基本用途是维护序列的前缀和。对于给定的序列 a a a,我们建立一个数组 c c c,其中 c [ x ] c[x] c[x]保存序列 a a a的区间 [ x − l o w b i t ( x ) + 1 , x ] [x-lowbit(x)+1,x] [x−lowbit(x)+1,x]中所有数的和,即 ∑ i = x − l o w b i t ( x ) + 1 x a [ i ] \sum_{i=x-lowbit(x)+1}^x a[i] ∑i=x−lowbit(x)+1xa[i]。
事实上,数组 c c c可以看做一个如下图所示的树形结构,图中最下边一行是 N N N个叶子结点(N=16),代表数值 a [ 1 ∼ N ] a[1\sim N] a[1∼N]。该结构满足以下性质:
1.每个内部结点 c [ x ] c[x] c[x]保存以它为根的子树中所有叶节点的和。
2.每个内部结点 c [ x ] c[x] c[x]的子节点个数等于 l o w b i t ( x ) lowbit(x) lowbit(x)的位数。
3.除树根外,每个内部结点 c [ x ] c[x] c[x]的父节点是 c [ x + l o w b i t ( x ) ] c[x+lowbit(x)] c[x+lowbit(x)]。
4.树的深度为 O ( l o g N ) O(logN) O(logN)。
如果 N N N不是2的整次幂,那么树状数组就是一个具有同样性质的森林结构。
树状数组支持的基本操作有两个,第一个操作是查询前缀和,即序列 a a a第 1 ∼ x 1\sim x 1∼x个数的和。按照我们刚才提出的方法,应该求出 x x x的二进制表示中每个等于1的位,把 [ 1 , x ] [1,x] [1,x]分成 O ( l o g N ) O(logN) O(logN)个小区间,而每个小区间的区间和都已经保存在数组 c c c中,可在 O ( l o g N ) O(logN) O(logN)的时间里查询前缀和:
int ask(int x)
{int ans=0;while(x){ans+=c[x];x-=x&(-x);}return ans;
}
如果查询序列 a a a的区间 [ l , r ] [l,r] [l,r]中所有数的和,只需要计算 a s k ( r ) − a s k ( l − 1 ) ask(r)-ask(l-1) ask(r)−ask(l−1)。
树状数组支持的第二个基本操作是单点增加,意思是给序列中的一个数 a [ x ] a[x] a[x]加上 y y y,同时正确维护序列的前缀和。只有节点 c [ x ] c[x] c[x]及其所有祖先节点保存的“区间和”包含 a [ x ] a[x] a[x],而任意一个节点的祖先至多只有 l o g N logN logN个,我们逐一对它们的 c c c值进行更新即可。下面的代码在 O ( l o g N ) O(logN) O(logN)时间内执行单点增加操作:
void add(int x,int y)
{while(x<=N){c[x]+=y;x+=x&(-x);}
}
对树状数组初始化,比较一般的方法是:直接建立一个全为0的数组 c c c,然后对每个位置 x x x执行 a d d ( x , a [ x ] ) add(x,a[x]) add(x,a[x]),时间复杂度为 O ( N l o g N ) O(NlogN) O(NlogN)。通常采用这种初始化方法已经足够。
更高效的方法是:从小到大依次考虑每个位置 x x x,借助 l o w b i t lowbit lowbit运算扫描他的父节点累加求和。若采用这种方法,上面树形结构中每条边只会被遍历一次,时间复杂度为 O ( ∑ k = 1 l o g N k ∗ N / 2 k ) = O ( N ) O(\sum_{k=1}^{logN}k*N/2^k)=O(N) O(∑k=1logNk∗N/2k)=O(N)。
void build()
{for(int i=1;i<=N;++i){c[i]+=a[i];int fa=i+(i&-i);if(fa<=N)c[fa]+=c[i];}
}
1.树状数组与逆序对
任意给定一个集合 a a a,如果用 t [ v a l ] t[val] t[val]保存数值 v a l val val在集合 a a a中出现的次数,那么数组 t t t上的区间和(即 ∑ i = 1 r t [ i ] \sum_{i=1}^rt[i] ∑i=1rt[i]就表示集合 a a a中范围在 [ l , r ] [l,r] [l,r]内的数有多少个。
我们可以在集合 a a a的数值范围上建立一个树状数组,来维护 t t t的前缀和。这样即使在集合 a a a中插入或删除一个数,也可以高效地进行统计。
我们在0x05
节中提到了逆序对问题以及使用归并排序的解法。对于一个序列 a a a,若 i < j i<j i<j且 a [ i ] > a [ j ] a[i]>a[j] a[i]>a[j],则称 a [ i ] a[i] a[i]与 a [ j ] a[j] a[j]构成逆序对。按照上述思路,利用树状数组也可以求出一个序列的逆序对个数:
1.在序列 a a a的数值范围上建立树状数组,初始化为全0。
2.倒序扫描给定的序列 a a a,对于每个数 a [ i ] a[i] a[i]:
(1)在树状数组中查询前缀和 [ 1 , a [ i ] − 1 ] [1,a[i]-1] [1,a[i]−1],累加到答案 a n s ans ans中。
(2)执行“单点增加”操作,即把位置 a [ i ] a[i] a[i]上的数加1(相当于 t [ a [ i ] ] t[a[i]] t[a[i]]++),同时正确维护 t t t的前缀和。这表示数值 a [ i ] a[i] a[i]又出现了1次。
3. a n s ans ans即为所求。
for(int i=n;i;--i)
{ans+=ask(a[i]-1);add(a[i],1);
}
时间复杂度为 O ( ( N + M ) l o g M ) O((N+M)logM) O((N+M)logM), M M M为数值范围的大小。
当数值范围较大时,当然可以先进行离散化,再用树状数组进行计算。不过因为离散化本身就要通过排序实现,所以在这种情况下就不如直接用归并排序来计算逆序对数了。
2.树状数组的扩展应用
区间增加
树状数组仅支持单点增加,需要做出一些转化来解决问题。
新建一个数组 b b b,起初为全零。在区间 [ l , r ] [l,r] [l,r]每个值增加 d d d,我们把它转化成以下两条指令:
1.把 b [ l ] b[l] b[l]加上 d d d。
2.把 b [ r + 1 ] b[r+1] b[r+1]减去 d d d。
执行完后我们考虑一下 b b b数组的前缀和( b [ 1 ∼ x ] b[1\sim x] b[1∼x]的和)的情况:
1.对于 1 ≤ x < l 1\leq x <l 1≤x<l,前缀和不变。
2.对于 l ≤ x ≤ r l\leq x \leq r l≤x≤r,前缀和增加了 d d d。
3.对于 r < x ≤ N r<x\leq N r<x≤N,前缀和不变( l l l处加 d d d, r + 1 r+1 r+1处减 d d d,抵消)。
我们发现, b b b数组的前缀和 b [ 1 ∼ x ] b[1\sim x] b[1∼x]就反映了指令对于 a [ x ] a[x] a[x]产生的影响。
于是我们可以用树状数组来维护数组 b b b前缀和(对 b b b只有单点增加操作)。因为各次操作之间具有可累加性,所以在树状数组上查询前缀和 b [ 1 ∼ x ] b[1\sim x] b[1∼x],就得出了到目前为止所有指令在 a [ x ] a[x] a[x]上增加的数值总和。再加上 a [ x ] a[x] a[x]的初始值,就得到了修改后 x x x位置上的值。
查询区间的和
在区间增加的基础上我们查询区间的和。
b b b数组的前缀和 ∑ i = 1 x b [ i ] \sum_{i=1}^x b[i] ∑i=1xb[i]就是经过这些指令后 a [ x ] a[x] a[x]增加的值。
那么序列 a a a的前缀和 a [ 1 ∼ x ] a[1\sim x] a[1∼x],整体增加的值就是:
∑ i = 1 x ∑ j = 1 i b [ j ] \sum_{i=1}^x \sum_{j=1}^i b[j] i=1∑xj=1∑ib[j]
上式可以改写成:
∑ i = 1 x ∑ j = 1 i b [ j ] = ∑ i = 1 x ( x − i + 1 ) ∗ b [ i ] = ( x + 1 ) ∑ i = 1 x b [ i ] − ∑ i = 1 x i ∗ b [ i ] \sum_{i=1}^x \sum_{j=1}^i b[j]=\sum_{i=1}^x (x-i+1)*b[i]=(x+1)\sum_{i=1}^x b[i]-\sum_{i=1}^x i*b[i] i=1∑xj=1∑ib[j]=i=1∑x(x−i+1)∗b[i]=(x+1)i=1∑xb[i]−i=1∑xi∗b[i]
那么我们再增加一个树状数组,用于维护 i ∗ b [ i ] i*b[i] i∗b[i]的前缀和 ∑ i = 1 x i ∗ b [ i ] \sum_{i=1}^x i*b[i] ∑i=1xi∗b[i],上式就可以直接查询、计算了。
具体来说我们建立两个树状数组 c 0 c_0 c0和 c 1 c_1 c1,起初全部赋值为零。对于每条指令“C l r d
”,执行四个操作:
1.在树状数组 c 0 c_0 c0中,把位置 l l l上的数加 d d d。
2.在树状数组 c 0 c_0 c0中,把位置 r + 1 r+1 r+1上的数减 d d d。
3.在树状数组 c 1 c_1 c1中,把位置 l l l上的数加 l ∗ d l*d l∗d。
4.在树状数组 c 1 c_1 c1中,把位置 r + 1 r+1 r+1上的数减 ( r + 1 ) ∗ d (r+1)*d (r+1)∗d。
另外,我们建立数组 s u m sum sum存储序列 a a a的原始前缀和。对于每天指令“Q l r
”,当然还是拆分成 1 ∼ r 1\sim r 1∼r和 1 ∼ l − 1 1\sim l-1 1∼l−1两个部分,二者相减。写成式子就是:
( s u m [ r ] + ( r + 1 ) ∗ a s k ( c 0 , r ) − a s k ( c 1 , r ) ) − ( s u m [ l − 1 ] + l ∗ a s k ( c 0 , l − 1 ) − a s k ( c 1 , l − 1 ) ) (sum[r]+(r+1)*ask(c_0,r)-ask(c_1,r))-(sum[l-1]+l*ask(c_0,l-1)-ask(c_1,l-1)) (sum[r]+(r+1)∗ask(c0,r)−ask(c1,r))−(sum[l−1]+l∗ask(c0,l−1)−ask(c1,l−1))
typedef long long ll;
int N,Q,a,b,c;
char op;
ll arr[100005];
ll sum[100005];
ll tr1[100005];
ll tr2[100005];ll ask(ll tr[],int x)
{ll ans=0;while(x){ans+=tr[x];x-=x&(-x);}return ans;
}void add(ll tr[],int x,ll y)
{while(x<=N){tr[x]+=y;x+=x&(-x);}
}int main()
{scanf("%d%d",&N,&Q);for(int i=1;i<=N;++i){scanf("%lld",&arr[i]);sum[i]=sum[i-1]+arr[i];}while(Q--){cin>>op;if(op=='Q'){scanf("%d%d",&a,&b);ll ans=sum[b]-sum[a-1];ans+=(b+1)*ask(tr1,b)-ask(tr2,b)-a*ask(tr1,a-1)+ask(tr2,a-1);printf("%lld\n",ans);}else{scanf("%d%d%d",&a,&b,&c);add(tr1,a,c);add(tr1,b+1,-c);add(tr2,a,a*c);add(tr2,b+1,-(b+1)*c);}}return 0;
}
值得指出的是,为什么我们把 ∑ i = 1 x ( x − i + 1 ) ∗ b [ i ] \sum_{i=1}^x (x-i+1)*b[i] ∑i=1x(x−i+1)∗b[i]变成 ( x + 1 ) ∑ i = 1 x b [ i ] − ∑ i = 1 x i ∗ b [ i ] (x+1)\sum_{i=1}^x b[i]-\sum_{i=1}^x i*b[i] (x+1)∑i=1xb[i]−∑i=1xi∗b[i]进行统计?这里的 x x x是关于“前缀和 a [ 1 ∼ x ] a[1\sim x] a[1∼x]”这个询问的变量,而 i i i是在每次修改时影响的对象。
对于前者来说,求和式中每一项同时包含 x x x和 i i i,在修改时无法确定 ( x − i + 1 ) (x-i+1) (x−i+1)的值,只能维护 b [ i ] b[i] b[i]的前缀和。在询问时需要面临一个“系数为等差数列”的求和式,计算起来非常困难。
对于后者来说,求和式中的每一项只与 i i i有关。它通过一次容斥,把 ( x + 1 ) (x+1) (x+1)提取为常量,使得 b [ i ] b[i] b[i]的前缀和与 i ∗ b [ i ] i*b[i] i∗b[i]的前缀和可以分别用树状数组进行维护。这种分离包含有多个变量的项,使公式中不同变量之间相互独立的思想非常重要,我们在下一章讨论动态规划时会多次用到。