6.10 Fenwick树

概念

  Fenwick树是用于快速计算前缀和的数据结构。前缀和,就是一个数列的前N项和,是不是有点像级数的概念。
  当然最笨的办法是按顺序相加,实时计算。优化一下就是缓存计算结果,这样能快速返回,用空间换时间。但是采取缓存方案时,修改的运算量比较大。比如修改数组的第0项,那么影响了前0项、前1项以及所有项的前缀和。
  有没有均衡的方案?我们可以用二进制分解。比如15=8+4+2+1,可以这样计算前十五项的前缀和,定义一个缓存数组bit:

    bit[8] = a[0] + a[1] +a[2] +a[3] +a[4]+ a[5]+ a[6]+ a[7]; //前8项的和
	bit[12] = a[8] + a[9] + a[10] + a[11]; // 中间4项的和
	bit[14] = a[12] + a[13]; //中间2项的和
	bit[15] = a[14]; //最后一项的和
	return bit[8] + bit[12] + bit[14] + bit[15]

  那么前15项的前缀和,就是前八项前缀和加上第9项到12项的和。所以再回来看这个缓存数组bit。bit是bit indexed tree的缩写。缓存数组bit的第1、2、4、8项,也就是 2 n 2^n 2n项,必须是完整的前缀和。那剩下的就是两种:奇数项和偶数项。奇数项最好处理,因为奇数的分解最后一定剩下一个1,所以对于所有奇数项, b i t [ n ] = a [ n − 1 ] bit[n]=a[n-1] bit[n]=a[n1]
  最麻烦的是不是 2 n 2^n 2n的偶数项,比如6、10、12、14这几项。看上式中,为什么bit[12]等于四项的和,而bit[14]却只等于两项的和?
  因为12=8+4,8存储在bit[8]中,所以bit[12]等于4项的和。而14=8+4+2,这个式子里的8和4由bit[8]和bit[12]存储了。所以bit[14]只需要存储两项就可以了。这实际上就是二进制最后一个1的大小。对于奇数项其实也是符合这个规则的,其实对于索引为 2 n 2^n 2n的项数也是适用的。
  所以这个总结:假设bit数组的索引n的二进制形式,从右到左,第一个1代表的数值x,那么有:
b i t [ n ] = ∑ i = n − x n − 1 a [ i ] bit[n] = \sum_{i=n-x}^{n-1} a[i] bit[n]=i=nxn1a[i]
  例如,把n=12代进去,12的二进制形式为1100,最低的1代表了4。所以x=4,所以:
b i t [ 12 ] = ∑ i = 12 − 4 12 − 1 a [ i ] = ∑ i = 8 11 a [ i ] = a [ 8 ] + a [ 9 ] + a [ 10 ] + a [ 11 ] bit[12] = \sum_{i=12-4}^{12-1} a[i]= \sum_{i=8}^{11} a[i]=a[8]+a[9]+a[10]+a[11] bit[12]=i=124121a[i]=i=811a[i]=a[8]+a[9]+a[10]+a[11]
  用一个图来表示这个公式:
在这里插入图片描述

  在上面这个公式的基础上进行数据再合并。那么这种数据结构就叫做Fenwick树。其图如下,图中B代表bit数组,a代表原始数组:
在这里插入图片描述

修改算法

  在上图可以看到,每个数据项的变化都会影响到本身及以后的节点。以a[1]为例子,会影响到b2、b4、b8,也就是影响到了bit数组的第2、4、8项。而a[4]的修改会影响到b5、b6和b8,也就是bit数组的第5、6、8项。将上图的箭头反向,可以形象地看到效果:
在这里插入图片描述

  到底怎么计算呢?我们看1到2到4到8的过程,变成二进制很容易发现,是1变成10,再变成100,再变成1000。而其他的数字,比如 5 → 6 → 8 5\rarr6\rarr8 568,换成二进制是 101 → 110 → 1000 101\rarr110\rarr1000 1011101000。逆推是很难的,比顺推要难多了,但是我们耐心理完。因为前文有这个公式:
