引入问题
给出一个长度为n的数组,完成以下两种操作
- 将第x个数加上k
- 输出区间[x,y]内每个数的和
使用暴力算法
- 单点修改:O(1)
- 区间查询:O(n)
对于大数据来说,这样的复杂度是不能接受的
树状数组处理:
- 单点修改:O(logn)
- 区间查询:O(logn)
可以应对非常大规模的数据
前置知识——lowbit()操作
lowbit()操作:非负整数n在二进制表示下最低为1及其后面的0构成的数值
例如:lowbit(44) = lowbit( (101100)2) =(100)2 =4
求101100
的lowbit值过程
- 按位取反
010011
- 然后加一
010100
- 然后与原来的数相与:
000100
由于二进制在计算机中是用补码存储的,因此(非负整数)N
按位取反加一就是-N
,故lowbit(N) = N&(~N+1) = N&(-N)
;
树状数组——思想及实现
区间查询 ——> 前缀和 ——> 树结构维护(logn)
因为求得是区间和,所以很容易想到用前缀和相减的方法,如果使用树结构来维护,那么就可以将复杂度降低到logn
该树状数组特性
- 每个节点t[x]保存以x为根的子树中的叶节点(即原数组的每个元素)值的和
- t[x]节点覆盖的长度就是
lowbit(x)
- t[x]节点的父节点为
t[x+lowbit(x)]
- 整棵树的深度为log2n+1
add(x,k)
操作,即将a[x]加上k
- 需要处理每一层的一个树状数组的数据
- 最坏时间复杂度log2n
ask(x)
操作,即查询a[1]~x[7]的和,即前缀和
- 最坏时间复杂度log2n
- 向左上找上一个节点,只需要将下标-=lowbit(index)即可
- 如果需要求区间和,则计算出两个前缀和,然后相减即可
示例
可以通过前缀和来初始化树状数组
//树状数组
#include <iostream>
using namespace std;
const int N=10;
int arr[11]={0,1,4,2,5,10,3,4,0,1,0}; //0号位置舍弃不用
int treeArr[11]; //0号位置舍弃不用
int prefix[11]; //记录前缀和,用于树状数组初始化
int lowbit(int x){
return x&(-x);
}
void init(){
prefix[1]=arr[1];
for(int i=2;i<=N;i++){
prefix[i]=arr[i]+prefix[i-1];
}
for(int i=1;i<=N;i++){
//treeArr[i]是以i为根的树的所有叶节点的和
treeArr[i]=prefix[i]-prefix[i-lowbit(i)];
}
}
//对a[i]增加x
void add(int i,int x){
while(i<=N){
treeArr[i]+=x;
i+=lowbit(i);
}
}
//查询1到x之间的区间和即前缀和
int ask(int x){
if(x==0)
return 0;
int sum=0;
while(x>0){ //注意这里一定要是>0
sum+=treeArr[x];
x-=lowbit(x);
}
return sum;
}
//计算坐标a到b之间的区间和
int interval(int a,int b){
return ask(b)-ask(a-1);
}
int main(){
init();
while(1){
cout<<"0:add 1:interval"<<endl;
int opt;
cin>>opt;
switch(opt){
case 0:
cout<<"enter index and addValue:"<<endl;
int index,value;
cin>>index>>value;
add(index,value);
break;
case 1:
cout<<"enter left and right boundary:"<<endl;
int l,r;
cin>>l>>r;
cout<<interval(l,r)<<endl;
break;
default:
return 0;
}
}
return 0;
}
总结
树状数组是动态维护前缀和的工具,最基本的用途是进行区间和查询和单点修改(均是logn的时间复杂度),此外还可以进行区间修改、单点查询;区间修改、区间查询等。
本文根据该视频的讲解总结而来,地址如下https://www.bilibili.com/video/BV1pE41197Qj?from=search&seid=13604527415584361816