【数据结构】树状数组 萌新的树状数组学习之路
首先从废话引入
在介绍树状数组前,我们先观察这样一个标准的树状数组板子题:【洛谷】--树状数组1
在不考虑数据范围的前提下,我们可以想到暴力的算法:使用数组存数,每次修改单点修改,每次查询都遍历一次
这样我们的时间复杂度:修改:O(1)
,查找O(M*N)
//存数
int n,m;cin>>n>>m;
int arr[N];
for(int i=0;i<n;i++)
cin>>arr[i];
//修改
cin>>i>>x;
arr[i]+=x;
//查询
int sum=0;
for(int i=l;i<=r;i++)
sun+=arr[i];
cout<<sum<<endl;
显然这样的查找方式有些太慢,于是我们可以使用一个数组维护前缀和,从而使查询的时间复杂度降为O(1)
,可相应的,修改的时间复杂度又变为了O(N)
//存数
int n,m;cin>>n>>m;
int arr[N];
int c[N];//另开一个数组维护前缀和
for(int i=0;i<n;i++){
cin>>arr[i];
c[i]=arr[i];
if(i>0)c[i]+=c[i-1];
}
//修改
cin>>i>>x;
while(i){
c[--i]+=x;
}
//查询
cin>>l>>r;
cout<<c[r]-c[l]<<endl;
然而考虑到洛谷给的数据范围,显然我们不能用暴力的方式水过:
【数据范围】
对于的数据, , ;
对于的数据, ;
对于的数据, 。
此时我们就要用树状数组来进行优化了,树状数组是一种用于维护区间增删改查的数据结构,通过二进制优化前缀的方式,将修改和查找的时间复杂度都保持在O(logN)
首先我们来看两种暴力思路各自的优劣
直接暴力 便于修改,可是查找需要遍历整个区间
前缀和 查找只需要进行两个区间和相减,但是修改时需要遍历整个区间来修改区间中每个位置的对应前缀和
我们知道程序的运行时间取决于各部分运行时间最长的部分,因此以上两种方式都是O(N)
的
那我们最好是有一种方法,可以同时较快的修改和查找(废话qwq),此时我们想到可以借助分块的思想,将每一个区间元素分成若干个块,再通过维护各自分块来查询和修改的时间复杂度。
关于如何划分,我们就要引入lowbit()了
lowbit
说lowbit是树状数组的灵魂一点也不为过,虽说我们可以很轻易的写出代码实现,但在此我们先介绍其原理
点击查看代码
int lowbit(int x){
return x&(-x);
}
首先我们将前缀和数组的下标写作二进制的形式,可以得出如下形式:
1 -> 1
2 -> 10
3 -> 11
4 -> 100
5 -> 101
6 -> 110
7 -> 111
8 -> 1000
我们可以看出,当我们将数字N
写成二进制时,这个二进制数的位数是不会大于logN+1
的,因此我们可以通过二进制分解下标的方式来确定我们要如何划分
我们将每个二进制位
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通