b i t [ n ] = ∑ i = n − x n − 1 a [ i ] bit[n] = \sum_{i=n-x}^{n-1} a[i] bit[n]=i=nxn1a[i]
  这个公式定义了我需要谁,所以我们再反过来想一想谁需要我。以bit[6]为例子6的二进制是110,最低位是10,所以包含了两个数,这两个数肯定是自己和小一点的数。其实6的二进制是110,保持100不变,加上自身的10和比10小的01,那么就是6和5。所以逆向思维过来,一个索引数被别人包含,就是这样计算,以5为例子,二进制位101,首先被自己包含。再找下一个尾部为0的数字,那就是110,再找下一个最接近自己的尾部至少两个0的数字,那就是1000(注意不能是1100,因为1100比1000大),然后再找下一个尾部至少4个0的数字,就是10000。其实更准确地说是最接近自己的尾部的0比自己多的数字。所以碰到了 2 n 2^n 2n之后,就是 2 n + 1 2^{n+1} 2n+1了。逻辑明白了,那么计算的难题就来了。我们知道要得到下一个尾部0比自己多的数字,就需要找到自己最低位的1,然后在这个位置上加上一个1,就是下一个尾部的0比自己多的且比自己大的数了。以5=101,为例子,最低位的1,再加上1就是110,也就是6。其实就是抽出最低位的1,然后加上这个数就可以了。把这个增量定义为 δ \delta δ,那么就用位运算来找这个数吧。
  首先,一个数的取反,把最低位的1变成了最高位的0,比如00111000变成了11000111,那么再加上一个1,这个最高位的0,恰好是同位置的1,也就是最低位的1,还是刚才的例子。
∼ 00111000 = 11000111 ∼ 00111000 + 1 = 11001000 \sim 00111000 = 11000111\\ \sim 00111000 +1 = 11001000 00111000=1100011100111000+1=11001000
  这个时候把两个数字00111000和11001000进行对比,因为前面的位刚好互反,原数字最低位的1,两个数字都是1,更低的位,原数字全部是0,这个数字也全部是0。所以做一个与运算,就可以把前面全部变成0,后面也全部变成0,原数字最低位的1还是1。算一下就是:
00111000   &   11001000 = 1000 00111000 ~ \& ~11001000 = 1000 00111000 & 11001000=1000
所以我们的增量 δ \delta δ
δ = a & ( ∼ a + 1 ) \delta = a \& (\sim a +1) δ=a&(a+1)
  再恶补下整数编码的基础,计算机界定义所有位为1的有符号整数是-1,一个数和它的反码相加,得到的是全部为1的数,也就是-1。那么就有了下面的公式:
a + ( ∼ a ) = − 1 ∴ ∼ a = − 1 − a ∴ ∼ a + 1 = − a ∴ − a = ∼ a + 1 a+(\sim a)=-1 \\ \therefore \sim a = -1 -a\\ \therefore \sim a +1 = -a\\ \therefore -a =\sim a +1\\ a+(a)=1a=1aa+1=aa=a+1
  所以我们的增量就是 a & − a a\& -a a&a

求和算法

  查询也就是求和了。求和需要用到bit数组。以前15项和为例子:
∵ 15 = 8 + 4 + 2 + 1 = 2 3 + 2 2 + 2 1 + 2 0 ∴ a [ 0 ] + a [ 1 ] + a [ 2 ] + a [ 3 ] + a [ 4 ] + a [ 5 ] + a [ 6 ] + a [ 7 ] = b i t [ 8 ] a [ 8 ] + a [ 9 ] + a [ 10 ] + a [ 11 ] = b i t [ 12 ] a [ 12 ] + a [ 13 ] = b i t [ 14 ] a [ 14 ] = b i t [ 15 ] ∴ s u m ( 15 ) = b i t [ 8 ] + b i t [ 12 ] + b i t [ 14 ] + b i t [ 15 ] \because 15= 8+4+2+1 = 2^3+2^2+2^1+2^0\\ \therefore a[0] + a[1] +a[2] +a[3] +a[4]+ a[5]+ a[6]+ a[7] = bit[8] \\ a[8]+a[9]+a[10]+a[11] = bit[12]\\ a[12]+a[13] = bit[14]\\ a[14]=bit[15]\\ \therefore sum(15)= bit[8] +bit[12]+ bit[14]+bit[15] 15=8+4+2+1=23+22+21+20a[0]+a[1]+a[2]+a[3]+a[4]+a[5]+a[6]+a[7]=bit[8]a[8]+a[9]+a[10]+a[11]=bit[12]a[12]+a[13]=bit[14]a[14]=bit[15]sum(15)=bit[8]+bit[12]+bit[14]+bit[15]
  这个算法呢?以15为例子,15的二进制形式为1111,二进制分解为1000,加上1000后面的1100,再加上1110,最后是1111。恰好对于了8,8后面的第4个,8后面的第6个,8后面的第7个。我们反过来,这样写:
