树状数组

来源:互联网 发布:人工智能的参考文献 编辑:程序博客网 时间:2024/05/16 17:50

定义

树状数组(Binary Indexed Tree(BIT), Fenwick Tree)是一个查询和修改复杂度都为log(n)的数据结构。主要用于查询任意两位之间的所有元素之和(维护前缀和),

但是每次只能修改一个元素的值。

逻辑结构上是一棵树,但实际上只是个数组。











树状数组和线段树:树状数组能解决的,线段树也能解决,但是树状数组的代码比线段树要简洁很多。

关于树状数组主要有3个函数,lowbit,add,sum


lowbit【关于二进制原码,补码,反码内容】:











add:





















sum:












树状数组一般用于求区间和,逆序数,区间最大值等。


---树状数组求区间和的一些常见模型【来源】---

树状数组在区间求和问题上有大用,其三种复杂度都比线段树要低很多……有关区间求和的问题主要有以下三个模型(以下设A[1..N]为一个长为N的序列,初始值为全0):


(1)“改点求段”型,即对于序列A有以下操作:


【1】修改操作:将A[x]的值加上c;


【2】求和操作:求此时A[l..r]的和。


这是最容易的模型,不需要任何辅助数组。树状数组中从x开始不断减lowbit(x)(即x&(-x))可以得到整个[1..x]的和,而从x开始不断加lowbit(x)则可以得到x的所有前趋。

void ADD(int x, int c){     for (int i=x; i<=n; i+=i&(-i)) a[i] += c;}int SUM(int x){    int s = 0;    for (int i=x; i>0; i-=i&(-i)) s += a[i];    return s;}

操作【1】:ADD(x, c);


操作【2】:SUM(r)-SUM(l-1)。


(2)“改段求点”型,即对于序列A有以下操作:


【1】修改操作:将A[l..r]之间的全部元素值加上c;


【2】求和操作:求此时A[x]的值。


这个模型中需要设置一个辅助数组B:B[i]表示A[1..i]到目前为止共被整体加了多少(或者可以说成,到目前为止的所有ADD(i, c)操作中c的总和)。


则可以发现,对于之前的所有ADD(x, c)操作,当且仅当x>=i时,该操作会对A[i]的值造成影响(将A[i]加上c),又由于初始A[i]=0,所以有A[i] = B[i..N]之和!而ADD(i, c)(将A[1..i]整体加上c),将B[i]加上c即可——只要对B数组进行操作就行了。


这样就把该模型转化成了“改点求段”型,只是有一点不同的是,SUM(x)不是求B[1..x]的和而是求B[x..N]的和,此时只需把ADD和SUM中的增减次序对调即可(模型1中是ADD加SUM减,这里是ADD减SUM加)。

void ADD(int x, int c){     for (int i=x; i>0; i-=i&(-i)) b[i] += c;}int SUM(int x){    int s = 0;    for (int i=x; i<=n; i+=i&(-i)) s += b[i];    return s;}

操作【1】:ADD(l-1, -c); ADD(r, c);


操作【2】:SUM(x)。


(3)“改段求段”型,即对于序列A有以下操作:


【1】修改操作:将A[l..r]之间的全部元素值加上c;


【2】求和操作:求此时A[l..r]的和。


这是最复杂的模型,需要两个辅助数组:B[i]表示A[1..i]到目前为止共被整体加了多少(和模型2中的一样),C[i]表示A[1..i]到目前为止共被整体加了多少的总和(或者说,C[i]=B[i]*i)。


对于ADD(x, c),只要将B[x]加上c,同时C[x]加上c*x即可(根据C[x]和B[x]间的关系可得);


而ADD(x, c)操作是这样影响A[1..i]的和的:若x<i,则会将A[1..i]的和加上x*c,否则(x>=i)会将A[1..i]的和加上i*c。也就是,A[1..i]之和 = B[i..N]之和 * i + C[1..i-1]之和。
这样对于B和C两个数组而言就变成了“改点求段”(不过B是求后缀和而C是求前缀和)。
另外,该模型中需要特别注意越界问题,即x=0时不能执行SUM_B操作和ADD_C操作!

