树状数组详解(简单超基础超详细c++)

什么是树状数组
树状数组是 一 个查询和修改复杂度都为 log(n) 的数据结构。主要用于: 数组的单点修 改 、 区 间 求 和
在使用前缀和求区间和的算法中,如果可以做到在0(1)的时间复杂度内查询任意的区 间和,但是如果要修改其中 一个点的值,那么需要修改 一遍前缀和数组,修改的时间复杂度 是 0(n)
(1)树状数组拆分原理
正如所有的整数都可以表示成2 的幂和,我们也可以把 一 串序列表示成 一 系列子序列 的和。采用这个想法,我们可将 一 个前缀和划分成多个子序列的和,而划分的方法与数的2 的幂和具有极其相似的方式。
比如:整数21的对应的2进制是10101,对应计算=2⁴+2²+21,因此一个长度
为21的数组,可以拆分为三段:
在这里插入图片描述
子序列的个数是其二进制表示中1的个数。
根据该理论, 一 个长度为len 的 区 间 [ 1 ,len], 可以采用上述思想划分为log(len) 个 子 区 间 。
从右向左看,每个区间的大小其实是len 对应二进制的最后一个1的2的次幂

(2)如何求整数x 对应二进制的最后一个1往后的值?
lowbit(x): 代表x 对应2进制的从最后一位1开始向后构成的值
例如 :
lowbit(10): 返 回 1 0 。
lowbit(40): 返 回 1000。
计算方法 :
假设x=270 对应的2进制为:1011,1000。
先将x 取反 = 0100,0111, 再+1 = 0100,1000,此时发现这个值和原来的x 对应的 2进制,最后一个1向后的值一致,前面的值正好相反,只要拿这两个值做&运算,结果就 是lowbit(x) 的值。
在计算机中,负数是以补码的形式存储的,补码就是数值位取反+1的过程。
因此:lowbit(x)=x&(~x+1)=x &-x;

(3)树状数组的划分方法
以 a[x] 结尾的区间,区间长度为x 的 最 后 一 个 1所 对 应 的 2 的 次 幂 。
在这里插入图片描述
A. 以 a[x] 结尾的区间,区间长度为x 的最后一个1所对应的2的次幂。
B. 设定 c[x] 表示:a 数组中,长度为 lowbit(x) 的区间和,所管理的区间为[x-
lowbit(x)+1,x]。

C. 该结构的特点:
a. 除树根外,结点x 的 父 结 点 = x+lowbit(x);
b. 长度为n 的数组 a, 所构建的树的深度= log(n);
注意:树状数组的下标一般从1开始,因为如果从0开始, lowbit(0)=0, 容易出 现死循环。

(4)树状数组修改元素
在这里插入图片描述`
代码示例:

//在数组a的第x数上增加数字k
void_update(int x,int k){
	for(int i=x;i<=n;i=i+lowbit(i)) c[×]+=k;
}

(5)树状数组求前缀和
在这里插入图片描述

//求[1,x]的前缀和
int qurrey(int x){
	int res=0;
	for(int i=x;i>=1;i-=lowbit(i)){
		res+=c[i];
	}
	return res;
}

典型例题助理解:
在这里插入图片描述

#include<bits/stdc++.h>
using namespace std;

const int N=1e5+10;

int c[N];
int n,m; 

//求二进制后缀一的值
int lowbit(int x){
	return x&(-x);
}

//在数组的x位置加上p
void add(int x,int p){
	for(int i=x;i<=n;i+=lowbit(i)){
		c[i]+=p;
	}
}

//求和数组[1,x]的和
int qurrey(int x){
	int res=0;
	for(int i=x;i>=1;i-=lowbit(i)){
		res+=c[i];
	}
	return res;
}
int main(){
	scanf("%d%d",&n,&m);
	
	int x;
	for(int i=1;i<=n;i++){
		scanf("%d",&x);
		add(i,x);
	}
	
	int y,z;
	for(int i=1;i<=m;i++){
		scanf("%d%d%d",&x,&y,&z);
		if(x==0){
			printf("%d\n",qurrey(z)-qurrey(y-1));//数组[y,z]的和
		}else if(x==1){
			add(y,z);
		}
	}

    return 0;
}


树状数组(Binary Indexed Tree,简称 BIT)是一种高效处理**前缀和查询**和**单点更新**操作的数据结构,常用于解决**动态前缀和问题**。它的时间复杂度为 `O(log n)`,空间复杂度为 `O(n)`。 --- ### 基本操作 树状数组支持以下两种主要操作: 1. **单点更新(Add)**:将数组中某个位置的值加上一个数。 2. **前缀和查询(Sum)**:求前 `i` 个元素的和。 --- ### 树状数组的结构 树状数组的索引从 `1` 开始(为了利用二进制特性),每个节点维护的是原数组中某个区间的和。 树状数组有两个核心函数: - `lowbit(x)`:返回 `x` 的二进制表示中最右边的 `1` 所对应的值。例如 `lowbit(6)` = `2`(因为 `6` 的二进制是 `110`,最右边的 `1` 是 `10`)。 --- ### C++ 实现树状数组 ```cpp #include <iostream> #include <vector> using namespace std; class FenwickTree { private: vector<int> tree; // 树状数组(从索引1开始) int n; // 数组大小 // 获取x的最低位1 int lowbit(int x) { return x & -x; } public: // 构造函数,初始化数组大小为n 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); } } // 查询前缀和:sum[1..i] int query(int i) { int res = 0; while (i > 0) { res += tree[i]; i -= lowbit(i); } return res; } // 查询区间和:sum[l..r] int range_query(int l, int r) { return query(r) - query(l - 1); } }; int main() { int n = 5; FenwickTree ft(n); // 初始化数组:初始数组为 [1, 2, 3, 4, 5] vector<int> arr = {1, 2, 3, 4, 5}; for (int i = 0; i < n; ++i) { ft.update(i + 1, arr[i]); // 注意索引从1开始 } cout << "前缀和 sum[1..3] = " << ft.query(3) << endl; // 1+2+3=6 cout << "区间和 sum[2..4] = " << ft.range_query(2, 4) << endl; // 2+3+4=9 // 更新第3个元素(原值为3),加2 ft.update(3, 2); cout << "更新后区间和 sum[2..4] = " << ft.range_query(2, 4) << endl; // 2+5+4=11 return 0; } ``` --- ### 代码解释 1. `lowbit(x)`:用于找到 `x` 的最低位 `1`,是树状数组的核心操作。 2. `update(i, delta)`:从 `i` 开始向上更新父节点,直到出数组长度。 3. `query(i)`:从 `i` 开始向上累加,直到 `i == 0`,得到前缀和。 4. `range_query(l, r)`:利用前缀和差值快速求区间和。 5. `main()` 函数中演示了如何初始化数组、查询前缀和、区间和以及更新操作。 --- ### 应用场景 - 动态前缀和问题 - 频率统计 - 逆序对统计(如求解“逆序对”问题) - 二维树状数组扩展(处理二维前缀和) --- ###
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值