s u m ( 15 ) = b i t [ 15 ] + b i t [ 14 ] + b i t [ 12 ] + b i t [ 8 ] sum(15)=bit[15] +bit[14]+bit[12]+bit[8] sum(15)=bit[15]+bit[14]+bit[12]+bit[8]
  其实就是把15的二进制形式1111不停地减去上面算出的 δ \delta δ

代码实现

  原理明白后代码就很短了。

public class FenWickTree {

    private int[] values;
    private int[] bit;

    public FenWickTree(int length) {
        values = new int[length];
        bit = new int[length + 1];
    }

    public void setValues(int index, int value) {
        values[index] = value;
        index += 1;
        while (index < bit.length) {
            bit[index] += value;
            index += index & -index;
        }
    }

    public int getSum(int index) {
        int sum = 0;
        while (index > 0) {
            sum += bit[index];
            index -= index & -index;
        }
        return sum;
    }
}

测试数据

public static void main(String[] args) {
        final FenWickTree tree = new FenWickTree(10);
        for (int i = 0; i < 10; i++) {
            tree.updateValue(i, i + 1);
        }
        System.out.println(tree.getSum(10));
        // 改成计算你n个1的和,注意因为是求和数组,所以传入的是增量
        for (int i = 0; i < 10; i++) {
            tree.updateValue(i, -i);
        }
        System.out.println(tree.getSum(10));
    }

  测试结果是55和10。

  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
状数组(Fenwick Tree,又称为Binary Indexed Tree)是一种基于数组实现的数据结构,用于高效地动态维护前缀和。它可以在O(log n)的时间内完成单点修改和查询操作,以及区间求和操作。 Fenwick Tree的实现原理比较简单,它使用一个数组来保存原始数据,同时维护另外一个数组用于计算前缀和。其中,数组中每个元素的值表示与当前位置相关的前缀和。为了方便计算,数组的长度通常比原始数据的长度大1。 Fenwick Tree的主要操作包括setValues函数用于设置原始数据的值,getSum函数用于查询某个位置的前缀和。具体实现中,setValues函数会更新原始数据数组的值,并同时更新前缀和数组的值。而getSum函数会根据当前位置的值以及前缀和数组的值,通过逐级向上累加来计算目标位置的前缀和。 Fenwick Tree的应用场景比较广泛,特别适用于需要频繁进行单点修改和区间求和操作的情况。比如,在一些算法问题中,我们需要不断更新某个位置上的值,并且需要快速计算某个区间的和。这时候,Fenwick Tree就能够提供高效的解决方案。 代码实现方面,可以参考引用和引用中的示例代码。它们展示了如何实现一个简单的Fenwick Tree,并且给出了一些测试例子来演示其使用方法。需要注意的是,具体的实现方式可能会根据具体的问题而有所差异,但基本原理都是相同的。 总结起来,Fenwick Tree是一种高效的数据结构,适用于需要频繁进行单点修改和区间求和操作的场景。它通过数组实现,并使用前缀和来提供快速的查询和更新功能。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *3* [【数据结构算法状数组](https://blog.csdn.net/zzy_NIC/article/details/130616434)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 50%"] - *2* [6.10 Fenwick](https://blog.csdn.net/m0_66201040/article/details/122923027)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

醒过来摸鱼

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值