树状数组细解
引言
树状数组(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\) 上的并行实现,以提高大规模数据处理的效率。
动态大小
支持动态调整矩阵大小的实现,以适应不同规模的数据处理需求。
混合结构
与其它数据结构(如线段树)结合使用,以发挥各自的优势。
内存优化
针对稀疏矩阵的特殊优化,以提高内存使用效率。
总结
树状数组是一种高效的数据结构,适用于频繁更新和查询的场景。它通过二进制位的特性实现了高效的单点更新和区间查询,广泛应用于算法竞赛和实际问题中
。随着技术的发展,树状数组在更高维度、并行计算、动态扩展和内存优化等方面仍有广阔的发展空间。
参考资料
本文来自博客园,作者:jtbg,转载请注明原文链接:https://www.cnblogs.com/jtbg/articles/19411441
博客最新公告
浙公网安备 33010602011771号