void ADD_B(int x, int c){     for (int i=x; i>0; i-=i&(-i)) B[i] += c;}void ADD_C(int x, int c){     for (int i=x; i<=n; i+=i&(-i)) C[i] += x * c;}int SUM_B(int x){    int s = 0;    for (int i=x; i<=n; i+=i&(-i)) s += B[i];    return s;}int SUM_C(int x){    int s = 0;    for (int i=x; i>0; i-=i&(-i)) s += C[i];    return s;}inline int SUM(int x){    if (x) return SUM_B(x) * x + SUM_C(x - 1); else return 0;}

操作【1】:
ADD_B(r, c); ADD_C(r, c);
if (l > 1) {ADD_B(l - 1, -c); ADD_C(l - 1, -c);}


操作【2】:SUM(r) - SUM(l - 1)。


----树状数组求区间最值【来源】----

树状数组(Binary Index Tree)利用二进制的一些性质巧妙的划分区间,是一种编程,时间和空间上都十分理想的求区间和的算法,同样我们可以利用树状数组优美的区间划分方法来求一个序列的最值


约定以 num[]  表示原数组, 以 idx[] 表示索引数组, Lowbit(x)=x&(-x) 


树状数组求和时通过构造数组 idx[] 使 idx[k]=sum(num[tk]), tk [k-Lowbit(k)+1,k], 使用同样的方法构造最值索引数组:


以最大值为例, 先讨论询问过程中不对数组做任何修改的情况, 用 idx[k] 记录 [k-Lowbit(k)+1,k] 区间内的最大值, 可以仿照求和时的方法得到:

void Init(int n)//构造区间最大值的树状数组{               //从后往前更新,更新位置i的所有孩子节点for (int i = 1; i <= n; i++){idx[i] = num[i];for (int j = 1; j<Lowbit(i); j <<= 1){idx[i] = MAX(idx[i], idx[i - j]);}}}

然后再来看查询的问题, 对于区间 [l,r] 把该区间转化为多个的小区间再进行求最值, 方法是从后往前对每一个索引数的范围进行判断, 如在进行到第k项时,该数控制的范围是 [k-Lowbit(k)+1,k], 如果k-Lowbit(k)+1在所求的范围内的话则将该区间的最值加入最值的判断,然后转至地k-Lowbit(k),否则的话就只对第k个数进行最值判断,然后转至k-1,具体实现如下:

int Query(int l, int r)//查询,[l,r]的最大值{int ans = num[r];while (1){ans = MAX(ans, num[r]);if (r == l){break;}for (r -= 1; r - l >= Lowbit(r); r -= Lowbit(r)){ans = MAX(ans, idx[r]);}}return ans;}

然后我们可以进一步扩展到边查询边修改的情况

每次直接去更新父亲节点自然是不行的, 为了维护索引数组的正确性,我们在对每个父亲节点进行更新时都要查询他的所有儿子节点,在其中取最优值, 得到代码如下:

void Modify(int p, int v, int n)//修改,位置p的数改为v{num[p] = v;for (int i = p; i <= n; i += Lowbit(i)){idx[i] = v;for (int j = 1; j<Lowbit(i); j <<= 1){idx[i] = MAX(idx[i], idx[i - j]);}}}

关于剪枝:
1、当更新的节点大于根节点,不需要查询他的子树。

2、当更新后的根节点没变时,不需要更新下颗子树。



补充:树状数组对于“一边插入,一边查询”的问题非常高效,但是对于“一次插入,多次查询”问题并不高效。

    对于“一次性插入,多次查询”问题,只需要用的前缀数组(统计前i项的和):num[i]=num[i-1]+a[i];


0 0
原创粉丝点击