【数据结构与算法】树状数组

Fenwick Tree


树状数组(Binary Indexed Tree,又称 Fenwick Tree)是一种基于数组实现的数据结构,用于高效地动态维护前缀和。

树状数组可以在 O ( log ⁡ n ) O(\log n) O(logn) 的时间复杂度内进行单点修改前缀求和操作,适用于一些仅需要维护前缀和的问题,例如区间求和、区间平均数、区间中位数等。

一、问题描述

对于给定数组 a 1 , a 2 , . . . , a [ n ] a_{1},a_{2},...,a[n] a1,a2,...,a[n],当需要频繁询问某一区间 a i , a i + 1 , . . . , a [ j ] a_{i},a_{i+1},...,a[j] ai,ai+1,...,a[j] (其中 1 < = i < j < = n 1<=i<j<=n 1<=i<j<=n),且存在修改 a i a_{i} ai 元素值的情况时,如何设计一种高效的查询和修改算法?

一种直观的做法是暴力法,即每次使用 O ( 1 ) O(1) O(1) 的时间复杂度进行单点修改,但是查询的过程中由于需要遍历区间中的每个元素,最坏情况需要 O ( n 2 ) O(n^{2}) O(n2) 的时间(这里是假设操作的规模是 O ( n ) O(n) O(n) )。为了解决这种查询效率低下的情况,一种思路是使用树状数组。

二、解决思想

在树状数组 d d d 中,每个数组位置所存储的值并不仅仅是当前元素的值,而是一定区间内元素的累加值,具体存储规则如下图左图示所示。

在这里插入图片描述

左图示中,元素下标对应的矩阵表示该位置存储的值为矩阵覆盖区域对应的原数组元素之和,比如下标为 1 1 1 处存储原数组下标为1的元素值,下标为4处存储原数组下标为 1 1 1 4 4 4 的元素值之和。

树状数组的存储图中反映的存储规律为:

编号为 x x x 的节点所管辖的区间为 2 k 2^{k} 2k(其中 k k k x x x 二进制末尾 0 的个数)个元素。

比如 d [ 6 ] = a [ 5 ] + a [ 6 ] d[6] = a[5] + a[6] d[6]=a[5]+a[6],因为位置 6 对应的二进制为 110,末尾的 0 个数为 1,因此需要存储 2 1 = 2 2^{1} = 2 21=2 个元素,其他同理。

有了上述的存储规则后,如何进行区间查询和元素修改操作呢?为了便于对操作的描述,可将左侧图示进行旋转获得右侧图示(树的深度为 l o g 2 n + 1 log_{2}{n+1} log2n+1),并用直线连接表示前缀累加节点之间的关联关系。另外,为了便于后续对区间查询和元素修改操作的描述,这里引入 lowbit 操作:

public int lowbit(int x) {
    return x & (-x);
}

即所谓的 lowbit 操作就是为了获取当前变量 x x x 二进制中最后一个 1 的位置。这个操作可以先记住,后续的操作介绍中能帮助进一步理解。

(1)区间查询

在右侧图示的树结构中,若查询某个节点的前缀和,则需要从这个点向左上找到上一个节点,并加上其节点的值,以此不断向左上查找节点值并累加。比如,查询 节点 7 的前缀和,需要从 节点7 出发, 找到 节点 6 进行累加操作,再找到 节点 4 进行累加操作(可以结合图中矩阵的覆盖面积进行理解)。那么这一遍历过程是如何实现的呢?这需要我们先将节点编号转成二进制的形式:

  • 7 ---- 0111
  • 6 ---- 0110
  • 4 ---- 0100

可以发现一个普遍的规律:下一次遍历的位置 x ′ x' x 是当前位置 x x x 在二进制位上抹去最后一个 1 1 1。因此,这时就可以使用到我们之前提到的 lowbit 操作了,即对于下一次遍历的位置 x ′ x' x 有: x ′ = x − l o w b i t ( x ) x' = x - lowbit(x) x=xlowbit(x)

(2)元素修改

元素修改可以看作是区间查询的逆过程,即在右侧图示的树结构中,若修改其中的某一节点,则需要一层一层向上找到父节点(向右上方,与区间查询正好相反),并按照需要对每个父节点进行相同的修改处理。比如,对 节点 3 进行加 k k k 操作,则需要依次遍历 节点 3节点 4节点 8 并在遍历的过程中进行加 k k k 操作。在遍历下一个节点的过程中,只需要使用与区间查询类似的逆向过程即可,即对当前 x x x 位置节点,下一遍历节点 x ′ = x + l o w b i t ( x ) x' = x+lowbit(x) x=x+lowbit(x)

三、代码实现

class FenwickTree {
    private int[] tree;
    
    public FenwickTree(int n) {
        tree = new int[n+1];
    }
    
    public int lowbit(int x) {
        return x & (-x);
    }
    
    public void add(int i, int val) {
        while(i < tree.length) {
            tree[i] += val;
            i += lowbit(i);
        }
    }
    
    public int query(int i) {
        int res = 0;
        while(i > 0) {
            res += tree[i];
            i -= lowbit(i);
        }
    }
}
  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
很抱歉,根据提供的引用内容,没有找到关于迪杰斯特拉算法树状数组优化的信息。但是,我可以为您介绍一下迪杰斯特拉算法树状数组的基本概念和应。 迪杰斯特拉算法,也称为狄克斯特拉算法,是一种用于解决图中最短路径问题的算法。该算法的基本思想是从起点开始,逐扩展到距离起点更远的节点,直到到达终点为止。在扩展的过程中,通过比较不同路径的距离,选择距离最短的路径进行扩展,直到到达终点。 树状数组,也称为二叉索引树,是一种用于高效维护序列前缀和的数据结构。该数据结构可以在O(log n)的时间内完成单点修改和区间查询操作,因此被广泛应用于解决各种算法问题,如逆序对问题、区间最大值/最小值问题等。 虽然迪杰斯特拉算法树状数组看似没有直接关系,但是在解决某些特定的问题时,两者可以结合使用,以达到更高效的解决方案。例如,在解决带权图最短路径问题时,可以使用迪杰斯特拉算法结合树状数组进行优化,以达到更快的计算速度。 具体来说,可以使用树状数组维护一个优先队列,用于存储当前已经扩展的节点和它们的距离。在每次扩展节点时,可以使用树状数组快速找到距离最小的节点,并将其从队列中删除。这样可以避免使用传统的堆数据结构,从而提高算法的效率。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值