树状数组细解

引言

树状数组(Fenwick Tree),也称为二进制索引树(Binary Indexed Tree),是一种高效的数据结构,用于处理数组的频繁更新和查询操作。它通过二进制位的特性实现高效操作,支持单点更新、区间求和、区间更新和单点查询等操作,时间复杂度为 \(O( \log n)\)

树状数组在算法竞赛和实际问题中有着广泛的应用,例如计算前缀和、区间查询、滑动窗口、逆序数统计等。

核心原理

核心思想

树状数组的核心思想是利用二进制位的特性,将数组划分为若干个长度为 \(2^i\) 的小区间,每个区间对应一个节点。树状数组的每个节点存储特定范围的和,通过 \(lowbit\) 函数(即 return \(x\) & \((-x)\) ) 实现索引的分散存储和查询。

例如,\(lowbit(x)\) 返回 的二进制表示中最低位的 1 的位置,用于确定更新和查询的步长

构造与操作

树状数组的构造基于二进制分解,每个节点存储一段连续区间的和。\(prefixSum\) 操作通过不断减去二进制最右边的 1 来累加结果;\(update\) 操作则通过在相应位置及更高层节点上累加值实现。构建树状数组时,先初始化全零数组,然后逐个位置调用 \(update\) 方法填充。所有数组下标从 \(1\) 开始。

时间复杂度

树状数组的更新和查询操作的时间复杂度均为 \(O( \log n)\),这使得它在频繁更新和查询的场景中非常高效

主要应用

单点更新与区间求和

树状数组最基础的应用是支持单点更新和区间求和。例如,通过 \(update\) 函数更新某个位置的值,通过 \(query\) 函数查询某个区间的和。这种操作在频繁更新和查询的场景中非常高效。

区间更新与单点查询

通过差分数组和树状数组的结合,可以实现区间更新和单点查询。例如,区间更新可以通过在区间起点和终点 \(+1\) 处进行加减操作来实现。

多维扩展

树状数组可以扩展到二维或三维,用于处理多维数组的更新和查询。例如,二维树状数组通过嵌套循环实现多维更新和查询。

实际应用

树状数组广泛应用于算法竞赛和实际问题中,如计算前缀和、区间查询、滑动窗口、逆序数统计等

代码示例

以下是一个简单的树状数组实现示例,展示了如何进行单点更新和区间查询:

#include <iostream>  
#include <vector>  
using namespace std;  

class FenwickTree {  
private:  
    vector<int> tree;    // 树状数组  
    int n;               // 数组大小  

    // 计算最低位的1 (Lowest Significant Bit)
    int lowbit(int x) {
        return x & -x;
    }

public:
    // 构造函数,初始化大小为 n+1 (索引从1开始)
    FenwickTree(int size) {
        n = size;
        tree.resize(n + 1, 0);
    }

    // 单点更新:将位置 i 的值增加 delta
    void update(int i, int delta) {
        while (i <= n) {
            tree[i] += delta;
            i += lowbit(i);
        }
    }

    // 前缀和查询:求前 i 个元素的和
    int query(int i) {
        int sum = 0;
        while (i > 0) {
            sum += tree[i];
            i -= lowbit(i);
        }
        return sum;
    }

    // 区间和查询:求 [left, right] 区间内的和
    int rangeQuery(int left, int right) {
        if (left > right) return 0;
        if (left == 1) return query(right);
        return query(right) - query(left - 1);
    }

    // 获取原始数组某个位置的值(通过两次前缀和查询)
    int getValue(int i) {
        return query(i) - query(i - 1);
    }

    // 设置原始数组某个位置的值(通过先获取原值再更新)
    void setValue(int i, int value) {
        int current = getValue(i);
        update(i, value - current);
    }
};

// 示例用法
int main() {
    // 创建大小为10的树状数组
    FenwickTree ft(10);
    
    cout << "初始状态: ";
    for (int i = 1; i <= 10; i++) {
        cout << ft.getValue(i) << " ";
    }
    cout << endl << endl;
    
    // 单点更新
    cout << "更新操作:" << endl;
    ft.update(3, 5);  // 位置3增加5
    ft.update(5, 7);  // 位置5增加7
    ft.update(7, 3);  // 位置7增加3
    
    cout << "更新后的数组: ";
    for (int i = 1; i <= 10; i++) {
        cout << ft.getValue(i) << " ";
    }
    cout << endl;
    
    // 前缀和查询
    cout << "\n前缀和查询:" << endl;
    for (int i = 1; i <= 10; i++) {
        cout << "前" << i << "个元素的和: " << ft.query(i) << endl;
    }
    
    // 区间和查询
    cout << "\n区间和查询:" << endl;
    cout << "[2, 5]区间和: " << ft.rangeQuery(2, 5) << endl;
    cout << "[4, 8]区间和: " << ft.rangeQuery(4, 8) << endl;
    cout << "[1, 10]区间和: " << ft.rangeQuery(1, 10) << endl;
    
    // 直接设置值
    cout << "\n设置值操作:" << endl;
    ft.setValue(4, 10);  // 设置位置4的值为10
    cout << "设置位置4为10后,数组: ";
    for (int i = 1; i <= 10; i++) {
        cout << ft.getValue(i) << " ";
    }
    cout << endl;
    
    cout << "[3, 6]区间和: " << ft.rangeQuery(3, 6) << endl;
    
    return 0;
}

优势与局限性

优势

1. 实现简单:树状数组实现简单,代码量少,时间复杂度低,适合频繁更新和查询的场景 
。

2. 高效:时间复杂度为 ,在频繁更新和查询的场景中非常高效 
。

3. 局限性:
不支持无交换率操作:树状数组不支持无交换率操作(如区间合并)。

4. 不支持动态扩展:树状数组不支持动态扩展(如动态添加元素)
  

未来发展趋势

更高维度

研究三维甚至更高维度的树状数组应用,以处理更高维度的数据结构。

并行计算

探索在多核和 \(GPU\) 上的并行实现,以提高大规模数据处理的效率。

动态大小

支持动态调整矩阵大小的实现,以适应不同规模的数据处理需求。

混合结构

与其它数据结构(如线段树)结合使用,以发挥各自的优势。

内存优化

针对稀疏矩阵的特殊优化,以提高内存使用效率。

总结

树状数组是一种高效的数据结构,适用于频繁更新和查询的场景。它通过二进制位的特性实现了高效的单点更新和区间查询,广泛应用于算法竞赛和实际问题中
。随着技术的发展,树状数组在更高维度、并行计算、动态扩展和内存优化等方面仍有广阔的发展空间。

参考资料

树状数组及应用

树状数组的拓展与应用

树状数组的扩展应用

posted @ 2026-01-01 20:40  jtbg  阅读(11)  评论(0)    收藏  举报